当前位置:首页 » 《随便一记》 » 正文

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

15 人参与  2024年04月05日 08:02  分类 : 《随便一记》  评论

点击全文阅读


  

大厂技术  高级前端  Node进阶

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

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

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

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

视频播放的技术方案调研

服务端实现:

视频转码

生成不同码率的视频

进行视频标准加密

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

web端实现

web端播放器的设计

web端播放器的自定义扩展

可拖拽进度条

音量控制

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

手动清晰度切换

倍速播放

样式自定义覆盖

标准加密视频播放

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

各浏览器控件统一

其中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 header            if (chunk.byteLength > 13) {                let probeData = FLVDemuxer.probe(chunk);                offset = probeData.dataOffset;            } else {                return 0;            }        }        if (this._firstParse) {  // handle PreviousTagSize0 before Tag1            this._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的兼容性问题。

业务场景需求

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

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

有现成的技术方案,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.infoconst 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端需要知道如何获取密钥

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

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

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

51df14daecee040eca1a6de9e4634e79.jpeg

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

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

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

采用私有加密方式,比如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,能满足

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

方便扩展和样式覆盖

支持hls播放

尽量适配前端的各种框架

方便接入,实现价值

#2.播放器设计

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

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

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

video生命周期:loadedmetacanplay, ended, error

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

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

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

#(1).video的生命周期

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

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

在不同的生命周期,能挂载自定义事件。比如视频触发了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.detail    if(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.detail    player = 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>

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

#八.总结

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

视频的标准加密

视频的动态码率播放

视频的分片加载

可拖拽进度条

音量控制

手动清晰度切换

倍速播放

样式自定义覆盖

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

这是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://zhangshiyu.com/post/90632.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1