自定义插件接入

1. 插件规范

插件能提供极大的权限,这是一把双刃剑,它可能会引入很多意料之外的问题,因此务必遵循以下原则:

  • 对外方法统一挂载到外层播放器实例上

2. 播放器对象模型

在编写插件之前,了解播放器对象模型,对编写插件有极大的帮助:

  1. 直通
┌─renderer                 #  播放器核心模块
│  ├─wrapper               #  播放器最外层的 dom, 是 container 的父级节点
│  ├─container             #  播放器渲挂载节点(一般也用于插件 dom 挂载),是渲染 dom 的父级节点
│  ├─dom                   #  视频渲染 dom(canvas | video)
│  └─decoder               #  解码模块
        └─renderer         #  渲染管理器
            ├─decodeType   #  渲染器类型
            ├─decodeType   #  渲染器当前播放时间
            └─vm           #  渲染器(mse渲染器 | webGL渲染器)
├─session                  #  s17 接口返回的播放 session
├─streamType               #  码流类型
└─volume                   #  音量
  1. 回放
┌─channelPlayers[]            #  通道播放器列表, v1.1 新增,取代 v1.0 的 mediaElements
│  ├─renderer                 #  播放器渲染模块
│  │   ├─wrapper              #  播放器最外层的 dom, 是 container 的父级节点
│  │   ├─container            #  播放器渲挂载节点(一般也用于插件 dom 挂载),是渲染 dom 的父级节点
│  │   ├─dom                  #  视频渲染 dom(canvas | video)
│  │   └─decoder              #  解码模块
│  ├─session                  #  s17 接口返回的播放 session
│  ├─channel                  #  通道号
│  ├─devId                    #  设备号号
│  ├─streamType               #  码流类型
│  ├─currentTime              #  当前时间,utc 秒
│  └─volume                   #  音量
├─streamType                  #  码流类型
├─currentTime                 #  当前时间,utc 秒
├─volume                      #  音量
├─status                      #  状态
└─requestParams               #  s17 接口请求参数

3. 创建自定义插件

3.1. 简介

创建插件非常简单,仅需两步:

  1. 创建一个函数,该函数接受一个 hooks 参数
export function customPlugin(hooks) {
    // do something ...
}

tips: 插件本身是一个方法,没有作为构造函数,因此不具有实例。

  1. 将插件注册到我们的播放器中
player.plugin.use(customPlugin);

小伙伴们都惊呆了吧! 其实插件的主体部分才是重点,下面我们以全屏插件为例,逐步带领大家深入插件

3.2. 实现全屏插件

插件函数中接受hooks参数,它提供了我们介入播放器各个生命周期(hooks)的入口。

1.分析需求 全屏插件需要为播放器拓展两个方法,分别用来进入全屏/取消全屏

2.确定拓展方法的时机 这两个方法需要在播放器较为靠前的时机拓展上,以便我们能尽早调用,因此最终从hooks中,选择afterPlayerInit hook,即:播放器初始化后,作为我们介入的时机。

3.拓展方法 afterPlayerInit 为其回调注入了两个参数player, config,这里我们仅需要用到player,它表示当前播放器实例,我们可以直接在其上新增我们的方法或者属性:

export function fullscreen(hooks) {
    hooks.afterPlayerInit.tap('install-fullscreen', (player) => {
        player.fullscreen = function () {
            // ...
        };
        player.cancelFullscreen = function () {
            // ...
        };
    });
}

tips: 每个 hook 回调里,都注入了一些参数,具体见hooks。 tips: 这里 tap 方法是由tapable提供

4.实现 fullscreen 函数

function fullscreen() {
    let canFullScreen = true;
    var mediaElement = this.renderer.container;
    if (mediaElement.RequestFullScreen) {
        mediaElement.RequestFullScreen();
    } else if (mediaElement.webkitRequestFullScreen) {
        mediaElement.webkitRequestFullScreen();
    } else if (mediaElement.mozRequestFullScreen) {
        mediaElement.mozRequestFullScreen();
    } else if (mediaElement.msRequestFullscreen) {
        mediaElement.msRequestFullscreen();
    } else {
        canFullScreen = false;
        alert("This browser doesn't supporter fullscreen");
    }
}

5.实现 cancelFullscreen 函数

function cancelFullscreen() {
    let canFullScreen = true;
    if (document.exitFullscreen) {
        document.exitFullscreen();
    } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
    } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
    } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
    } else {
        canFullScreen = false;
        alert("Exit fullscreen doesn't work");
    }
}

6.最后一步,将其注册到播放器上

player.plugin.use(fullscreen);

至此,就实现了一个完整的全屏插件!

3.3. 实现放大镜插件

1.需求分析 我们需要一些 dom 和对应的交互,用来选取一个范围,另外还需要更改我们的渲染器行为,用来将第一步选取的范围,应用到视频上,将其放大。

2.确定 dom 及对应交互的添加时机 需要添加的 dom,需要在视频渲染之前,但又必须在播放器初始化时,或者初始化后(合理即可),这里我们仍旧选择播放器初始化后这个 hook,即:afterPlayerInit

hooks.afterPlayerInit.tap('install-magnifier-dom', (player) => {
    // 安装相关dom和交互
    player.magnifier = new Magnifier(player);
});

tips: 这里 tap 方法是由tapable提供

Magnifier 类局部一览

export class Magnifier {
    ...
    constructor(player) {
        const { canvas, renderer } = player;
        const container = canvas.parentNode;

        canvas.addEventListener('mousedown', (e) => {
            ...
            this.rect = new zoomRect(container, x, y);
        });
        canvas.addEventListener('mousemove', (e) => {
            ...
        })
        canvas.addEventListener('mouseup', (e) => {
            ...
        })
        ...
    }
    ...
}

3.确定介入渲染器的时机 我们需要更改渲染器行为,因此需要介入渲染阶段,可以在渲染器渲染开始时,也可以在渲染器初始化完毕后等(合理即可),这里我们选用了渲染器初始化完毕之后,即 afterRendererInit

4.为播放器的渲染器拓展一个方法 zoomIn 当用户在视频中选择一个区域后,会将区域的数据作为参数,调用 zoomIn,以此来完成将用户选择框进行放大的效果

export function magnifier(hooks) {
    hooks.afterPlayerInit.tap('install-magnifier-dom', (player) => {
        // 安装相关dom和交互
        // 这里将player作为参数传进了Magnifier中,使其能够访问到下面的zoomIn方法(renderer是player的子、孙属性)
        player.magnifier = new Magnifier(player);
    });
    hooks.afterRendererInit.tap('install-renderer-magnifier', (renderer) => {
        // 更改渲染器行为,为其拓展一个缩放的方法;
        renderer.zoomIn = MagnifierForGlRenderer;
        // MagnifierForGlRenderer实现暂略,需要对播放器webgl有一定了解
        // 如果需要对视频渲染层面有定制需求,可咨询sdk研发人员做一定了解
    });
}

到此为止,就实现了一个电子放大镜插件,最后我们将它应用到到播放器上,让它生效

player.plugin.use(magnifier);

tips:放大镜插件过于硬核了点,这仅做关键插件实现步骤的讲解,对于业务层面,一般情况下不会有渲染层面的定制需求。