关于插件

在开发自己的播放器的时候,我们最常用的就是插件。我们可以通过插件自定义各种操作和逻辑,从而打造出一款易用的播放器。

本文会深度介绍插件的原理。包括以下几部分。

什么是插件

插件是 Chimee 播放器中的一部分。它具有以下特性。

通过插件的解耦,我们能够将业务上的新功能模块化,便于我们进行灰度发布或者测试等操作。

生命周期

插件的生命周期比较简单,包括如下几个部分:

beforeCreate

插件创建前的生命周期,如果开发者有提供 beforeCreate 函数,则会调用之。

此钩子函数会获得两个参数

一般可以在这个钩子中动态修改一些配置,例如增加事件绑定

function beforeCreate (config) {
  config.events[this.newKey] = this.newHandler;
}

create

插件创建的生命周期。在此阶段,播放器会进行以下操作:

  1. methods 中定义的方法绑定至实例上。
  2. 绑定events中所指定的事件。
  3. data中的数据绑定到实例上。
  4. computed中的计算属性绑定到实例上。
  5. 为该插件申请 DOM 节点,并绑定在$dom上。
  6. 根据用户设置和插件初始化设置设定该插件的$operable$level值。
  7. 将用户传入的阐述绑定到$config上。
  8. 执行开发者自定义的create函数。

故在create函数中,我们可以:

  1. 利用 this直接调用自己设置的方法
  2. 利用 this直接使用自己设置的参数
  3. 通过$on$off动态绑定/解绑事件, 通过$emit触发事件。
  4. $config中获取用户设置
  5. 通过$attr获取现时 video, container 和 wrapper 的参数值,但是由于初始化进行中,故该值获取意义不大。
  6. 可以通过$css获取或设置 video, container 和 wrapper 的参数值,对于一些需要添加loading 样式的需求可以满足。
  7. 通过$dom获得本插件所拥有的 DOM 节点,可以进行相关的初始化。

但是需要注意的是:

  1. 不能控制 $level
  2. 不能使用$attr操作 video 等部分的属性值。但是该函数可以调用,调用会延迟到初始化后才知行。
  3. 该函数是个同步函数,与 ready 初始化无关。

create 周期的主要任务是初始化插件。在此阶段尽量不要对 video, container 和 wrapper 进行操作

init

此阶段会进行播放器的初始化,所有在此阶段前被安装使用的插件将会被触发相应的 init 函数。

若果插件在播放器生成后才动态安装,则不会触发此生命周期函数,而直接跳转至inited

init 函数阶段插件将会获得用户对整个播放器的配置(不包括其余插件的配置)。插件可以根据该配置进行相应的特殊初始化。插件可以对此作相应更改。此期间的任何更改都不会直接写入到 video 实例上。

此后,插件可以使用 $attr操作 video, container 和 wrapper的值。

我们可以通过以下三种方式修改相关设置

  1. 从 init 接受的参数中直接更改 videoConfig
function init (config) {
  config.controls = false;
}
  1. 直接操作实例上的参数
function init (config) {
  this.controls = false;
}
  1. 通过 $videoConfig 操作参数
function init (config) {
  this.$videoConfig.controls = false;
}

inited

此时原生 player 已经初始化完毕,所有插件均已创建。开发者可以在此生命周期里执行一些自定义操作。

同时,该函数支持异步操作。开发者可以在函数末尾返回 Promise。当且仅当所有函数执行完毕(即同步函数 return 和异步函数返回的 Promise 状态变为 resolved 后),整个播放器才进入 ready 状态。

async function inited () {
  await doSomethingAsync()
}

在播放器实例化时,当所有插件都安装完毕后,ready 后会触发 ready 事件。

如果该插件是异步安装,则不会收到该事件。

destroy

当用户移除插件或者整个播放器销毁的时候触发。

作为销毁时的钩子函数。

插件位置

插件位置主要涉及以下几个问题

