Writing a Plugin
While we have published a majority of the plugins we have developed, there will always be new use cases that may require new functionality. Writing a plugin in the easiest way to extend Player functionality for these cases. Plugins work slightly differently on each platform so in this guide we will cover how to write a plugin for each platform.
Core plugins are the easiest way to extend Player functionality regardless of what platform you are using Player on. To make writing core plugins easy @player-ui/player
exposes an interface PlayerPlugin
that denotes everything needed. The two mandatory features are a name
property which is lets Player know how to refer to the plugin and an implemented apply
function that takes a player
object. Optionally a symbol
property can be used to provide a unique identifier that can be used to retrieve the plugin from Player.
The first step for creating a plugin is making our plugin class, making sure it implements the PlayerPlugin
interface from @player-ui/player
. By convention, a name attribute with the dash-cased name of your plugin should be defined.
export default class ExamplePlayerPlugin implements PlayerPlugin { name = 'example-player-plugin';
{/* A constructor can go here */}
apply(player: Player) { {/* Your logic here */} }
{/* Helper methods can go here */}}
The apply
function is where the actual logic of the plugin lives. By tapping the hooks exposed via player.hooks
you gain access to the internal pipeline of components that comprise Player and can inject your functionality into their exposed hooks. For example if you want to do something any time Player’s state changes you could do the following:
apply(player: Player) { player.hooks.state.tap(this.name, (state) => { {/* Your logic here */} });}
Some components expose hooks themselves which may require multiple levels of taps which is not uncommon. For example if you wanted to modify the ViewInstance
before it was resolved you would do the following:
apply(player: Player) { player.hooks.viewController.tap(this.name, (vc) => { vc.hooks.view.tap(this.name, (vc) => { {/* Your logic here */} }) });}
It is not uncommon for core plugins to have constructors for cases where the plugin needs to take some configuration. In cases where plugin configs are more complicated than basic feature flags, it is recommended to make an interface to represent the config object. As an added benefit it also makes it easier to down stream consumers to use your plugin.
For a more comprehensive guide on plugins, check out this Plugin Implementation example.
Note: For the React Player you can import and load the plugin the same way you would a React Player Plugin but for the iOS and Android Players you will need to wrap the javascript bundle in a iOS/Android plugin to ensure it is available on your platform.
React Player Plugins are very similar to core plugins in both their composition and use. The @player-ui/react
package exposes an interface ReactPlayerPlugin
that, much like the PlayerPlugin
interface provides the necessary attributes that are required for a React Player plugin. Again a dash-cased name
attribute should be used by convention, and a function applyReact
is required that takes a ReactPlayer
instance. Similarly to core plugins in the applyReact
function you have access to the React Player object and access to the three exposed hooks:
- The
webComponent
hook allows you to modify a React component that is stored in the React Player for use when it renders content. This happens during the initialization phase and ise useful if you want to wrap components in various content providers. - The
playerComponent
hook allows you to modify a component or execute functionality when the React Player is rendering a component after the view has been reconciled in Player. This is useful if you want to inject additional props to components or collect data on which component was rendered. - The
onBeforeViewReset
hook is fired when the view is resetting to undefined and you want to execute some asynchronous tasks.
Below is an example of a basic ReactPlayerPlugin
that would expose a function to every component that gets loaded in the React Player:
export const FunctionContext = React.createContext<{ function: ();}>({ handler: () => {} });
export const useFunction = (): () => { const { function } = React.useContext(FunctionContext); return function;};
export class FunctionPlugin implements ReactPlayerPlugin { name = 'function-plugin'; private function: ();
constructor(fn = defaultFunction) { this.function = fn; }
applyReact(rp: ReactPlayer) { rp.hooks.webComponent.tap(this.name, (Comp) => { const { function } = this;
return () => ( <FunctionContext.Provider value={{ function }}> <Comp /> </FunctionContext.Provider> ); }); }
{/* Helper methods can go here */}}
Lastly React plugins can also act as a core plugin in cases where core functionality needs to be extended for the React plugin to work. Since both the PlayerPlugin
and ReactPlayerPlugin
are typescript interfaces a plugin can implement both and be considered a valid plugin.
Android Plugins
Section titled “Android Plugins”For all types of plugins, the apply(instance: T)
signature is the standard way of getting the instance of whatever the plugin is applying to. It is best practice that this method remains idempotent, but that doesn’t mean that plugins can’t maintain state to hold a reference to said instance
. Different types of plugins are handled at different stages of initialization, but the order of the same types of plugins will be preserved when applying the plugins.
Android Player Plugins
Section titled “Android Player Plugins”Android Player plugins must implement the AndroidPlayerPlugin
interface, which will give them access to an AndroidPlayer
instance through the apply
method. Plugins can use this instance to register UI assets, apply themes, or even access the Player hooks.
Likely the most common usecase for plugins is to provide UI through assets. More information can be found in the Custom Assets guide.
JVM Player Plugins
Section titled “JVM Player Plugins”Player plugins that are only required for the JVM use cases should implement the PlayerPlugin
interface. This provides a limited Player
wrapper of the core JS Player, which are exposed through the Player hooks
. A basic example of PlayerPlugin
to handle errors:
class HandleErrorPlayerPlugin : PlayerPlugin { override fun apply(player: Player) { player.hooks.state.tap { state -> if (state is ErrorState) { // handle error } } }}
For more practical examples, take a look at the coroutines plugins which tie asynchronous Player paradigms to Kotlin Coroutines constructs.
JS Player Plugins
Section titled “JS Player Plugins”Given the core Player is written in TypeScript incidentally means that most Player plugins will also be written in TypeScript. Loading these on the JVM platform requires some additional setup. At the very least, the plugin will need to be bundled with its dependencies and transpiled according the the target JS runtime.
Once you have your bundle, all that is left is implementing the wrapper on the JVM. The plugin wrapper should implement the JSPluginWrapper
, which includes an apply(runtime: Runtime)
method to provide the Runtime
in which to instantiate the JS Player plugin within.
For plugins that do not have any constructor arguments, the JSScriptPluginWrapper
can be used to simplify the overhead of instantiated that plugin. It is an abstract class that implements of the JSPluginWrapper
. This can be instantiated with the loaded bundle as a string or the location of the bundle on the classpath and will automatically read the bundle and instantiate the plugin within the JS runtime.
class MyCorePlugin : JSScriptPluginWrapper("MyCorePluginName", sourcePath = "path/to/source.js") { // Expose a member of the core instance public val someField: String by NodeSerializableField(String.serializer())
// Privately declare loosely typed-function of the core instance private val someFunc: Invokable<String> by NodeSerializableFunction() // Expose strictly typed method for consumption public fun someFunc(p1: String): String = someFunc(p1)}
Runtime Plugins
Section titled “Runtime Plugins”RuntimePlugin
s are supertypes of the JSPluginWrapper
for JS Player plugin wrappers. At this layer, they can be used to add non-player specific functionality to the JS runtime. For example, the BeaconPlugin
requires the setTimeout
global method, which does not exist by default in some runtimes. Thus, the BeaconPlugin
applies the SetTimeoutPlugin
, which is a RuntimePlugin
, before it instantiates the actual JS Player beacon plugin.
Plugin Order of Operations
Section titled “Plugin Order of Operations”With all these different types of plugins, it may be difficult to understand how they are treated and when each is applied. Each of these plugin types implements the base Plugin
interface, which provides a bounding layer for any type of plugin. Both the Android and JVM Players accept a collection of Plugin
s, which may or may not be specific to that Player. This allows non-player plugins, such as the RuntimePlugin
to be passed into the instantiation of either Player, simplifying the overhead of using such plugins. During the different layers of initialization, the internals will handle each type of plugin that it cares about. Because the internals are expecting certain types of plugins, any direct implementation of the base Plugin
is certainly an error. Below is a detailed sequence diagram describing the order in which plugins are applied.

iOS Plugins
Section titled “iOS Plugins”iOS Player Plugins are very similar to core and react plugins in both their composition and use.
NativePlugin
Section titled “NativePlugin”The PlayerUI/Core
subspec exposes an interface NativePlugin
that, much like the core PlayerPlugin
interfaces, provides the necessary attributes that are required for an iOS Player plugin. A pluginName
attributed is required, and a function apply
is required that takes an instance of a Player implementation. Similarly to core plugins, in the apply
function you have access to the Player object and access to the hooks. apply
uses generics to future proof so plugins can be used for multiple Player implementations should they be created.
The player
passed to apply
exposes hooks from the core player, as well as hooks specific to that player implementation. For the current state of this project, the SwiftUIPlayer
is the primary iOS Player, and exposes two hooks for the SwiftUI layer specifically:
- The
view
hook allows you to modify the root view that will be displayed in the SwiftUIPlayer body. This is useful for applying changes to the environment for the SwiftUI view tree, or apply ViewModifiers and such. - The
transition
hook allows you to specify aPlayerViewTransition
object to be applied when the flow transitions from one view to another, to animate the transition.
Basic Example
Section titled “Basic Example”Below is an example of a basic NativePlugin
that sets a value in the EnvironmentValues when the plugin is included:
import PlayerUI
class EnvironmentPlugin: NativePlugin { let pluginName = "EnvironmentPlugin"
func apply<P>(player: P) where P: HeadlessPlayer { guard let player = player as? SwiftUIPlayer else { return } player.hooks.view.tap(name: pluginName) { (view: AnyView) -> AnyView in return AnyView(view.environment(\.someEnvironmentKey, someValue)) } }}
Likely the most common usecase for plugins is to provide UI through assets. More information can be found in the Custom Assets guide.
JSBasePlugin
Section titled “JSBasePlugin”Building native features on top of shared functionality is one of the primary benefits of using player. As such we expose convenience utilities to enable loading JavaScript Player Plugins as the base for your NativePlugin
.
Basic Setup
Section titled “Basic Setup”This example will load the SharedJSPlugin
in the JavaScript layer when included as a plugin to SwiftUIPlayer
.
import PlayerUI
class SharedJSPlugin: JSBasePlugin { convenience init() { // pluginName must match the exported class name in the JavaScript plugin self.init(fileName: 'shared-js-plugin-bundle', pluginName: 'SharedJSPlugin') }
// Construct the URL to load the JS bundle override open func getUrlForFile(fileName: String) -> URL? { ResourceUtilities.urlForFile( fileName: fileName, ext: "js", bundle: Bundle(for: YourPlugin.self), pathComponent: "YOUR_POD.bundle" ) }}
Arguments for constructing the JavaScript plugins
Section titled “Arguments for constructing the JavaScript plugins”To simplify the ease of use, the JSContext
for JSBasePlugin
implementations is provided when the underlying resources for the core player
are being setup. This means that we need to provide the arguments for the JavaScript constructor late. To do this, override the getArguments
function:
import PlayerUI
class SharedJSPlugin: JSBasePlugin { var option: Boolean
convenience init(option: Boolean) { // pluginName must match the exported class name in the JavaScript plugin self.init(fileName: 'shared-js-plugin-bundle', pluginName: 'SharedJSPlugin') self.option = option }
override open func getArguments() -> [Any] { // plugin just takes a boolean arguments return [option] // More common in JavaScript, constructor takes an object return [["enable": option]] }}
Note: As JavaScriptCore
cannot resolve dependencies at runtime, using a module bundler such as tsup is required.
Note: JSBasePlugin
implementations do not necessarily need to be a PlayerPlugin
, for example, the BeaconPlugin can take plugins in it’s constructor, that are not PlayerPlugin
.