最完整的Web视频加密播放技术实现(含技术调研和Demo源码)

 

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

作者:然燃 (感谢小伙伴投稿分享)
原文链接: https://juejin.cn/post/7307934456995856419

最近又遇到了web视频化的场景,之前也有过调研:H5视频化调研浅析1

但这次稍微复杂一些,这次解决的是:

  1. 视频播放的技术方案调研

服务端实现:

  1. 视频转码

  2. 生成不同码率的视频

  3. 进行视频标准加密

  4. 不同码率视频合并,用于动态码率播放

web端实现

  1. web端播放器的设计

  2. web端播放器的自定义扩展

  3. 可拖拽进度条

  4. 音量控制

  5. 根据当前带宽自适应码率切换

  6. 手动清晰度切换

  7. 倍速播放

  8. 样式自定义覆盖

  9. 标准加密视频播放

  10. 基于原生开发,可在所有框架运行,统一跨框架情况

  11. 各浏览器控件统一

其中web端源码已添加MIT协议并完全开源,如果看完对大家有帮助的话,欢迎大家star,issue,pr,也希望能友好交流~

demo地址:⇲https://chaxus.github.io/ran/src/ranui/player/

源码地址:⇲https://github.com/chaxus/ran/tree/main/packages/ranui

demo文档做了国际化,可切换到中文

任何一个项目,立项肯定先是技术调研,我们先看看一些大公司的视频播放方案

#一.一些知名公司的web视频播放方案

#1.B站

我们先看看B站的,毕竟B站的主营业务就是视频弹幕网站,简直专业对口。

先找一个例子:⇲www.bilibili.com/video/BV1FM…⇲2 访问它。

cf77803effd8b6cb16ac8cea61d63ee3.jpeg

打开控制台,可以看到,视频在播放的时候,会不断的请求m4s的视频文件。

毕竟一整个视频文件往往比较大,不可能先请求完视频文件,再进行播放。因此将一个大的视频文件分割成很多小的片段,边加载边播放,是一种更好的方式。

1d4ccd9f8115b215fb838501e9f24e3d.jpeg

每次请求的m4s文件大概在几十kb到几百kb不等。

c1c0fae76667dd98ab5001064d4f858e.jpeg

那为什么不采用httprange呢,可以请求一个文件的部分内容,而且粒度更细,可以设置字节范围。在http请求的header中,类似这样

Range: bytes=3171375-3203867

我们可以检查这个链接请求https://upos-sz-mirror08c.bilivideo.com/upgcxcode/67/92/1008149267/1008149267-1-30064.m4s的请求头,就能发现,B站采用的是,即分片加载,同时还用了range的方式。

#2. 爱奇艺:(爱奇艺、土豆、优酷)

爱奇艺这里就不贴视频链接了,因为随便点一个视频,都要先看广告。

4bec7db7685c2e855aa225712948318a.jpeg

爱奇艺的视频主要请求的是f4v格式,也是分片加载。

播放一个视频时,请求多个f4v文件。

也采用Range。但和B站不一样的是,B站的Range属性是在m4s请求的请求头里面,而爱奇艺的看起来是在querystring上,在请求query上带着range参数。

因为没发现这个请求的header里面有range参数。比如: https://v-6fce1712.71edge.com/videos/other/20231113/6b/bb/3f3fe83b89124248c3216156dfe2f4c3.f4v?dis_k=2ba39ee8c55c4d23781e3fb9f91fa7a46&dis_t=1701439831&dis_dz=CNC-BeiJing&dis_st=46&src=iqiyi.com&dis_hit=0&dis_tag=01010000&uuid=72713f52-6569e957-351&cross-domain=1&ssl=1&pv=0.1&cphc=arta&range=0-9000

#3.抖音:

抖音的方案简单粗暴,访问的链接是这个: ⇲m.ixigua.com/douyin/shar…⇲3

通过查看控制台,我们可以发现,直接请求了一个视频的地址

81d3e0907c5db897ccb9f5c5bcc383e7.jpeg

没有进行分片,但用到了请求range,所以可以看到视频,是边播放边缓冲一部分。

不过我在开发的时候发现,目前租用的服务云厂商,默认会帮我们实现这项技术。

因为我把mp4视频上传到云服务器,通过链接进行播放的时候,就是边缓冲边播放的。

我们可以直接把这个视频地址拿出来,放到浏览器里面能直接播放,这样观察更明显。

7fdb93690ac82a0fb3476f9b0aabaacf.jpeg

但B站和爱奇艺却不能这样,因为他们采用的m4sf4v都不是一种通用的视频格式,需要使用专门的软件或工具才能打开和编辑。

#4.小红书:

测试用的例子链接:⇲www.xiaohongshu.com/discovery/i…⇲4

小红书的方案更加简单粗暴,打开控制台,直接告诉你就是请求一个mp4,然后直接播放就完事了。

af542b7d676c0dbda3252f1fc387a09f.jpeg

#5.总结

看完了以上的各家大厂的方案,我们可以看到,基本原理都是边播放边加载,减少直接加载大视频文本的成本。并且通过分片传输,还能动态控制视频码率(清晰度)。做到根据网速,加载不同码率的分片文件,做到动态码率适应。

同时采用的视频格式,比如f4vm4s,都不是能直接播放的媒体格式,需要一定的处理。增加盗取视频的成本,增加一定的安全性。

如果没有强要求,也可以直接采用mp4,或者直接用video播放一个视频文件地址。

#二.常见的视频格式与协议

