目录一、西瓜播放器介绍1. 核心功能特色2. 快速上手二、学习任务三、插件系统和事件机制1. 导入插件2. 注册插件3. 初始化插件4. 插件生命周期4.1 创建 DOM 元素4.2 注册事件监听4.3 移除事件监听4.4 销毁 DOM 元素5. 自定义插件5.1 定义插件5.2 使用插件6. 事件机制介绍">6.1 插件内部事件机制6.2 播放器内部事件机制7. 播放器销毁四、重要问题1. 西瓜播放器的 video 元素如何创建?2. 各个插件如何跟主播放器关联起来?五、知识点总结1. 插件可插拔1.1 热插拔1.2 冷插拔2. 插件生命周期和抽象类3. querySelector 兼容处理4.批量导入指定目录下的文件4.1 babel-plugin-bulk-import4.2 Webpack require.context5. 解决 video 销毁时引起的浏览器奔溃问题6. 前端本地视频预览实现6.1 源码解读6.2 实现原理6.3 内存管理6.4 拓展:图片预览6.5 URL.createObjectURL 特性检测7. 前端文件下载实现7.1 a标签 + initMouseEvent7.2 a标签 + createObjectURL8.播放器截图实现8.1 原理分析8.2 Image 元素跨域8.3 生成图片地址8.4 下载图片9. 获取 HTMLMediaElement 元素状态9.1 网络状态9.2 就绪状态
目录一、西瓜播放器介绍二、学习任务三、插件系统和事件机制四、重要问题五、学习总结
一、西瓜播放器介绍西瓜播放器是字节跳动开源的一款带解析器、能节省流量的Web视频播放器类库,它本着一切都是组件化的原则设计了独立可拆卸的 UI 组件。从底层解析 MP4、HLS、FLV 探索更大的视频播放可控空间。摆脱视频加载、缓冲、格式支持对 video 的依赖。支持清晰度无缝切换、加载控制、节省视频流量。同时,它也集成了对 FLV、HLS、DASH 的点播和直播支持。
点击查看演示页(图片来源 —— http://h5player.bytedance.com/)
**
1. 核心功能特色
易拓展:灵活的插件体系、PC/移动端自动切换、安全的白名单机制;更丰富:强大的MP4控制、点播的无缝切换、有效的带宽节省;较完整:完整的产品机制、错误的监控上报、自动的降级处理 ;
项目官网:http://h5player.bytedance.com/开源地址:https://github.com/bytedance/xgplayer
2. 快速上手
上手西瓜播放器只需三个步骤:安装、DOM占位和实例化即可,官方demo如下:
1.安装
$ npm install xgplayer// 或 CDN 引入
2.DOM 占位
3.实例化
let player = new Player({ id: 'mse', url: '//abc.com/**/*.mp4'});
即可快速上手西瓜播放器。
二、学习任务此次学习 西瓜播放器架构设计 的目的如下:
提升自己框架设计能力和源码阅读能力;为团队接下来插件化项目实战做基础;理解插件化和事件机制的实际应用;
从官网中对产品主要功能特色的描述,罗列出可以学习的内容,包括:
如何设计灵活的插件体系?为什么需要实现 PC /移动端自动切换,如何实现?什么是安全白名单?作用是什么?如何实现?如何有效的节省带宽?点播如何无缝切换?完整的产品机制包括哪些方面?错误监控如何上报?为什么要自动降级,如何降级?
本文主要学习和思考 插件设计及事件机制 相关知识。
三、插件系统和事件机制首先我们大致了解一下西瓜播放器的插件系统和事件机制架构图,后面详细介绍:
图中每个步骤分别是:
引入西瓜播放器框架;注册所有内置插件,并保存在插件对象中;实例化播放器Player,初始化UI和插件;执行 Proxy 构造方法,创建播放器 video 元素;添加 Player 和 Proxy 上的事件监听到事件总线,完成实例化;播放器调用销毁,执行相关销毁任务。
1. 导入插件在引入西瓜播放器框架时,便开始导入所有插件:
// index.jsimport Player from './player'import * as Controls from './control/*.js' // 导入所有插件import './style/index.scss'export default Player
2. 注册插件以 localPreview 插件为例(control\localPreview.js ),注册流程如下:
插件中引入 Player 对象,实现完插件功能,通过调用 Player.install 方法,注册插件。
// localPreview.jsimport Player from '../player'let localPreview = function () { // ...}Player.install('localPreview', localPreview) // 执行注册
这样即可注册一个插件,接着了解下 Player.install 是如何实现:
// player.js 447行开始static install (name, descriptor) { if (!Player.plugins) { Player.plugins = {} } Player.plugins[name] = descriptor // 保存插件}
在注册插件时,接收两个参数 name 和 descriptor ,对应之前 Player.install('localPreview', localPreview) 的两个参数。接着在 Player.plugin 对象上,将 name 作为 key , descriptor 作为 value 进行保存。这样就将插件保存到 Player.plugins 对象中。
3. 初始化插件插件注册完成后,保存到 Player.plugins 对象中,在播放器实例化时,会进行插件初始化。并且西瓜播放器提供 ignores 配置项,来让开发者可以过滤指定内置插件不初始化:
// player.js// 初始化读取合并配置this.config = util.deepCopy({ // ... ignores: [], // 需要过滤不初始化的内置插件 // ...}, options)pluginsCall () { // ... if (Player.plugins) { let ignores = this.config.ignores Object.keys(Player.plugins).forEach(name => { let descriptor = Player.plugins[name] // 获取每一个插件的描述方法 if (!ignores.some(item => name === item)) { // 插件过滤操作 // 初始化插件,执行插件的描述方法,进行初始化 if (['pc', 'tablet', 'mobile'].some(type => type === name)) { if (name === sniffer.device) { setTimeout(() => { descriptor.call(self, self) }, 0) } } else { descriptor.call(this, this) } } }) }}
4. 插件生命周期在研究 插件生命周期 中,本节以“播放器贴图(poster)”内置插件为例介绍,该插件使用方法如下。
// poster.jslet player = new Player({ el:document.querySelector('#mse'), url: 'video_url', poster: "//s2.pstatp.com/cdn/expire-1-M/byted-player-videos/1.0.0/poster.jpg",});
“播放器贴图(poster)”插件的实现原理:插件基于 EventEmitter 事件机制,通过监听播放器的 play 事件来隐藏 poster 播放器贴图,并通过监听播放器的 destroy 事件实现事件移除(包含 play 事件和 destory )。
一个好的插件系统设计,对于插件生命周期包含以下四个模版方法:创建 DOM 元素,注册事件监听,移除事件监听,销毁 DOM 元素:
4.1 创建 DOM 元素从源码可知,该插件了 DOM 元素
// poster.jslet poster = util.createDom('xg-poster', '', {}, 'xgplayer-poster');let root = player.rootif (player.config.poster) { poster.style.backgroundImage = `url(${player.config.poster})` root.appendChild(poster)}
4.2 注册事件监听在插件中利用 EventEmitter 事件机制注册了 play 方法:
// poster.jsfunction playFunc () { poster.style.display = 'none'}player.on('play', playFunc)
4.3 移除事件监听在插件中,对 destroy 进行了一次性事件监听( once ),用来监听事件移除:
// poster.jsfunction destroyFunc () { player.off('play', playFunc) player.off('destroy', destroyFunc)}player.once('destroy', destroyFunc)
通过定义 destroyFunc 方法,在内部调用播放器 off 方法来移除 EventEmitter 的事件监听器。
4.4 销毁 DOM 元素在西瓜播放器的插件中,没有看到销毁 DOM 元素的操作,考虑到插件生命周期的完整性,可参考 Player 实例实现的销毁播放器的方法,下面按照我个人思考进行修改源码。
考虑到 poster 插件中当 player.config.poster 存在时,才会在播放器插入 poster 的 DOM 元素。
// rotate.jsif (player.config.poster) { poster.style.backgroundImage = `url(${player.config.poster})` root.appendChild(poster)}
所以在修改 destroyFunc 方法时也需要考虑该情况,即修改后如下:
// rotate.jsfunction destroyFunc () { player.off('play', playFunc) player.off('destroy', destroyFunc) if (player.config.poster) { root.removeChild(poster) // 移除 root 上的 poster 元素 }}player.once('destroy', destroyFunc)
5. 自定义插件西瓜播放器中,可以理解为一切功能皆为插件。当西瓜播放器内置插件不满足我们的业务时,我们可以自定义一个插件,只需两步操作:
5.1 定义插件在 /control/ 目录下新建一个插件文件(如 MyPlugin.js),并实现插件功能。这里以一个很简单的例子演示,当播放器实例化时,传入参数 alertMsg 并指定参数值为一个字符串,实现调用插件会全局提示一个 alert 框,并展示参数指定的字符串。
// MyPlugin.jsimport Player from 'xgplayer';let MyPlugin=function(){ // 实现插件功能 如定义一个 if (player.config.alertMsg) { alert(alertMsg); }}Player.install('MyPlugin',MyPlugin);
建议设计自定义插件时,也参考 1.4插件生命周期 进行设计。
5.2 使用插件使用插件时,需要知道,在导入西瓜播放器框架时,就已经一起导入插件了:
import * as Controls from './control/*.js' // 导入所有插件
和使用其他插件一样:
// 业务代码文件import Player from 'xgplayer';let player = new Player({ id: 'xg', url: 'my_video_url.mp4', alertMsg: '你好,西瓜播放器!'})
6. 事件机制介绍在西瓜播放器中,插件通信机制通过 事件总线 实现事件驱动。插件中有 EventEmitter 和 JS EventListener 两类事件。
6.1 插件内部事件机制通过 插件内部事件机制 实现插件间通信,参照上图左侧部分。
事件注册监听:
在播放器实例化时,会在插件初始化过程中,将插件中的事件注册到事件总线上(插件通过 on / once 方法注册到 EventEmitter,插件也可以通过 addEventListener 方法注册到 JS EventListener)。
// poster.jsplayer.on('play', playFunc)player.once('destroy', destroyFunc)// rotate.jsbtn.addEventListener('click',() => { player.rotate() })
事件移除监听:
在 destroy 过程中,也会从事件总线中进行移除事件监听( off 通过 EventEmitter 移除监听, removeEventListener 通过 JS EventListener 移除监听)。
// poster.jsplayer.off('play', playFunc)player.off('destroy', destroyFunc)// volume.jswindow.removeEventListener('mousemove', move)window.removeEventListener('touchmove', move)window.removeEventListener('mouseup', up)window.removeEventListener('touchend', up)
业务逻辑触发事件监听:
在业务逻辑中触发,EventEmitter 使用 player.emit 触发事件。
// rotate.jsplayer.emit('rotate', rotateDeg * 360)
6.2 播放器内部事件机制将播放器中的事件分 Player 事件和 Proxy 事件,参照上图右侧部分。
事件注册监听:
在播放器实例化时,批量将实例中的事件注册到事件总线上(通过 on / once 方法注册到 EventEmitter,通过 addEventListener 注册到 JS EventListener)。
// proxy.js// 定义事件名称this.ev = ['play', 'playing', /* 省略其他方法 */].map((item) => { return { [item]: `on${item.charAt(0).toUpperCase()}${item.slice(1)}` }})// player.js 监听事件this.ev.forEach((item) => { let evName = Object.keys(item)[0] let evFunc = this[item[evName]] if (evFunc) { this.on(evName, evFunc) }});// proxy.js 监听事件this.ev.forEach(item => { self.evItem = Object.keys(item)[0] let name = Object.keys(item)[0] self.video.addEventListener(Object.keys(item)[0], function () { // 省略 })})
事件移除监听:
在 destroy 过程中,也会从事件总线中进行移除事件监听(通过 off 方法从 EventEmitter移除,通过 removeEventListener 方法从 JS EventListener 中移除。
// player.jsdestroy (isDelDom = true) { // ... // 移除单个事件监听 if(this.playFunc) { this.off('play', this.playFunc) } // 批量移除事件监听 ['focus', 'blur'].forEach(item => { this.off(item, this['on' + item.charAt(0).toUpperCase() + item.slice(1)]) })}
7. 播放器销毁西瓜播放器中提供一个 destroy 实例方法,用于销毁播放器。在 destroy 方法中,主要做了几件事:
移除定时器相关;移除事件监听相关(EventEmitter、JS事件);移除DOM相关;移除实例对象属性相关;
下面简要贴出一些代码:
// player.jsdestroy (isDelDom = true) { // 遍历移除定时器相关 for (let k in this._interval) { clearInterval(this._interval[k]) this._interval[k] = null } // 遍历移除EventEmitter事件监听 this.ev.forEach((item) => { if (evFunc) { this.off(evName, evFunc) } }); // 省略,移除部指定事件 // 遍历移除 addEventListener 事件监听 ['video', 'controls'].forEach(item => { if (this[item]) { this[item].removeEventListener('keydown', /*...*/) } }) // 销毁播放器 DOM 结构 if (isDelDom) { parentNode.removeChild(this.root) } // 省略移除 this 所有属性}
四、重要问题
1. 西瓜播放器的 video 元素如何创建?在源码中,西瓜播放器的 video 元素是在 proxy.js 构造函数中初始化:
// proxy.jsthis.video = util.createDom( this.videoConfig.mediaType, textTrackDom, this.videoConfig, '')
在后面代码中,都将 video 事件挂载到 this.video 对象上。
比如我们点击播放按钮的时候,实际上也是调用了 this.video 对象上的 play() 方法:
// player.js start (url = this.config.url) { // 省略其他代码 this.canPlayFunc = function () { let playPromise = player.video.play() } }
2. 各个插件如何跟主播放器关联起来?核心是 Player 通过继承 Proxy 类,Proxy 又通过 EventEmitter.call 实现构造继承。然后每个插件内部都注入 Player 类的实例,然后利用 EventEmitter 的 API 来实现事件驱动。
① Player 类继承 Proxy 类:
// player.jsclass Player extends Proxy { // ...}
② Proxy 通过 EventEmitter.call 实现构造继承:
// proxy.js 78行EventEmitter(this)
③ 插件注入 Player 类的实例,通过 EventEmitter API 实现事件驱动:
这里以 play 插件为例(control/play.js)。
// proxy.js 84行player.on('play', playFunc)// ...player.on('pause', pauseFunc)
五、知识点总结
1. 插件可插拔在设计插件系统,可以考虑插件的可插拔性。使插件系统更灵活,易拓展,优化用户体验和提升加载速度。插件可插拔一般分为:热插拔和冷插拔。
1.1 热插拔一般情况下,热插拔的插件是在打包阶段就打进整体的包里面,实现方式介绍:
西瓜播放器过滤插件
西瓜播放器通过 ignores 配置项支持热插拔,使得我们可以在使用阶段,可以通过 ignores 数组来过滤不需要使用的内置插件:
// player.jspluginsCall () { // ... 省略其他代码 let ignores = this.config.ignores // 获取需要过滤的内置插件数组 Object.keys(Player.plugins).forEach(name => { let descriptor = Player.plugins[name] if (!ignores.some(item => name === item)) { // 执行过滤操作 // 初始化插件 } })}
弊端:全量打包时包体积较大,当开发者明确不需要使用某些内置插件时,插件还是全量插件,占用插件包的体积。比如:为了使用获取DOM元素的方法或使用Ajax请求的方法而将整个jQuery引入项目。
动态导入模块
使用 import() 作为函数调用,将其作为参数传递给模块的路径。 它返回一个promise,它用一个模块对象来实现,让我们可以访问该对象的导出。
let loadModule = document.querySelector('.load');loadModule.addEventListener('click', () => { import('/modules/leo.js').then((Module) => { loadModule.draw(); })});
详细介绍可以查看《动态加载模块》。
使用场景:在实现页面懒加载时,可以采用,当加载项目时,不会完整加载,而是等到进入某些指定页面,才会去加载相应的模块文件。弊端:动态加载模块时,比较影响用户体验,如果模块比较大,加载偏慢,影响体验。
1.2 冷插拔即在插件包打包时,就将需要支持的插件一起打包进去,并且不提供过滤插件的方法。实现方式有:
通过 Webpack DefinePlugin 配置
通过 Webpack DefinePlugin 读取打包配置,可以打轻量版或完整版的包,比较灵活,如果轻量版功能不够,可以再根据需要打增量的轻量版覆盖。
// plugin.jsconst litePlugin = [ module1, module2, module9 ];const fullPlugin = [ module1, module2, ..., module9 ];const plugin = {litePlugin, fullPlugin};const version = pluginVersion;export default plugin[version];// webpack.lite.jsplugins: [ // ... new webpack.DefinePlugin({ 'pluginVersion': JSON.stringify("litePlugin"), })]// webpack.full.jsplugins: [ // ... new webpack.DefinePlugin({ 'pluginVersion': JSON.stringify("fullPlugin"), })]
2. 插件生命周期和抽象类设计插件时,考虑在抽象类中定义插件生命周期,在实际插件开发中去实现生命周期的方法。实际上就是在插件层上,添加一层通用插件层,用来定义需要实现的插件方法。这样有几个好处:
抽象类中统一对所有插件进行操作,便于管理;规定相同开发模式,降低开发难度;
简单实例:
// Plugin.tsabstract class EFTPlugin{ constructor(){} abstract creatDOM():void abstract addEventListener():void abstract removeEventListener():void abstract destoryDOM():void}// TestPlugin.tsclass TestPlugin extends EFTPlugin{ constructor(){ super() } creatDOM(){ // ... } addEventListener(){ // ... } removeEventListener(){ // ... } destoryDOM(){ // ... }}const testPlugin = new TestPlugin()testPlugin.creatDOM();testPlugin.addEventListener();testPlugin.removeEventListener();testPlugin.destoryDOM();export default testPlugin;
3. querySelector 兼容处理参考文章:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4#c453c991
querySelector 方法使用 CSS3 选择器,用来查找DOM。CSS3 中的 ID 选择器不支持以数字开头,需要降级使用 getElementById 。
// utils\util.js 代码第62行开始util.findDom = function (el = document, sel) { let dom // fix querySelector IDs that start with a digit // https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document try { dom = el.querySelector(sel) } catch (e) { if (sel.startsWith('#')) { dom = el.getElementById(sel.slice(1)) } } return dom}
4.批量导入指定目录下的文件参考文章:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4#c453c991
在西瓜播放器项目中,通过import * as Controls from './control/*.js' 语句实现批量导入播放器的所有内置插件:
// index.jsimport Player from './player'import * as Controls from './control/*.js'import './style/index.scss'export default Player
实现批量导入指定目录下的文件,有两种方式:
借助 babel-plugin-bulk-import 插件实现;借助 Webpack require.context API 来实现;
4.1 babel-plugin-bulk-import这是一款 Babel 插件,用于批量导入。安装:
npm install babel-plugin-bulk-import --save-dev
配置 .babelrc :
{ "presets": ["es2015"], "plugins": ["babel-plugin-bulk-import"]}
使用:
import * as Controls from './control/*.js'
4.2 Webpack require.context这是一个 webpack 的 api,通过执行 require.context 函数获取一个特定的上下文,主要用来实现自动化导入模块。在前端工程化中,如果遇到从一个文件夹引入多个模块时,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入使得不需要每次显式的调用 import 导入模块。
另外,还有插件 babel-plugin-bulk-import 可以使用。
5. 解决 video 销毁时引起的浏览器奔溃问题在源码中,注释了这么一个地址:https://stackoverflow.com/questions/3258587/how-to-properly-unload-destroy-a-video-element
其中介绍的大概是为了解决 video 销毁时引起的浏览器奔溃问题:**
var videoElement = document.getElementById('id_of_the_video_element_here');videoElement.pause();videoElement.removeAttribute('src'); // empty sourcevideoElement.load();
另外一篇文章中也介绍到这个情况:https://html.spec.whatwg.org/multipage/media.html#best-practices-for-authors-using-media-elements
推荐一篇文章:《如何解决内存泄漏引发的血案》
6. 前端本地视频预览实现参考文章:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4#c453c991
6.1 源码解读在西瓜播放器中,视频本地预览插件的核心代码如下:
// localPreview.jslet localPreview = function () { let player = this; let util = Player.util // 动态创建上传按钮 let preview = util.createDom('xg-preview', '', {}, 'xgplayer-preview') let upload = preview.querySelector('input') if (player.config.preview && player.config.preview.uploadEl) { player.config.preview.uploadEl.appendChild(preview) // 监听上传按钮变化的事件 upload.onchange = function () { player.uploadFile = upload.files[0] // 通过调用 URL.createObjectURL() 创建 URL 对象 // 建议:当结束使用这个 URL 对象时,使用 window.URL.revokeObjectURL(objectURL) , // 来释放这个 URL 对象对文件的引用。 let url = URL.createObjectURL(player.uploadFile) if (util.hasClass(player.root, 'xgplayer-nostart')) { player.config.url = url player.start() } else { player.src = url player.play() } } }}Player.install("localPreview", localPreview);
6.2 实现原理在西瓜播放器中,视频本地预览主要利用 URL.createObjectURL() API 实现。
实现过程如下:
创建上传按钮,通过 获取本地 file 对象;将 File 对象通过 URL.createObjectURL() 方法转换为 ObjectURL 地址;设置指定 video 标签的 src 属性值为视频本地的 ObjectURL 地址;
URL.createObjectURL() 静态方法将创建一个 DOMString ,参数是一个用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。最终返回一个DOMString包含了一个对象URL,该URL可用于指定源 object的内容。
下面整理一个非常简单的例子:
6.3 内存管理在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象。但是,当我们不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。
浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,建议应该在安全的时机主动释放掉它们。
6.4 拓展:图片预览目前常见的前端本地图片预览实现方式有两种:通过 FileReader.readAsDataURL 或 URL.createObjectURL 的方式来实现。
readAsDataURL 方法会读取指定 Blob 或 File 对象。读取完成时,readyState 会变成已完成DONE,并触发 [loadend]() 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。
两者区别:
区别内容
FileReader.readAsDataURL
URL.createObjectURL
是否同步
异步执行
同步执行
内存使用
返回 base64 格式字符串,比 Blob URL 方式更占内存空间,当不需要使用时,可通过系统垃圾回收机制自动进行回收。
返回本地 URL 地址对象,并一直保存在内存中,直到文档触发 unload 事件或者手动调用 revokeObjectURL。
兼容性
支持 IE 10 以上的主流浏览器,详细的兼容性查看 caniuse - createObjectURL。
支持 IE 10 以上的主流浏览器,详细兼容性查看 caniuse - readAsDataURL
6.5 URL.createObjectURL 特性检测function createObjectURL (file) { if (window.webkitURL) { return window.webkitURL.createObjectURL(file); } else if (window.URL && window.URL.createObjectURL) { return window.URL.createObjectURL(file); } else { return null; } }
7. 前端文件下载实现参考文章:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4#c453c991在西瓜播放器中,场景如截图下载。
7.1 a标签 + initMouseEvent使用 a 标签 + initMouseEvent 方法实现下载:
let saveScreenShot = function (data, filename) { let saveLink = document.createElement('a') saveLink.href = data saveLink.download = filename let event = document.createEvent('MouseEvents') event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null) saveLink.dispatchEvent(event)}
7.2 a标签 + createObjectURL使用 a 标签 + createObjectURL 方法实现下载:
let saveScreenShot = function (blob, filename) { const a = document.createElement('a'); const url = window.URL.createObjectURL(blob); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url);}
8.播放器截图实现参考文章:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4#c453c991
8.1 原理分析西瓜播放器截图流程如下:
创建截屏按钮并添加到播放器控制栏中;创建 Canvas 元素和一个 Image 实例;为截屏按钮绑定事件监听,如 click 和 touchstart 等事件;当用户点击触发截屏,在回调函数中通过 drawImage() 方法截取当前视频帧,完成截图。
对于播放器截图功能,主要利用 CanvasRenderingContext2D.drawImage() API 来实现。 drawImage() 方法提供多种方式在 Canvas 上绘制图片,语法如下:
void ctx.drawImage(image, dx, dy);
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
详细参数可以查看文档 CanvasRenderingContext2D.drawImage() 。
8.2 Image 元素跨域在 HTML5 中,一些 HTML 元素提供了对 CORS 的支持, 例如 audio、image、link、script 和 video 均有一个跨域属性(crossOrigin property),它允许你配置元素获取数据的 CORS 请求。
这些属性是枚举的,并具有以下可能的值:
关键字
描述
anonymous
对此元素的 CORS 请求将不设置凭据标志。
use-credentials
对此元素的 CORS 请求将设置凭证标志;这意味着请求将提供凭据。
""
设置一个空的值,如 crossorigin 或 crossorigin="",和设置 anonymous 的效果一样。
默认情况下(即未指定 crossOrigin 属性时),CORS 根本不会使用。如 Terminology section of the CORS specification 中的描述,在非同源情况下,设置 “anonymous” 关键字将不会通过 cookies,客户端 SSL 证书或 HTTP 认证交换用户凭据。即使是无效的关键字和空字符串也会被当作 anonymous 关键字使用。
参考资源 —— CORS_settings_attributes
使用案例:
8.3 生成图片地址这里主要使用 HTMLCanvasElement.toDataURL() 方法生成,返回一个包含图片展示的data URI。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。
如果画布的高度或宽度是0,那么会返回字符串“data:,”。如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。Chrome支持“image/webp”类型。
语法如下:
canvas.toDataURL(type, encoderOptions);
type 可选,图片格式,默认为 image/pngencoderOptions 可选,在指定图片格式为 image/jpeg 或image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
代码演示:
获取一个 data-URL:
var canvas = document.getElementById("canvas");var dataURL = canvas.toDataURL();console.log(dataURL);// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"
设置 jpegs 图片的质量:
var fullQuality = canvas.toDataURL("image/jpeg", 1.0);// data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...9oADAMBAAIRAxEAPwD/AD/6AP/Z"var mediumQuality = canvas.toDataURL("image/jpeg", 0.5);var lowQuality = canvas.toDataURL("image/jpeg", 0.1);
8.4 下载图片下载图片的实现,可以查看【第7点 前端文件下载实现】。
9. 获取 HTMLMediaElement 元素状态
9.1 网络状态文档地址:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/networkState
HTMLMediaElement.networkState 属性表示在网络上获取媒体的当前状态。语法如下:
const networkState = audioOrVideo.networkState;
返回值是一个 unsigned short。可能的值包括:
常量
值
描述
NETWORK_EMPTY
0
还没有数据。并且 readyState的值是 HAVE_NOTHING。
NETWORK_IDLE
1
HTMLMediaElement 是有效的并且已经选择了一个资源,,但是还没有使用网络。
NETWORK_LOADING
2
浏览器正在下载 HTMLMediaElement 数据。
NETWORK_NO_SOURCE
3
没有找到 HTMLMediaElement src。
代码演示:这个例子监听audio元素以开始播放,然后检查是否仍然在加载数据。
const obj = document.getElementById('example');obj.addEventListener('playing', function() { if (obj.networkState === 2) { // Still loading... }});
西瓜播放器中的使用方式:
// proxy.js get networkState () { let status = [{ en: 'NETWORK_EMPTY', cn: '音频/视频尚未初始化' }, { en: 'NETWORK_IDLE', cn: '音频/视频是活动的且已选取资源,但并未使用网络' }, { en: 'NETWORK_LOADING', cn: '浏览器正在下载数据' }, { en: 'NETWORK_NO_SOURCE', cn: '未找到音频/视频来源' }] return this.lang ? this.lang[status[this.video.networkState].en] : status[this.video.networkState].en }
9.2 就绪状态文档地址:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/readyState
HTMLMediaElement.readyState 属性返回音频/视频的当前就绪状态。语法如下:
const readyState = audioOrVideo.readyState;
返回值是一个无符号整型 An unsigned short。可能的值包括:
Constant
Value
Description
HAVE_NOTHING
0
没有关于音频/视频是否就绪的信息
HAVE_METADATA
1
音频/视频已初始化
HAVE_CURRENT_DATA
2
数据已经可以播放(当前位置已经加载) 但没有数据能播放下一帧的内容
HAVE_FUTURE_DATA
3
当前及至少下一帧的数据是可用的(换句话来说至少有两帧的数据)
HAVE_ENOUGH_DATA
4
可用数据足以开始播放-如果网速得到保障 那么视频可以一直播放到底
代码演示:这个例子会监听id为example的 audio 的数据. 他会检查当前位置是否可以播放, 会的话执行播放。
const obj = document.getElementById('example');obj.addEventListener('loadeddata', function() { if(obj.readyState >= 2) { obj.play(); }});
西瓜播放器中的使用方式:
get readyState () { let status = [{ en: 'HAVE_NOTHING', cn: '没有关于音频/视频是否就绪的信息' }, { en: 'HAVE_METADATA', cn: '关于音频/视频就绪的元数据' }, { en: 'HAVE_CURRENT_DATA', cn: '关于当前播放位置的数据是可用的,但没有足够的数据来播放下一帧/毫秒' }, { en: 'HAVE_FUTURE_DATA', cn: '当前及至少下一帧的数据是可用的' }, { en: 'HAVE_ENOUGH_DATA', cn: '可用数据足以开始播放' }] return this.lang ? this.lang[status[this.video.readyState].en] : status[this.video.readyState] }