节点位置

需要了解的是,一般播放器都具有全屏的需求。而如果直接全屏原生 video 组件,那么诸如弹幕、控制条等内容都会丢失,并且会展现原播放器的控制条。

因此,一般我们采取将外层 div 全屏的策略。但是考虑到并非所有插件都需要全屏(例如外部列表)。我们引入了外层插件和内层插件的概念。

层级概念如下

在开发插件时,开发者可以主动注明属于何层插件,详细注明方式见参数相关介绍。由于 inner 属性的特殊性一经声明不能更改。

层级位置

另外一个比较重要的是插件层摆放的层级。因为较高的层级会遮挡较低的层级,不利于后者交互。现时播放器内部会主动设置相应的插件的层级,使用 z-index 进行相应设置。

设置的顺序依据如下:

用户安装插件的顺序

用户越早安装的插件,层级数越低。

可以理解为,后期安装的插件相当于直接 append 到相应父节点上。

level值的设置

level值较高者的层级位置永远在level值较低者之上。

因为插件的安装顺序与插件事件执行顺序有关。因此对于某些插件必须要有先安装。但是该插件仍然需要显示在顶层。此时应该使用较高的level值。

此需求最常见发生在广告插件上。因为广告需要阻截其他插件的事件处理,所以应该优先安装。但是广告同时要与周边交互,所以level值应该较高。

level 值可以由用户设置、插件默认设置或插件内部通过 $level 动态设置。

事实上并不鼓励插件自定义 level 值, 那样会造成用户使用的时候混乱。

但是万一插件需要置顶怎么办,此时调用 $bumpToTop 即可。

另外相关的知识点

透明插件

部分插件我们会尽可能置顶,但是其本身并没有任何交互,如台标,logo等。

为了方便大家制作此类插件,我们引入了透明插件的概念,其本质为 CSS 特性pointer-events: none;

通过 operable 选项进行设置,或者插件内部通过 $operable 进行动态设置。

穿透插件

还有一种插件在大部分情况下他不需要进行页面交互,此时它附着在 video 层上方,行为与透明插件无异。但某种情况下,他具有一定的交互能力。如弹幕和送礼。

为了方便开发者开发此类插件,我们增加了穿透属性 penetrate 。当一个插件为穿透插件时,所有冒泡到其外层的事件都会视为发生在 video 元素本身的事件,从而作相应的转发。

例如,当一个穿透元素被点击时,如果元素内部没有组织点击事件冒泡,则视为点击在 video 元素上。

因为该特殊属性,在播放器中,会认为穿透插件是原生 video 的一部分

常见的 mouseenter事件,当鼠标移入穿透插件时,会认为移入 video 中, 从而广播 mouseenter 事件。

若果时从穿透插件移入 video 中,穿透插件的 mouseleave 事件和 video 元素的 mounter 均不会被广播。

因为穿透插件的特殊性, penetrate 只允许开发者主动声明,且不允许更改。

事件机制

可用方法

在插件上可以调用的事件函数

监听对象

利用上述方法进行事件监听,监听的对象均为原生 video 对象。

有的时候我们需要监听 container 和 wrapper 的相关事件,此时我们需要在监听事件时,传入对应的 option。

如:

官方提供的事件绑定方法主要目的是搭建插件间沟通桥梁,和插件对 video, container, wrapper 三者的监听。

当然大家可以自定义相关事件,只要不涉及到相应保留前缀即可。

事件触发流程

鉴于插件间,插件和 video 间没有层级关系,不提供不捕获监听选项。

但是插件间仍然会存在逻辑关系。故在播放器事件内部仍然有相应的事件逻辑。

异步事件

一个完整的事件流程包括以下几部分

假设我们使用 $emit 触发了事件 play

同步事件

以上为异步事件流程,其有几大弊端。

  1. 异步,导致理解上有部分困难
  2. 异步,故不能执行 event.stopPropagation() 和 event.preventDefault() 事件