我们知道视频的常见格式有mp4,同时上面介绍了B站播放用的m4s格式,爱奇艺用的f4v格式

  • •除了这些还有哪些视频格式?

  • •为什么有这么多视频格式,有哪些不同点呢?

  • •为什么这些公司会采用这种格式来播放视频呢?

#1. B站用的m4s

M4S格式不是一种通用的视频格式,需要使用专门的软件或工具才能打开和编辑。

M4S 通常会和 MPEG-DASH 流媒体技术一起,通过流式传输的视频的一小部分。播放器会按接收顺序播放这些片段。第一个 M4S 段会包含一些初始化的数据标识。

MPEG-DASH 是一种自适应比特率流媒体技术,通过将内容分解为一系列不同码率的M4S片段,然后根据当前网络带宽进行自动调整。如果想在在web音视频中采用DASH技术,可以看下 ⇲github.com/Dash-Indust…⇲5

#2. 爱奇艺的f4v

F4V是一种流媒体格式,它是由Adobe公司推出的,继FLV格式之后支持H.264编码的流媒体格式。F4V格式的视频不是一种通用的视频格式,但通常情况下,都可以将文件后缀改为FLV,这样就可以使用支持FLV的播放器进行观看。

FLV格式跟常见的MP4格式比起来,结构更加简单,所以加载metadata(视频元数据,比如视频时长等信息)会更快。具体结构我们可以在这里查到:⇲en.wikipedia.org/wiki/Flash_…⇲6

比如,这是FLV文件的标准头,定义了从几个比特到几个比特之间,是什么含义。我们知道后,可以用MediaSource进行读取和转码。

Field Data Type Default Details  
Signature 签名byte[3]"FLV"  始终就是“FLV”
Version 版本uint81 只有0x01才有效
Flags 标志uint8 位掩码0x050x04是音频,0x01是视频(所以0x05是音频+视频)
Header Size uint32_be9用于跳过较新的扩展标头

MP4格式会稍微复杂一些,具体标准在 ⇲ISO/IEC 14496-12⇲7 大概有两百多页,这里放不下,对这方面有兴趣的可以自行查看。

然而这并不表示MP4更差,因为它是一种基础通用标准,所以定义上会留有很多空间,和各种情况,甚至允许在标准之内进行自行发挥和扩展。而FLV格式则更加固定,但优点也是更加简单。

对于FLV的视频播放,我们可以采用:⇲github.com/bilibili/fl…⇲8flvjs主要作用就是用MediaSourceflv转码成mp4从而喂给浏览器进行播放。

接下来是一些其他的视频格式,简单介绍一下:

#3.AVI

文件名以.avi结尾,AVI 最初由 Microsoft1992 年开发,是 Windows 的标准视频格式。AVI 文件使用较少的压缩来存储文件,并且比许多其他视频格式占用更多空间,这导致文件大小非常大,每分钟视频大约 2-3 GB

无损文件不会随着时间的推移而降低质量,无论您打开或保存文件多少次。此外,这允许在不使用任何编解码器的情况下播放。参考资料:⇲Audio Video Interleave⇲9

#4.MPEG

文件名以“.mpg”或“.mpeg”结尾,MPEG 是由 ISO 和 IEC 联合成立的工作组联盟,旨在制定媒体编码标准,包括音频、视频、图形和基因组数据的压缩编码;以及各种应用程序的传输和文件格式。MPEG 格式用于各种多媒体系统。最广为人知的旧 MPEG 媒体格式通常使用 MPEG-1、MPEG-2 和 MPEG-4 AVC 媒体编码,MPEG-2 系统传输流和节目流。较新的系统通常使用 MPEG 基本媒体文件格式和动态流式处理(又名 .MPEG-DASH)。参考资料:⇲Moving Picture Experts Group⇲10

#5.MP4

带有音频和视频的 MPEG-4 文件通常使用标准的 .mp4 扩展名。纯音频 MPEG-4 文件通常具有 .m4a 扩展名,原始 MPEG-4 可视比特流命名为 .m4v。Apple iPhone使用MPEG-4音频作为其铃声,但使用.m4r扩展名而不是.m4a扩展名。参考资料:⇲MPEG-4 Part 14⇲11

#6.QuickTime

文件名以“.mov”结尾,QuickTime 能够包含媒体数据的抽象数据引用,并将媒体数据与媒体偏移和轨道编辑列表分离,这意味着 QuickTime 特别适合编辑,因为它能够就地导入和编辑(无需数据复制)。由于 QuickTime 和 MP4 容器格式都可以使用相同的 MPEG-4 格式,因此在仅限 QuickTime 的环境中,它们大多可以互换。MP4作为国际标准,得到了更多的支持。参考资料:⇲QuickTime File Format⇲12

#7.TS

TS是MPEG2-TS的简称,是一种音视频封装格式。TS流的后缀通常是.ts、.mpg或者.mpeg,多数播放器直接支持这种格式的播放。TS格式主要用于直播的码流结构,具有很好的容错能力。

#三.浏览器对各种视频格式的兼容性

上面了解常用的视频格式,和适用范围之后,还需要看一下当前浏览器,对各种视频格式的支持程度,然后制定技术方案。

#1. Chrome

