I’ve used video.js for about 4 years now, with a brief break where I used a proprietary third party video player. I also love using Typescript. I find the safety the type system provides to be extremely helpful once a web app gets past a certain level of complexity or when time to refactor comes.
Video.js has a great plugin ecosystem, unfortunately, the plugin model was not super compatible with Typescript. It requires casting the videojs.Player
object to any
which, if you know Typescript, you know means you lose the type checking Typescript provides.
I decided to do something about it, and started by making some changes to the open source typings of video.js in the DefinitelyTyped repository. The biggest change was exporting a few key interfaces in a way that would allow module augmentation. Now your video.js plugins can extend the core video.js interfaces giving type-safe use of these plugins.
All of the code found in this post is available in this StackBlitz for you to fork and try things out.
Let’s start with the basics. Let’s create an html video element, install video.js (along with the types from DefinitelyTyped) and initialize a video.js player object.
index.html
<video id="video" class="video-js" width="500" height="320" controls>
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4"></source>
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm"></source>
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg"></source>
</video>
Install dependencies
$ npm i video.js @types/video.js
index.ts
import videojs from 'video.js';
const player = videojs('video', {})
Now let’s try to use a third party video.js plugin. For this example we’ll use the videojs-seek-buttons plugin.
Install the plugin…
$ npm i videojs-seek-buttons
Now the plugin can be imported. But you can’t actually use it without casting your video.js player object to any
. The following code will NOT work.
index.ts
import videojs from 'video.js';
import 'videojs-seek-buttons'; // <-- this imports and registers the plugin
const player = videojs('video', {});
// this next line will not actually work because videojs.Player has no method seekButtons
player.seekButtons({ forward: 15, back: 15 });
We need to tell Typescript that this seekButtons
method exists on the VideoJsPlayer
object. To do so, we create d.ts
file that augments the VideoJsPlayer
interface. We’ll also augment VideoJsPlayerPluginOptions
*. Create a new file videojs-seek-buttons.d.ts
(the file name does not actually matter though, you could name it anything.) The contents will be:
videojs-seek-buttons.d.ts
import { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js';
declare module 'video.js' {
// this tells the type system that the VideoJsPlayer object has a method seekButtons
export interface VideoJsPlayer {
seekButtons(options: VideoJsSeekButtonsOptions): void;
}
// this tells the type system that the VideoJsPlayer initializer can have options for plugin seekButtons
export interface VideoJsPlayerPluginOptions {
seekButtons?: VideoJsSeekButtonsOptions;
}
}
export interface VideoJsSeekButtonsOptions {
forward?: number;
back?: number;
}
If you maintained the videojs-seek-buttons project, you would add the type definition found above to your project and reference it in the types
property of your package.json .
package.json
{
"name": "videojs-seek-buttons",
"version": "0.0.0",
"dependencies": {
...
},
"types": "types/index.d.ts" // <---- wherever you saved that file in your plugin project
}
If the maintainer of the plugin does not package the type definition with the project, you can submit them to be included in DefinitelyTyped. See the DefinitelyTyped Contribution Guide: Create a new package for instructions on how.
So that was pretty cool, now we can use a plugin authored in Javascript (or coffeescript) in Typescript. What if we now wanted to author a plugin in Typescript?
It’s not too different to author the plugin in Typescript. You still need to augment the video.js module. Just add that to the end of your main plugin file.
This plugin will render a button in the player controlBar with a text label and clicking it will trigger an alert box.
example-plugin.ts
import videojs, { VideoJsPlayer } from 'video.js';
const Button = videojs.getComponent('button');
// implement our Button
export class VideoJsExampleButton extends Button {
static defaultOptions: VideoJsExamplePluginOptions = {
label: "Default Label",
message: "Default Message"
};
private exampleOptions: VideoJsExamplePluginOptions;
constructor(
player: VideoJsPlayer,
exampleOptions: Partial<VideoJsExamplePluginOptions> = {}
) {
super(player);
this.exampleOptions = {
...VideoJsExampleButton.defaultOptions,
...exampleOptions
};
this.el().innerHTML = this.exampleOptions.label;
}
createEl(tag = 'button', props = {}, attributes = {}) {
let el = super.createEl(tag, props, attributes);
return el;
}
handleClick() {
alert(this.exampleOptions.message);
}
}
videojs.registerComponent('exampleButton', VideoJsExampleButton);
const Plugin = videojs.getPlugin('plugin');
// implement the plugin that adds the button the the controlBar
export class VideoJsExamplePlugin extends Plugin {
constructor(player: VideoJsPlayer, options?: VideoJsExamplePluginOptions) {
super(player);
player.ready(() => {
player.controlBar.addChild('exampleButton', options);
});
}
}
videojs.registerPlugin('examplePlugin', VideoJsExamplePlugin);
declare module 'video.js' {
// tell the type system our plugin method exists...
export interface VideoJsPlayer {
examplePlugin: (options?: Partial<VideoJsExamplePluginOptions>) => VideoJsExamplePlugin;
}
// tell the type system our plugin options exist...
export interface VideoJsPlayerPluginOptions {
examplePlugin?: Partial<VideoJsExamplePluginOptions>;
}
}
export interface VideoJsExamplePluginOptions {
label: string;
message: string;
}
Now you can use your new plugin authored in Typescript. You can either pass the plugin options in the player options object on creation or call the plugin method after initialization.
index.ts
import videojs from 'video.js';
import "./example-plugin.ts";
import "videojs-seek-buttons";
const player = videojs('video', {
plugins: {
examplePlugin: { message: "Here's a message!" },
seekButtons: {
forward: 30,
back: 10
}
}
})
const player2 = videojs('video2', {})
player2.examplePlugin({
label: "Custom button label",
message: "Here's a message from player 2!"
});
player2.seekButtons({
forward: 30,
back: 10
});
It’s my hope that this guide will spur on the adoption of Typescript within the video.js community and the use of video.js within the Typescript community.
* as of Fri. Oct. 18, 2019 the change to export VideoJsPlayerPluginOptions
has not been merged into @types/video.js quite yet. So you will not see type safety in your video.js setup plugin options yet.