Video.js ❤ Typescript

joeflateau

Joe Flateau

Posted on November 1, 2019

Video.js ❤ Typescript

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.

First things first…

All of the code found in this post is available in this StackBlitz for you to fork and try things out.

Using Video.js in your Typescript Project

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', {})

Adding Typescript support for someone else’s Video.js plugin

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;
}

View in StackBlitz

I use the types videojs.Player and VideoJsPlayer interchangeably. videojs.Player is a type alias for VideoJsPlayer so they both refer to the same interface. But you cannot use module augmentation to modify videojs.Player directly. You need to augment VideoJsPlayer which will modify videojs.Player

Adding Typescript support to your Video.js plugin

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
}

Submitting to DefinitelyTyped

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.

Writing a Video.js plugin in Typescript

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;
}

View in StackBlitz

Conclusion/Footnote

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.

💖 💪 🙅 🚩
joeflateau
Joe Flateau

Posted on November 1, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Video.js ❤ Typescript
angular Video.js ❤ Typescript

November 1, 2019