支持的视频格式从官方文档可以查到,主要有以下这些

  • •MP4 (QuickTime/ MOV / ISO-BMFF / CMAF)

  • •Ogg

  • •WebM

  • •WAV

  • •HLS [Only on Android and only single-origin manifests

官方文档如下:⇲www.chromium.org/audio-video…⇲13

#2. Safari

支持的视频格式有这些:

91edb88d6ab5e64d80d385ee49bcda6a.jpeg

官方文档:⇲developer.apple.com/library/arc…⇲14

#3.Firefox

支持的视频格式:

56a808d8b178595d8c93065aeb52e154.jpeg

官方文档:⇲support.mozilla.org/en-US/kb/ht…⇲15

#四.MediaSource和视频编码,解码,封装介绍

上面介绍了一些视频格式,和目前浏览器的一些兼容性问题。就能发现,在web上播放音视频其实限制还是很大的。如何解决这些限制,就会用到MediaSource

视频其实是无数个图片的叠加,如果视频是一秒60帧,大约一秒中需要播放60张图片。这就导致一个几分钟的视频,就会非常大。比如上面介绍的无损格式,avi格式,每分钟视频大约 2-3 GB。这时候视频就需要进行编码。其实就是压缩。

编码分为视频编码和音频编码,常见的视频编码有:

  • MPEG系列MPEG-1第二部分、MPEG-2第二部分(等同于H.262)、MPEG-4第二部分、MPEG-4第十部分(等同于H.264,有时候也被叫做“MPEG-4 AVC”或“H.264/AVC”)。

  • H.26x系列H.261H.262H.263H.264(等同于MPEG-4第十部分)、H.265/HEVCITU-TISO/IEC联合推出)。

  • 其它视频编码WMV系列、RV系列、VC-1DivXXviDX264X265VP8VP9Sorenson VideoAVS

常见的音频编码有:AACMP3AC-3

编码之后,还需要将音频和视频合并在一个文件里,这就是封装

38bb58bda9058599d93f0758ba27389e.jpeg

所以相对的,播放一个视频,就需要解封装,解码,音视频同步喂给声卡和显卡进行播放。

MediaSource做的就是这个工作,读取视频流,转换成浏览器能播放的格式。

以下是flv.jsparseChunks部分内容。读取buffer,一个字节一个字节的根据标准进行解析。然后转码。