因此,和 DOM 有关的操作,我们建议使用 $emitSync事件。

与异步流程相同,也分为相应的五个阶段,假设我们使用 $emitSync 触发了 sync 事件。

注意,因为同步的优势,一般发生在 video 上的 DOM 事件,都会以同步的方式传输回来。

换言之,假如我们进行如下操作

this.$emit('click'); // 理解为模拟对 video 的点击
  1. 首先会触发异步的 before 事件
  2. 进入 process 阶段,点击 video
  3. video 返回 click 事件,使用同步(triggerSync)的方式传播,分别触发,click, (afterClick), _click事件。

注意,processor 只会处理 video 上的事件。

换言之this.$emitSync('click') 会触发 video.click(), 而this.$emitSync('c_click')并不会触发 container.click()

video事件

因为要兼顾到各种容器,顾 video 上的事件可能与我们日常有所不同。

video的DOM事件

其实与正常无异。不过有一点需要注意的是。由于穿透性插件的存在,该事件未必发生在原生 video 上。因此不要通过事件对象去获取 dom。

video元素相关方法

* 前缀为 chimee 自定义方法

我们可以把插件实例理解为 video 元素的子集映射。因此我们可以通过插件实例直接操作video。而插件上也有相应的方法和属性。

load

参数

load 方法会将地址设置到 video 元素上。之后才能进行相应的播放。

play

播放视频的函数。

pause

暂停视频播放的函数

seek

参数

seek函数本质等同于设置 video 上的 currentTime。一般用于快进后退。在 chimee 上也可以直接设置 currentTime,并不一定需要运用此函数。

focus

自动聚焦到 video 元素上。

* $fullscreen

全屏和退出全屏的相关操作。

关于全屏对象的设置可到插件介绍部分了解更多

requestFullscreen

进入全屏

exitFullscreen

退出全屏

canPlayType

* $silentLoad

静默加载视频。视频在规定时间内加载成功,则无缝切换视频源,多用于清晰度切换。

若视频加载失败可进行重试。

无缝切换的本质是,在后台打开一个新视频源并加载到约定时间,当主视频播放到约定时间后进行切换。

参数

我们可以利用 $silentLoad 完成以下需求。

  1. 无缝切换同种视频
this.$silentLoad('http://cdn.toxicjohann.com/%E4%BA%8E%E6%98%AF.mp4');
  1. 多次尝试切换
this.$silentLoad('http://cdn.toxicjohann.com/%E4%BA%8E%E6%98%AF.mp4', {repeatTimes: 5, increment: 2});

在上例中,若加载失败将会重试多达四次。每次尝试时间分别是3、5、7、9、11秒。

  1. 切换不同种类的视频
this.$silentLoad('http://yunxianchang.live.ujne7.com/vod-system-bj/TL1ce1196bce348070bfeef2116efbdea6.flv', {
  box: 'flv',
  kernels: {
    flv: chimeeKernelFlv
  }
});
  1. 加载途中放弃
const option = {};
this.$silentLoad('http://cdn.toxicjohann.com/%E4%BA%8E%E6%98%AF.mp4', option);
...
option.abort = true;

requestPictureInPicture(v0.11.0后支持)

画中画指 Picture-in-Picture Api。点击此处可观看 google chrome 最新的 demo。

为此, chimee 也给用户提供了相关的 api。

在不支持画中画功能的浏览器上,我们使用 canvas 模拟画中画图标。

该函数为异步函数,无需传入参数。

调用此方法会触发enterpictureinpicture事件。

exitPictureInPicture(v0.11.0后支持)

退出画中画模式。

同步函数,无需传入参数。

调用此方法会触发leavepictureinpicture事件。

video元素相关属性

* 前缀为 chimee 自定义属性

我们可以把插件实例理解为 video 元素的子集映射。因此我们可以通过插件实例直接操作video。而插件上也有相应的 video 属性。