if (byteStart === 0) {  // buffer with FLV headerif (chunk.byteLength > 13) {let probeData = FLVDemuxer.probe(chunk);offset = probeData.dataOffset;} else {return 0;}}if (this._firstParse) {  // handle PreviousTagSize0 before Tag1this._firstParse = false;if (byteStart + offset !== this._dataOffset) {Log.w(this.TAG, 'First time parsing but chunk byteStart invalid!');}let v = new DataView(chunk, offset);let prevTagSize0 = v.getUint32(0, !le);if (prevTagSize0 !== 0) {Log.w(this.TAG, 'PrevTagSize0 !== 0 !!!');}offset += 4;}

#1.MediaSource兼容性

2d4708a1efa0dcfd67ac3d9de070d7ca.jpeg

由此可见,基本都是绿色,但有一个特殊情况,就是Safari on IOS。这部分支持程度还是棕色。

#五. HLS 播放方案

采用HLS技术方案,有以下几个原因:

1.兼容性

上面介绍了各种视频格式,还有浏览器的兼容性

其中 HLS协议是Apple公司实现的,在 Apple 的全系列产品包括 iPhoneiPadSafari 等都可以原生支持播放 HLS

对于其他浏览器,可以通过MediaSource解封装,解码,转码,进行播放。

这样也就解决了MediaSource的兼容性问题。

  1. 业务场景需求

目前对于视频的加密有着强需求,比如需要用户付费才能观看一些视频。而HLS协议天然自带标准加密,同时也能基于HLS扩展私有加密。

  1. HLS协议自带支持分片传输和动态码率自适应播放。

  2. 有现成的技术方案,Hls.js

#六. 服务端开发

选择了采用HLS协议的播放方式,那么首先需要处理视频,这部分目前是在服务端进行处理。利用ffmpeg的能力。

如果以后能将ffmpeg搬上浏览器,且没有性能问题就好了。现在有类似的webassemblynpm包,但性能有点小问题

#1.视频的转码

视频的转码的ffmpeg命令如下:

ffmpeg -i input.mp4 -hls_time 10 -hls_list_size 0 -c:v h264 -b:v 2M -hls_segment_filename output_%05d.ts output.m3u8 -y

每个参数的解释:

  • -i 指定输入的视频

  • -hls_time 指定分片的时间,单位是秒

  • -hls_list_size 指定hls列表的数量,这里不限制

  • -c:v 指定视频的编码格式

  • -b:v 指定视频的码率,这里是2M比特率

  • -hls_segment_filename 指定输出的ts文件名字,这里表示是output_ + 五位数字

  • output.m3u8指定输出m3u8文件的名字

  • -y有些场景,比如是否覆盖,直接选择是,避免程序卡住

为了自动化执行,这里会用到nodespawn模块,创建一个子进程,在子进程中执行ffmpeg的命令。

const exec = ({ params, data }: ExecOption): Promise<ExecResult> => {return new Promise((r, j) => {const cp = spawn('ffmpeg', params);cp.stderr.pipe(process.stdout);cp.on('error', (err) => {j(err);});cp.on('close', (code) => {r({ code, data });});cp.on('exit', (code) => {r({ code, data });});});
};

这时候,视频就会在指定的位置输出了,会生成一个m3u8和多个ts

a1587ca131a711b0cbeb2d6d58d719bb.jpeg

ts是视频文件,m3u8更像是索引文件,用来描述ts,比如在什么时间,播放什么ts。主要内容如下:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.380622,
5_00000.ts
#EXTINF:10.380622,
5_00001.ts
#EXTINF:10.380622,
5_00002.ts
#EXTINF:10.380622,
5_00003.ts
#EXTINF:6.560556,
5_00004.ts
#EXTINF:1.619378,
5_00005.ts
#EXTINF:5.024189,
5_00006.ts
#EXT-X-ENDLIST

#2.视频的标准加密

HLS协议标准加密采用的是AES对称加密方案。先来实现一个最标准的加密:

首先通过node原生模块crypto生成加密密钥:

import crypto from 'node:crypto';
// 生成加密密钥
const key = crypto.createHash('sha256').update(crypto.randomBytes(32)).digest('base64');
const filePathKey = path.join(__dirname,`../../public/uploads/hls/${dir}/${fileName}.key`);const content = `${ctx.origin}/uploads/hls/${dir}/${fileName}.key\n${filePathKey}\n`;
// 密钥的文件
const fileKey = await writeFile(path.join(__dirname, `../../public/uploads/hls/${dir}/${fileName}.key`),key);
const keyInfoPath = path.join(__dirname,`../../public/uploads/hls/${dir}/${fileName}_key.bin`);
// ffmpeg 需要的key.info
const keyInfo = await writeFile(keyInfoPath, content);

然后再执行ffmpeg命令,这里同样需要用nodespawn模块进行封装成接口:

ffmpeg -i input.mp4 -hls_time 10 -hls_list_size 0 -c:v h264 -b:v 2M -hls_key_info_file keyInfoPath -hls_segment_filename output_%05d.ts output.m3u8 -y

主要就增加了一个hls_key_info_file参数,表示加密密钥的地址。这时候,生成的m3u8文件就发生了变化,多了一行:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="http://localhost:30103/uploads/hls/5_1701577743851/5.key",IV=0x00000000000000000000000000000000
#EXTINF:10.380622,
5_00000.ts
#EXTINF:10.380622,
5_00001.ts
#EXTINF:10.380622,
5_00002.ts
#EXTINF:10.380622,
5_00003.ts
#EXTINF:6.560556,
5_00004.ts
#EXTINF:1.619378,
5_00005.ts
#EXTINF:5.024189,
5_00006.ts
#EXT-X-ENDLIST

多了

#EXT-X-KEY:METHOD=AES-128,URI="http://localhost:30103/uploads/hls/5_1701577743851/5.key",IV=0x00000000000000000000000000000000
  • METHOD字段表示加密方式,这里是AES

  • URI表示密钥地址,这里是http://localhost:30103/uploads/hls/5_1701577743851/5.key

  • IV是加密解密时的偏移量,现在是0

上述加密方式,虽然视频确实加密了,但会把密钥地址写在m3u8里。等于把房间上锁,然后在锁上贴一个纸条,上面写了密码。

#3.更好的安全方案

  • •有加密,必然需要解密

首先我们知道,视频要在web端进行播放,那么无论如何,都肯定需要先解密,再播放。

  • •肯定不能在web端放置密钥

  • web端需要知道如何获取密钥

  • •密钥用一次即失效,每次加密视频都生成新的密钥

目前更好的安全性方式主要有两种:

  1. 在请求密钥的地址上进行加固:

51df14daecee040eca1a6de9e4634e79.jpeg
  • •校验cookie,既然是发起请求,那么同域名会自动携带cookie,只有购买过的用户才能获取密钥。(总不能让付费的用户也不能看吧)

  • •生成密钥链接时,带上ticket,短时间失效,控制时效性

  • •请求头携带auth,进行用户校验。比如jwt方案就是如此

  1. 采用私有加密方式,比如m3u8里的METHOD,可能不再是AES这种对称加密。自定义一套加密规则,这种方式安全性会极大提高,但同时就不遵守HLS协议的标准了。但大多数浏览器支持MediaSource。可以读取文件内容,进行自定义加密和解密。根据上文的兼容性调研,MediaSourceIOS上将会有兼容性问题,所以这种方案在IOS上也会有兼容性问题。

#4.自适应码率播放

这里先介绍一下码率和清晰度的关系:

码率是指:

码率(也称为比特率)是指视频文件在单位时间内使用的数据流量。它反映了视频文件的数据压缩程度,码率越高,压缩比就越小,画面质量就越高,但文件体积也越大。通俗来说,码率可以看作是取样率,是视频编码中画面质量控制中最重要的部分。计算公式是文件体积=时间X码率/8。

所以简单来说,码率越高,清晰度就越好。成正比关系。

需要根据视频的质量,和业务场景,去定义,这里给出阿里云对码率和清晰度的定义,可供参考:

9cb5a8f2d3bd4146cbc21db2d214f70f.jpeg

为了实现自适应码率播放,我们需要将不同码率的m3u8合并成一个多码率的m3u8

这里我没找到合适的ffmpeg命令,但总有办法的,最万能的方法就是,查看HLS协议中多码率的m3u8格式标准,自己写一个。

目前用node去写一个这样的索引文件,具体内容如下:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
hls/5_1701577771368/5.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=2,BANDWIDTH=50000,CODECS="mp4a.40.5,avc1.42000d",RESOLUTION=320x184,NAME="320"
hls/5_1701577744714/5.m3u8
  • #EXT-X-STREAM-INF: 流媒体的描述

  • PROGRAM-ID: 表示唯一的ID

  • BANDWIDTH: 流媒体的带宽,即每秒传输的数据量。这里带宽为50000,意味着每秒传输的数据量大约为50kbps

  • CODECS: 流媒体使用的编解码器。这里是使用了mp4a.40.5AAC音频编码)和avc1.42000dAVC视频编码)。

  • RESOLUTION: 这个字段指示了视频的分辨率,即宽度和高度。在这个例子中,视频分辨率为320x184

  • NAME: 这个字段为流媒体提供了一个名称,本例中名称为"320"。我会习惯把清晰度放在NAME这个字段这里,方便web端获取