src

* isLive

* box

* preset 🚫(v0.4.0废弃,请使用 kernels )

import Flv from 'chimee-kernel-flv';
const player = new Chimee({
  src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/TL1ce1196bce348070bfeef2116efbdea6.flv',
  preset: {
    flv: Flv
  },
  // 编解码容器
  box: 'flv', // flv hls mp4
  // dom容器
  wrapper: '#wrapper',
  // video
  autoplay: true,
  controls: true
})

* kernels

import Flv from 'chimee-kernel-flv';
const player = new Chimee({
  src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/TL1ce1196bce348070bfeef2116efbdea6.flv',
  kernels: {
    flv: Flv
  },
  // 编解码容器
  box: 'flv', // flv hls mp4
  // dom容器
  wrapper: '#wrapper',
  // video
  autoplay: true,
  controls: true
})

有的时候我们需要为 kernel 配置单独的参数。这个时候我们可以用如下方式传入参数。

import Flv from 'chimee-kernel-flv';
const player = new Chimee({
  src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/TL1ce1196bce348070bfeef2116efbdea6.flv',
  kernels: {
    flv: {
      handler: Flv,
      stashSize: 1000 * 1000 * 1024,
    },
  },
  // 编解码容器
  box: 'flv', // flv hls mp4
  // dom容器
  wrapper: '#wrapper',
  // video
  autoplay: true,
  controls: true
})

buffered

duration

volume

currentTime

autoplay

controls

width

height

crossOrigin

loop

defaultMuted

muted

preload

poster

playsInline

x5VideoPlayerFullscreen

x5VideoOrientation

xWebkitAirplay

playbackRate

defaultPlaybackRate

disableRemotePlayback

inPictureInPictureMode(v0.11.0后支持)

还有更多属性可以直接获取,如下

buffered, currentSrc, duration, error, ended, networkState, paused, readyState, seekable, sinkId, controlsList, tabIndex, dataset, offsetHeight, offsetLeft, offsetParent, offsetTop, offsetWidth

如果你需要的属性没有暴露,可以通过 issue 向我们反映。

container元素相关属性

在 v0.5.0 后,chimee 提供入口直接操作 container 的配置。

现在提供四个 css 样式,分别为 width, height, display, block.

你可以采取如下方式直接更改 container 的宽度

this.container.width = '90%';

##$attr

$attr 允许我们操作 video, container, wrapper 的属性。

默认操作的是 video。可以在第一个参数传入 'container', 'wrapper', 'video'更改操作对象。

$css

$css 允许我们操作 video, container, wrapper 的样式。

默认操作的是 video。可以在第一个参数传入 'container', 'wrapper', 'video'更改操作对象。

$bumpToTop

置顶函数。调用该方法,你的插件的 level 值将会移至最顶。

$watch

$watch 可用于监听特定属性的变化。当属性变化时,会执行传入的回调函数,回调函数会接收到新的属性值和原属性值。

参数

例子:

你可以轻易监听 video 上的一些属性。

import Chimee from 'chimme';
const plugin = {
  name: 'plugin',
  create () {
    this.$watch('controls', (newVal, oldVal) => console.log(newVal, oldVal));
  }
}
Chimee.install(plugin);
const player = new Chimee({
  wrapper: 'body',
  plugin: ['plugin']
});
player.controls = true; // true, false

又或者自定义属性:

import Chimee from 'chimme';
const plugin = {
  name: 'plugin',
  data: {
    test: 1
  }
  create () {
    this.$watch('test', (newVal, oldVal) => console.log(newVal, oldVal));
  }
}
Chimee.install(plugin);
const player = new Chimee({
  wrapper: 'body',
  plugin: ['plugin']
});
player.plugin.test = 2; // 2, 1

你也可以深度监听数组,直接调用数组的操作方法:

import Chimee from 'chimme';
const plugin = {
  name: 'plugin',
  data: {
    test: [1, 2, 3]
  }
  create () {
    this.$watch('test', (newVal, oldVal) => console.log(newVal, oldVal), {deep: true});
  }
}
Chimee.install(plugin);
const player = new Chimee({
  wrapper: 'body',
  plugin: ['plugin']
});
player.plugin.test.push(4); // [1, 2, 3, 4], [1, 2, 3, 4]

同理你也可以深度监听对象,但是对新增元素或者删除元素需要使用 $set$del 进行辅助。

import Chimee from 'chimme';
const plugin = {
  name: 'plugin',
  data: {
    test: {
      foo: 1
    }
  }
  create () {
    this.$watch('test', (newVal, oldVal) => console.log(newVal, oldVal), {deep: true});
  }
}
Chimee.install(plugin);
const player = new Chimee({
  wrapper: 'body',
  plugin: ['plugin']
});
player.plugin.test.foo = 2; // {foo: 2}, {foo: 2}
player.$set(test, 'bar', 1); // {foo: 2, bar: 1}, {foo: 2, bar: 1}
player.$del(test, 'bar'); // {foo: 2}, {foo: 2}

注意:

  1. 并非所有 video 相关属性都可以监听。现阶段只支持监听$videoConfig 中除src 以外的部分。

src 的值因为涉及到 video 播放核心的变换,以及事件拦截等,建议采取事件驱动模式编写。

paused 等 video 只读属性,因为需要监听原生 video,故暂不提供。且以上属性大部分可以通过事件获取。

  1. 采取深度监听时,子元素修改后回调函数并不会获得原有对象快照
  2. 深度监听时需要使用 $set$del 进行辅助。

全屏相关方法

* $fullscreen

全屏和退出全屏的相关操作。

关于全屏对象的设置可到Chimee 插件 API 介绍中的插件位置部分了解更多

requestFullscreen

进入全屏

exitFullscreen

退出全屏

全屏相关属性

isFullscreen

若实例中的任意一个子节点全屏,则返回 true

fullscreenElement

如果全屏的是 containerwrappervideo 三者之一,则直接返回字符串。

否则返回正在全屏的对象。

若无全屏则为 undefined

fullscreenchange

如果需要监听全屏事件。只要监听 fullscreenchange 即可。

player.on('fullscreenchange', evt => {
  console.log('wowo, fullscreen', evt);
});

PluginConfig参数

开发者开发插件时可以设定的参数

name

level

opearable

penetrate

inner

autoFocus

el

className

beforeCreate

create

init

inited

destroy

events

data

computed

methods

插件属性

以下属性可以直接在插件实例上获取

ready

readySync

$dom

$wrapper

$container

$video

$level

$operable

$config

$videoConfig

属性 含义 类型 默认值 备注
src 播放地址 string '' 假如 autoloadtrue,则当我们设置 src 后,该地址会加载到 video 元素上,并作出相应加载。若果 autoloadfalse, 则意味着我们仅仅在 videoConfig 上设置了地址,此时可以手动调用 load 方法进行
autoplay 是否自动播放 boolean false autoplay 指在分配 src 后自动播放,即调用chimee.load()后。
controls 是否展示控制条 boolean false 在没有安装任何皮肤插件时,该属性控制是否展示原生控制条。若果安装了皮肤插件,则意味着是否展示皮肤自带的控制条。
width video 的宽度 number \ string '100%' 支持数字、百分比或像素值。在大部分浏览器下,数字和px值一致,其余单位没有效果,建议尽量使用数字。且该属性优先级较低,会被 CSS 值覆盖。建议谨慎使用。
height video 的高度 number \ string '100%' 支持数字、百分比或像素值。在大部分浏览器下,数字和px值一致,其余单位没有效果,建议尽量使用数字。且该属性优先级较低,会被 CSS 值覆盖。建议谨慎使用。
crossOrigin 是否跨域 boolean undefined
loop 是否循环 boolean false
muted 是否静音 boolean false
preload 是否预加载 string auto
poster 封面 string ''
playsInline 是否内联 boolean false 我们会为此添加 playsinle webkit-playsinline x5-playsinline
xWebkitAirplay 是否添加 x-webkit-airplay boolean false
x5VideoPlayerFullscreen 是否添加x5-video-play-fullscreen boolean false
x5VideoOrientation x5-video-orientation string \ void undefined 可选 landscape 和 portrait
x5VideoPlayerType x5-video-player-type 'h5' \ void undefined
playbackRate 回放速率 number 1 大于1加速,小于1减速
defaultPlaybackRate 默认回放速率 number 1 大于1加速,小于1减速
autoload 设置src时是否进行自动加载 boolean true
defaultMuted 是否是默认静音 boolean false 对应于 video 上的 muted 标签
disableRemotePlayback 是否不展示远程回放标志 boolean false 对应于 video 上的 disableRemotePlayback 标签
volume 音量 number 1