实现一个接口,传入以上的参数,动态拼接字符串,写入文件

async generateMasterPlayList(ctx: Context): Promise<void> {try {const { paths, filename = Date.now() } = ctx.request.body;let content = `#EXTM3U\n`;paths.forEach((item: MasterPlayListOption, index: number) => {const { id = index, bandWidth, codecs, resolution, name, url } = item;content += `#EXT-X-STREAM-INF:PROGRAM-ID=${id},BANDWIDTH=${bandWidth},CODECS="${codecs}",RESOLUTION=${resolution},NAME="${name}"\n${url}\n`;});const dir = path.join(__dirname, `../../public/uploads/hls/`);if (!existsSync(dir)) {await createDir(dir);}const filePath = dir + (filename.toString().endsWith('.m3u8') ? filename : `${filename}.m3u8`)const { success, error } = await writeFile(filePath, content);const basename = path.basename(filePath);return success? ctx.successHandler({url: `${ctx.origin}/uploads/hls/${basename}`,}): ctx.failHandler(error);} catch (error) {ctx.errorHandler(error);}}

那么HLS是如何自适应码率的呢?

其实是根据BANDWIDTH这个字段,因为我们给不同的视频设置了不同的BANDWIDTH。那么就可以根据当前的网速,进行动态切换。

#七.web端的实现

上面做了大量的工作,主要是生成了HLS协议的视频播放的地址。接下来就是如何在web端进行播放。

#1.技术选型

我首先是看了现有的播放器npm,比较知名的

  • •有西瓜playler: ⇲github.com/bytedance/x…⇲16

  • •阿里云点播方案:⇲help.aliyun.com/zh/vod/deve…⇲17

  • •知乎的player:⇲github.com/zhihu/griff…⇲18

其中知乎和西瓜的播放器是开源的,阿里云点播方案没有开源代码,但是有开源demo

ddebace975d72867d4d6cec0f3099d75.jpeg

但基本上实现的功能都很丰富,同时配置项会不断的增加。

如果只是简单的初始化一个播放器,那还好。但一般这种场景,我们都会对播放器进行一定程度的定制化。比如B站。

5129f02c74064c8fe24ccaa6968c5350.jpeg

就多了很多自定义的控件。进度条也是小电视的形状。

我们这边也是如此,有自己的主题色,有自己的播放按钮等等,还有一些业务功能,也要放在控制条上。

上述的开源方案,都需要花时间去研究配置项,而且使用方法都是new Player(options)的形式。

但尽量视图的归视图,逻辑的归逻辑会更好些。

更何况实现一个播放器也不是很困难。

期望的player,能满足

  1. 配置够简单,最好看到就知道是怎么用的

  2. 方便扩展和样式覆盖

  3. 支持hls播放

  4. 尽量适配前端的各种框架

  5. 方便接入,实现价值

#2.播放器设计

由于现在既有react项目,也有vue项目,甚至还有一些老的jquery项目。为了做到一次开发,任何项目都可以使用和接入。采用了web components技术方案。

web components就不做介绍了,具体可以去看这篇文章:⇲手写web components组件⇲19简单来说就是可以自定义元素,让我们像使用section一样使用自定义元素。

播放器我们需要考虑的点有:

  1. video生命周期:loadedmetacanplay, ended, error

  2. video状态:播放,正在播放,暂停,静音等

  3. video属性:总时长,当前时长,音量大小,倍速等

  4. video交互:暂停,播放,知识点,清晰度,倍速,全屏,进度控制,音量控制等

#(1).video的生命周期

对于video的生命周期,我们期望做到两件事情:

  1. 我们能知道当前video处于什么生命周期

  2. 在不同的生命周期,能挂载自定义事件。比如视频触发了ended。我们需要在ended时期跳转下一个视频

因此,我们需要监听video的生命周期:

listenEvent = () => {if (!this._video) return;this.clearListenerEvent();this._video.addEventListener('canplay', this.onCanplay);this._video.addEventListener('canplaythrough', this.onCanplaythrough);this._video.addEventListener('complete', this.onComplete);this._video.addEventListener('durationchange', this.onDurationchange);this._video.addEventListener('emptied', this.onEmptied);this._video.addEventListener('ended', this.onEnded);this._video.addEventListener('error', this.onError);this._video.addEventListener('loadeddata', this.onLoadeddata);this._video.addEventListener('loadedmetadata', this.onLoadedmetadata);this._video.addEventListener('loadstart', this.onLoadstart);this._video.addEventListener('pause', this.onPause);this._video.addEventListener('play', this.onPlay);this._video.addEventListener('playing', this.onPlaying);this._video.addEventListener('progress', this.onProgress);this._video.addEventListener('ratechange', this.onRatechange);this._video.addEventListener('seeked', this.onSeeked);this._video.addEventListener('seeking', this.onSeeking);this._video.addEventListener('stalled', this.onStalled);this._video.addEventListener('suspend', this.onSuspend);this._video.addEventListener('timeupdate', this.onTimeupdate);this._video.addEventListener('volumechange', this.onVolumechange);this._video.addEventListener('waiting', this.onWaiting);};

在触发不同的时期时,让开发者知道。所以我们要先自定义事件,进行触发

const change = (name: string, value: unknown): void => {
const currentTime = this.getCurrentTime();
const duration = this.getTotalTime();
this.dispatchEvent(new CustomEvent('change', {detail: {type: name,data: value,currentTime,duration,tag: this, // 整个player的实例},}),
);
};
const onCanplaythrough = (e: Event) => {this.ctx.currentState = e.type;this.change('canplaythrough', e);
};

这样就可以,当生命周期事件触发后,就会触发onchange。 在使用上,我们可以:

<r-player onChange={change} src="hls/example.m3u8" ></r-player>const change = (e:CustomEvent) => {const { type, data, currentTime, duration, tag } = e.detailif(type === 'ended'){console.log('video ended')}
}

其中type的类型有:

名称说明
canplay浏览器可以播放媒体文件了,但估计没有足够的数据来支撑播放到结束,不必停下来进一步缓冲内容。
canplaythrough浏览器估计它可以在不停止内容缓冲的情况下播放媒体直到结束。
completeOfflineAudioContext 渲染完成。
durationchangeduration 属性的值改变时触发。
emptied媒体内容变为空;例如,当这个 media 已经加载完成(或者部分加载完成),则发送此事件,并调用 load() 方法重新加载它。
ended视频停止播放,因为 media 已经到达结束点。
loadedmetadata已加载元数据。
progress在浏览器加载资源时周期性触发。
ratechange播放速率发生变化。
seeked跳帧(seek)操作完成。
seeking跳帧(seek)操作开始。
stalled用户代理(user agent)正在尝试获取媒体数据,但数据意外未出现。
suspend媒体数据加载已暂停。
loadeddatamedia 中的首帧已经完成加载。
timeupdatecurrentTime 属性指定的时间发生变化。
volumechange音量发生变化。
waiting由于暂时缺少数据,播放已停止。
play播放已开始。
playing由于缺乏数据而暂停或延迟后,播放准备开始。
pause播放已暂停。
volume音量发生变化。
fullscreen触发全屏事件

在不同的生命周期,能挂载事情,因此我们需要一个发布订阅类。

type Callback = (...args: unknown[]) => unknown;type EventName = string | symbol;type EventItem = {name?: string | symbol;callback: Callback;initialCallback?: Callback;
};const NEW_LISTENER = 'NEW_LISTENER';export class SyncHook {private _events: Record<EventName, Array<EventItem>>;constructor() {this._events = {};}on = (eventName: EventName, eventItem: EventItem | Callback): void => {if (this._events[eventName] && eventName !== Symbol.for(NEW_LISTENER)) {this.emit(Symbol.for(NEW_LISTENER), eventName);}const callbacks = this._events[eventName] || [];if (typeof eventItem === 'function') {callbacks.push({name: eventName,callback: eventItem,});} else {callbacks.push(eventItem);}this._events[eventName] = callbacks;};emit = (eventName: EventName, ...args: Array<unknown>): void => {const callbacks = this._events[eventName] || [];callbacks.forEach((item) => {const { callback } = item;callback(...args);});};once = (eventName: EventName, eventItem: EventItem | Callback): void => {let one: EventItem;if (typeof eventItem === 'function') {one = {name: eventName,callback: (...args: Array<unknown>) => {eventItem(...args);this.off(eventName, one);},initialCallback: eventItem,};} else {const { callback } = eventItem;one = {name: eventName,callback: (...args: Array<unknown>) => {callback(...args);this.off(eventName, one);},initialCallback: callback,};}this.on(eventName, one);};off = (eventName: EventName, eventItem: EventItem | Callback): void => {const callbacks = this._events[eventName] || [];const newCallbacks = callbacks.filter((item) => {if (typeof eventItem === 'function') {return (item.callback !== eventItem && item.initialCallback !== eventItem);} else {const { callback } = eventItem;return item.callback !== callback && item.initialCallback !== callback;}});this._events[eventName] = newCallbacks;};
}

因此会给player元素上增加一个ctx属性,作为全局的上下文。

this.ctx = {currentTime: 0, // 当前时间duration: 0, // 总时长currentState: '', // 当前视频状态action: new SyncHook(), // 不同时期触发的状态
};

我们想订阅视频的结束事件,我们可以

通过Ref的方式:

<r-player ref={PlayerRef} onChange={change} src="hls/example.m3u8" ></r-player>const endedEvent = () => {console.log('video ended')
}PlayerRef.current.ctx.action.off('ended',endedEvent)PlayerRef.current.ctx.action.on('ended',endedEvent)

通过change方法获取的实例:

<r-player onChange={change} src="hls/example.m3u8" ></r-player>let playerconst endedEvent = () => {console.log('video ended')
}const change = (e:CustomEvent) => {const { type, data, currentTime, duration, tag } = e.detailplayer = tag
}player.action.off('ended',endedEvent)
player.action.on('ended',endedEvent)

#(2).video的状态和属性

需要在全局上下文中记录下播放器的状态和属性:

this.ctx = {currentTime: 0, // 当前时间duration: 0, // 总时长currentState: '', // 当前视频状态action: new SyncHook(), // 不同时期触发的状态volume: 0.5, // 当前音量playbackRate: 1, // 当前倍速clarity: '', // 当前清晰度fullScreen: false, // 是否全屏levels: [], // 清晰度列表url: '', // 当前播放的地址levelMap: new Map(), // 清晰度和名字的映射关系
};

#(3).自定义video

默认长这个样子

a99ea10c9950ad299536e03ef892a790.jpeg

demo地址:⇲chaxus.github.io/ran/src/ran…⇲20

源码地址:⇲github.com/chaxus/ran/…⇲21

如果不喜欢控制器或者按钮,直接样式覆盖,更符合直觉,没有学习成本。

.ran-player-controller{display: none
}

由于播放器本身就是一个元素,那么可以任意的在里面添加元素,添加逻辑。

<r-player onChange={change} src="hls/example.m3u8" ><section>111111</section>
</r-player>

所以,这就解决配置项,长达好几页的问题,同时看到也就知道怎么配置,怎么开发了。

#八.总结

目前已经从前后端的角度,实现了

  1. 视频的标准加密

  2. 视频的动态码率播放

  3. 视频的分片加载

  4. 可拖拽进度条

  5. 音量控制

  6. 手动清晰度切换

  7. 倍速播放

  8. 样式自定义覆盖

  9. 基于原生开发,可在所有框架运行,统一跨框架情况,各浏览器控件统一

这是demo和源码地址:

demo和文档地址:⇲https://chaxus.github.io/ran/src/ranui/player/

源码地址:⇲https://github.com/chaxus/ran/tree/main/packages/ranui

demo文档做了国际化,可切换到中文

Node 社群

 

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

f2c57f15eca7b318bd67fd526adfa039.png

“分享、点赞、在看” 支持一下

参考资料

[1]

H5视频化调研浅析:https://juejin.cn/post/7238739662822735933

[2]

www.bilibili.com/video/BV1FM…:https://www.bilibili.com/video/BV1FM411N7LJ

[3]

m.ixigua.com/douyin/shar…:https://m.ixigua.com/douyin/share/video/7206914252840370721?aweme_type=107&schema_type=1&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme

[4]

www.xiaohongshu.com/discovery/i…:https://www.xiaohongshu.com/discovery/item/63b286d1000000001f00b495

[5]

github.com/Dash-Indust…:https://github.com/Dash-Industry-Forum/dash.js

[6]

en.wikipedia.org/wiki/Flash_…:https://en.wikipedia.org/wiki/Flash_Video#Flash_Video_Structure

[7]

ISO/IEC 14496-12:https://www.iso.org/standard/83102.html

[8]

github.com/bilibili/fl…:https://github.com/bilibili/flv.js

[9]

Audio Video Interleave:https://en.wikipedia.org/wiki/Audio_Video_Interleave

[10]

Moving Picture Experts Group:https://en.wikipedia.org/wiki/Moving_Picture_Experts_Group

[11]

MPEG-4 Part 14:https://en.wikipedia.org/wiki/MP4_file_format

[12]

QuickTime File Format:https://en.wikipedia.org/wiki/QuickTime_File_Format

[13]

www.chromium.org/audio-video…:https://www.chromium.org/audio-video/

[14]

developer.apple.com/library/arc…:https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html

[15]

support.mozilla.org/en-US/kb/ht…:https://support.mozilla.org/en-US/kb/html5-audio-and-video-firefox

[16]

github.com/bytedance/x…:https://github.com/bytedance/xgplayer

[17]

help.aliyun.com/zh/vod/deve…:https://help.aliyun.com/zh/vod/developer-reference/overview-14

[18]

github.com/zhihu/griff…:https://github.com/zhihu/griffith

[19]

手写web components组件:https://juejin.cn/post/7170219296226803725

[20]

chaxus.github.io/ran/src/ran…:https://chaxus.github.io/ran/src/ranui/player/

[21]

github.com/chaxus/ran/…:https://github.com/chaxus/ran/tree/main/packages/ranui

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/228817.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Axure动态面板的使用

一. 动态面板 Axure动态面板是Axure RP软件中的一个功能模块&#xff0c;用于创建交互式原型和模拟应用程序的动态效果。它可以模拟用户在应用程序中的操作流程&#xff0c;并展示不同状态之间的变化&#xff0c;提供更真实的用户体验。通过创建不同的状态和添加交互效果&…

21--集合小案例

案例--图书管理系统 1.创建实体类Book package com.work.pojo; /** *Author: 憨憨浩浩 *CreateTime: 2023-12-16 17:27 *Description: Book实体类 */ public class Book {private int id; // 编号private String name; // 图书名称private String author;…

C++软件调试与异常排查技术从入门到精通学习路线分享

目录 1、概述 2、全面了解引发C软件异常的常见原因 3、熟练掌握排查C软件异常的常见手段与方法 3.1、IDE调试 3.2、添加打印日志 3.3、分块注释代码 3.4、数据断点 3.5、历史版本比对法 3.6、Windbg静态分析与动态调试 3.7、使用IDA查看汇编代码 3.8、使用常用工具分…

【AI】模型结构可视化工具Netron应用

随着AI模型的发展&#xff0c;模型的结构也变得越来越复杂&#xff0c;理解起来越来越困难&#xff0c;这时候能够画一张结构图就好了&#xff0c;就像我们在开发过程中用到的UML类图&#xff0c;能够直观看出不同层之间的关系&#xff0c;于是Netron就来了。 Netron支持神经网…

leetcode 236. 二叉树的最近公共祖先

leetcode 236. 二叉树的最近公共祖先 题目 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽…

Vue3使用Three.js导入gltf模型并解决模型为黑色的问题

背景 如今各类数字孪生场景对三维可视化的需求持续旺盛&#xff0c;因为它们可以用来创建数字化的双胞胎&#xff0c;即现实世界的物体或系统的数字化副本。这种技术在工业、建筑、医疗保健和物联网等领域有着广泛的应用&#xff0c;可以帮助人们更好地理解和管理现实世界的事…

VAR模型

VAR&#xff08;Vector Autoregression&#xff09;模型是一种用于时间序列分析的统计模型&#xff0c;它可以描述多个变量之间的相互关系和动态演化。VAR模型最初是由Sims&#xff08;1980&#xff09;提出的&#xff0c;广泛应用于宏观经济学、金融领域以及其他时间序列数据分…

Restrict Content Pro WordPress – 限制会员内容 付费内容网站(包含所有扩展)

Restrict Content Pro WordPress限制会员内容专业插件 强大的内容限制工具和强大的 WordPress 会员网站&#xff0c;都在一个易于管理的插件中。 购买Restrict Content Pro 最新版本并加入超过23000 名快乐客户的俱乐部。 使用 Restrict Content Pro 插件将您的独家内容锁定…

Python 全栈体系【四阶】(六)

第四章 机器学习 五、线性模型 1. 概述 线性模型是自然界最简单的模型之一&#xff0c;它描述了一个&#xff08;或多个&#xff09;自变量对另一个因变量的影响是呈简单的比例、线性关系。例如&#xff1a; 住房每平米单价为 1 万元&#xff0c;100 平米住房价格为 100 万…

windows电脑半夜突然睡眠自动唤醒的问题查找与治理

遇见几次了&#xff0c;半夜起来上厕所&#xff0c;发现休眠的电脑居然自己开了&#xff0c;还得跑过去把电脑再休眠&#xff0c;很烦。昨天晚上居然自动唤醒两次&#xff0c;忍无可忍了&#xff0c;于是开始查找原因。 查询原因如下&#xff0c;解决方面也在后面。 固件 S3 计…

Linux驱动开发学习笔记4《设备树下的LED驱动实验》

目录 一、设备树LED驱动原理 二、硬件原理图分析 三、实验程序编写 1.修改设备树文件 2.LED 灯驱动程序编写 3.编写测试APP 四、运行测试 1. 编译驱动程序和测试APP &#xff08;1&#xff09; 编译驱动程序 &#xff08;2&#xff09; 编译测试APP ​ 2.运行测试 一、…

Win11 PS无法拖动文件到任务栏打开

Win11 PS无法拖动文件到任务栏打开 1.软件环境2.问题描述3.解决方法3.1.确保Win11更新到22H2版本以上3.2.确保禁止拖放关闭3.3.修复系统注册表 4.修复效果预览 1.软件环境 Windows11 企业版64位 22H2 Adobe Photoshop 25.2.0 20231101.m.2385 38bb2d3 x64 2.问题描述 很多人在…

SpringBoot配置文件加载的优先级及自定义配置

Spring Boot使用一个非常特殊的PropertySource顺序&#xff0c;旨在允许合理的值重写&#xff0c;越靠前优先级越高。属性按以下顺序考虑&#xff1a; 开发者工具Devtools全局配置参数 在IDEA或Eclipse中&#xff0c;安装并启用Spring Boot Devtools插件。打开项目的Settings…

股票价格预测 | Python实现基于ARIMA和LSTM的股票预测模型(含XGBoost特征重要性衡量)

文章目录 效果一览文章概述模型描述源码设计效果一览 文章概述 Python实现基于ARIMA和LSTM的股票预测模型(Stock-Prediction) Data ExtractionFormatting data for time seriesFeature engineering(Feature Importance using X

信号与线性系统预备训练3——MATLAB软件在信号与系统中的应用初步

信号与线性系统预备训练3——MATLAB软件在信号与系统中的应用初步 The Preparatory training3 of Signals and Linear Systems 对应教材&#xff1a;《信号与线性系统分析&#xff08;第五版&#xff09;》高等教育出版社&#xff0c;吴大正著 一、目的 1.熟悉和回顾MATLAB…

Pycharm第三方库导入失败避坑!

最近遇到了明明安装了 python 第三方库&#xff0c;但是在 pycharm 当中却导入不成功的问题。 使用Pycharm手动安装三方库和自动安装三方库都失败&#xff0c;以及Pycharm终端使用pip命令安装也未解决。网上找各种方法尝试都没成功&#xff0c;原来是一不小心就跳进了虚拟环境…

C++中的继承(一)

文章目录 前言概念访问限定符基类和派生类的赋值转换继承中的作用域派生类的默认成员函数构造函数 拷贝构造析构函数 继承的其他一些细节 前言 我们之前说过&#xff0c;继承是面向对象的三大特性。 面向对象的三大特性&#xff1a; 封装、继承、多态。 封装在类和对象体现出…

2043杨辉三角(C语言)

目录 一&#xff1a;题目 二&#xff1a;思路分析 三&#xff1a;代码 一&#xff1a;题目 二&#xff1a;思路分析 1.通过杨辉三角&#xff0c;不难发现中间的数等于肩头两个数之和 2.但是当我们的输出结果&#xff0c;与杨辉三角的形式有所不同&#xff0c;但是我们可以找…

Freemarker基本语法与案例讲解

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《SpringBoot》。&#x1f3af;&#x1f3af; &…

python接口自动化测试-unittest-批量用例管理

我们日常项目中的接口测试案例肯定不止一个&#xff0c;当案例越来越多时我们如何管理这些批量案例&#xff1f;如何保证案例不重复&#xff1f;如果案例非常多&#xff08;成百上千&#xff0c;甚至更多&#xff09;时如何保证案例执行的效率&#xff1f;如何做&#xff08;批…