注意

1)autoplay 属性在并不是在所有情况下都会生效。但是通过一些配置,我们可以使其在大部分模式下生效。

  1. 在 iOS 下需要 inline 的模式下才能自动播放,因此在传入的时候需要设置 inline: true。我们会为你设置playsinline="true" webkit-playsinline="true"
  2. 然而并不是所有 iOS 的 webview 都支持该模式,如果你的 iOS 版本比较旧,请检查 webView 上有否设置 webview.allowsInlineMediaPlayback = YES;
  3. 在腾讯的 X5 浏览器也需要同理,设为 inline: true,我们会为你设置 x5-playsinline
  4. 部分浏览器必须要一开始就添加 video 元素,此时,请将 wrapper 的 html 写成如下格式。
<div id="wrapper">
  <container>
    <video></video>
  </container>
</div>

2)以上所有属性均可以在 chimee 实例上直接自上使用,如this.src

$inner

$autoFocus

$penetrate

$plugins

$pluginsOrder

VERSION

以下设置均是与 video 元素属性相关的属性。可以在任意阶段直接设置。

但是在 beforeCreate, create, init 阶段进行设置并不会立即设定到 video 元素上,因为此阶段还没生成 video 元素。但是会在生成 video 元素后立即设定。

buffered

duration

volume

currentTime

插件方法

$css

查询或设置 wrapper、video 或 container 的样式。

参数

用法

// 查询
this.$css('width');

// 设置
this.$css('width', 100);

$attr

具体用法与 $css 一致

$fullscreen

详情见全屏章节

$watch

详情见$watch章节

$set

设置对象或者数组的值, 可以触发$watch 的回调函数

参数

$del

删除对象或者数组的值, 可以触发$watch 的回调函数

参数

$bumpToTop

置顶函数。调用该方法,你的插件的 level 值将会移至最顶。

$on

绑定事件

$off

解绑事件,参数与$on一致

$once

一次性监听,参数与 $on 一致

$emit

触发事件,事件函数会被 Promise 包裹。

$emitSync

触发事件,事件函数均会被同步执行

$throwError

框架报错专用,将会被 errorHandler 处理。

基本应用

首先我们要编写一个符合 PluginConfig 约定的插件配置,然后通过 Chimee.install 注册到播放器类上:

const pluginConfig = {
    name: 'myplugin',
    el: '<div>我的第一个插件</div>'
};
Chimee.install(pluginConfig);

接下来在实例化播放器的时候,便可以通过 option 设置或主动 'use' 来使播放器实例具有该UI组件功能。

const chimee1 = new Chimee({
  wrapper: '#wrapper',
  plugins: [pluginConfig.name]
});
const chimee2 = new Chimee({
  wrapper: '#wrapper'
});
chimee2.use(pluginConfig.name);

要了解如看编写与使用弹窗组件,请看这里