Web 视频播放前前后后那些事

})

.then(function(audioSegment1) {

audioSourceBuffer.appendBuffer(audioSegment1);

})

.then(function() {

return fetchSegment(“http://server.com/audio/segment2.mp4”);

})

.then(function(audioSegment2) {

audioSourceBuffer.appendBuffer(audioSegment2);

})

// …

// same thing for video segments

fetchSegment(“http://server.com/video/segment0.mp4”)

.then(function(videoSegment0) {

videoSourceBuffer.appendBuffer(videoSegment0);

});

// …

这意味着我们在服务器端也有那些多个段。在前面的示例中,我们的服务器至少包含以下文件:

./audio/

├── segment0.mp4

├── segment1.mp4

└── segment2.mp4

./video/

└── segment0.mp4

注意:音频或视频文件可能不会在服务器端真正进行切片,客户端可能会使用Range HTTP标头代替来获取切片的文件(或者,实际上,服务器可能会根据您的请求进行任何操作您返回具体内容)。

但是,这些情况是实现细节。在这里,我们将始终认为服务器端具有这些分片文件。

所有这些意味着, 我们不必等待整个音频或视频内容下载就可以开始播放。我们通常只需要第一部分。

当然,大多数播放器并不像我们在此处那样为每个视频和音频段手动执行此逻辑,但是他们遵循相同的想法:依次下载段并将其推入源缓冲区。

看到这种逻辑在现实生活中发生的一种有趣方式是,可以在Firefox / Chrome / Edge上打开网络监视器(在Linux或Windows上,键入“ Ctrl + Shift + i”,然后转到“网络”标签,在Mac上应依次为Cmd + Alt + i和“网络”),然后在您喜欢的流媒体网站中启动视频。

您应该可以看到各种视频和音频片段正在快速下载:

顺便说一句,您可能已经注意到,我们的段只是\被推送到源缓冲区中,而没有指示 WHERE, 参考时间正确的位置的地方进行添加。

实际上,片段的容器确实定义了应将它们放入整个媒体的时间。这样,我们不必在JavaScript中立即进行同步。

自适应码流 Adaptive Streaming

许多视频播放器具有“自动播放清晰度”功能,根据用户的网络和处理能力自动选择具体视频质量。

这是称为自适应流的网络播放器的核心问题。

借助媒体分片的概念,也可以启用此行为。

在服务器端,段实际上是用多种质量编码的。例如,我们的服务器可能存储了以下文件:

./audio/

├── ./128kbps/

| ├── segment0.mp4

| ├── segment1.mp4

| └── segment2.mp4

└── ./320kbps/

├── segment0.mp4

├── segment1.mp4

└── segment2.mp4

./video/

├── ./240p/

| ├── segment0.mp4

| ├── segment1.mp4

| └── segment2.mp4

└── ./720p/

├── segment0.mp4

├── segment1.mp4

└── segment2.mp4

然后,网络播放器将随着网络或CPU条件的变化自动选择正确的段进行下载。

这完全是用JavaScript完成的。例如,对于音频片段,它可能看起来像这样:

/**

  • Push audio segment in the source buffer based on its number

  • and quality

  • @param {number} nb

  • @param {string} language

  • @param {string} wantedQuality

  • @returns {Promise}

*/

function pushAudioSegment(nb, wantedQuality) {

// The url begins to be a little more complex here:

const url = “http://my-server/audio/” +

wantedQuality + “/segment” + nb + “.mp4”);

return fetch(url)

.then((response) => response.arrayBuffer());

.then(function(arrayBuffer) {

audioSourceBuffer.appendBuffer(arrayBuffer);

});

}

/**

  • Translate an estimated bandwidth to the right audio

  • quality as defined on server-side.

  • @param {number} bandwidth

  • @returns {string}

*/

function fromBandwidthToQuality(bandwidth) {

return bandwidth > 320e3 ? “320kpbs” : “128kbps”;

}

// first estimate the bandwidth. Most often, this is based on

// the time it took to download the last segments

const bandwidth = estimateBandwidth();

const quality = fromBandwidthToQuality(bandwidth);

pushAudioSegment(0, quality)

.then(() => pushAudioSegment(1, quality))

.then(() => pushAudioSegment(2, quality));

如您所见,我们将不同质量的段组合在一起没有问题,这里的 JavaScript 方面一切都是透明的。在任何情况下,容器文件都包含足够的信息,以使此过程平稳运行。

切换语言

在更复杂的网络视频播放器上,例如 Netflix,Amazon Prime Video 或 MyCanal 上的视频播放器,还可以根据用户设置在多种音频语言之间进行切换。

既然您知道了什么,对您来说,完成此功能的方法应该看起来很简单。

像自适应流一样,我们在服务器端也有许多段:

./audio/

├──./esperanto/

| ├──segment0.mp4

| ├──segment1.mp4

| └──segment2.mp4

└── ./french/

├──segment0.mp4

├──segment1.mp4

└──segment2.mp4

./video/

├──segment0.mp4

├──segment1.mp4

└── segment2.mp4

这次,视频播放器必须不根据客户端的功能而是根据用户的喜好在语言之间进行切换。

对于音频段,这是客户端上的代码:

// …

/**

  • Push audio segment in the source buffer based on its number and language.

  • @param {number} nb

  • @param {string} language

  • @returns {Promise}

*/

function pushAudioSegment(nb, language) {

// construct dynamically the URL of the segment

// and push it to the SourceBuffer

const url = “http://my-server/audio/” +

language + “/segment” + nb + “.mp4”

return fetch(url);

.then((response) => response.arrayBuffer());

.then(function(arrayBuffer) {

audioSourceBuffer.appendBuffer(arrayBuffer);

});

}

// recuperate in some way the user’s language

const language = getUsersLanguage();

pushAudioSegment(0, language)

.then(() => pushAudioSegment(1, language))

.then(() => pushAudioSegment(2, language));

您可能还希望在切换语言时“清除”以前的SourceBuffer的内容,以避免混合多种语言的音频内容。

这可以通过SourceBuffer.prototype.remove方法完成,该方法以秒为单位的开始和结束时间:

audioSourceBuffer.remove(0, 40);

当然,也可以将自适应流和多种语言结合在一起。我们可以这样组织服务器:

./audio/

├──./esperanto/

| ├──./128kbps/

| | ├──segment0.mp4

| | ├──segment1.mp4

| | └──segment2.mp4

| └── …/320kbps/

| ├──segment0.mp4

| ├──segment1.mp4

| └──segment2.mp4└──./

french/

├──./128kbps/

| ├──segment0.mp4

| ├──segment1.mp4

| └──segment2.mp4

└── ./320kbps/

├──segment0.mp4

├──segment1.mp4

└──segment2.mp4

./video/

├──./240p/

| ├──segment0.mp4

| ├──segment1.mp4

| └──segment2.mp4

└── ./720p/

├──segment0.mp4

├──segment1.mp4

└──segment2.mp4

而我们的客户将不得不同时管理语言和网络条件:

/**

  • Push audio segment in the source buffer based on its number, language and quality

  • @param {number} nb

  • @param {string} language

  • @param {string} wantedQuality

  • @returns {Promise}

*/

function pushAudioSegment(nb, language, wantedQuality) {

// The url begins to be a little more complex here:

const url = “http://my-server/audio/” +

language + “/” + wantedQuality + “/segment” + nb + “.mp4”);

return fetch(url)

.then((response) => response.arrayBuffer());

.then(function(arrayBuffer) {

audioSourceBuffer.appendBuffer(arrayBuffer);

});

}

const bandwidth = estimateBandwidth();

const quality = fromBandwidthToQuality(bandwidth);

const language = getUsersLanguage();

pushAudioSegment(0, language, quality)

.then(() => pushAudioSegment(1, language, quality))

.then(() => pushAudioSegment(2, language, quality));

如您所见,现在有很多方法可以定义相同的内容。

这揭示了分开的视频和音频段相对于整个文件的另一个优点。对于后者,我们将不得不在服务器端结合各种可能性,这可能会占用更多空间:

segment0_video_240p_audio_esperanto_128kbps.mp4

segment0_video_240p_audio_esperanto_320kbps.mp4

segment0_video_240p_audio_french_128kbps.mp4

segment0_video_240p_audio_french_320kbps.mp4

segment0_video_720p_audio_esperanto_128kbps.mp4

segment0_video_720p_audio_esperanto_320kbps.mp4

segment0_video_720p_audio_french_128kbps.mp4

segment0_video_720p_audio_french_320kbps.mp4

segment1_video_240p_audio_esperanto_128kbps.mp4

segment1_video_240p_audio_esperanto_320kbps.mp4

segment1_video_240p_audio_french_128kbps.mp4

segment1_video_240p_audio_french_320kbps.mp4

segment1_video_720p_audio_esperanto_128kbps.mp4

segment1_video_720p_audio_esperanto_320kbps.mp4

segment1_video_720p_audio_french_128kbps.mp4

segment1_video_720p_audio_french_320kbps.mp4

segment2_video_240p_audio_esperanto_128kbps.mp4

segment2_video_240p_audio_esperanto_320kbps.mp4

segment2_video_240p_audio_french_128kbps.mp4

segment2_video_240p_audio_french_320kbps.mp4

segment2_video_720p_audio_esperanto_128kbps.mp4

segment2_video_720p_audio_esperanto_320kbps.mp4

segment2_video_720p_audio_french_128kbps.mp4

segment2_video_720p_audio_french_320kbps.mp4

Here we have more files, with a lot of redundancy (the

这里我们有更多的文件,并且有很多冗余(多个文件中包含完全相同的视频数据)。

如您所见,在服务器端效率很低。但这在客户端也很不利,因为切换音频语言可能会导致您也重新下载视频(带宽成本很高)。

直播

我们还没有谈论直播。

网络上的直播流媒体(twitch.tv,YouTube实时流媒体…)变得非常普遍,并且由于我们的视频和音频文件已分段,因此再次大大简化了这一过程。

为了说明它基本上以最简单的方式工作,让我们考虑一个4秒钟前才开始直播传输的 YouTube 频道。

如果我们的片段长2秒,那么我们应该已经在YouTube的服务器上生成了两个音频片段和两个视频片段:

  • 两个代表从0秒到2秒的内容(1个音频+ 1个视频)

  • 两个代表2秒到4秒(同样是1个音频+ 1个视频)

./audio/

├──segment0s.mp4

└── segment2s.mp4

./video/

├──segment0s.mp4

└── segment2s.mp4

在5秒钟时,我们还没有时间生成下一个片段,因此,到目前为止,服务器具有完全相同的可用内容。

6秒钟后,可以生成一个新的段,我们现在有:

./audio/

├──segment0s.mp4

├──segment2s.mp4

└── segment4s.mp4

./video/

├──segment0s.mp4

├──segment2s.mp4

└── segment4s.mp4

在服务器端,这是很合乎逻辑的,实时内容实际上并不是真正连续的,它们像非实时内容一样进行分段,但是随着时间的流逝,分段会逐渐出现。

现在,我们如何从JS中知道服务器上某个时间点可用的段?

我们可能只在客户端上使用一个时钟,然后随着时间的流逝推断出新的段在服务器端变得可用。

我们将遵循“ segmentX.mp4 ”的命名方案,并且每次都将从上次下载的“ X”开始递增(“ segment0.mp4”,然后是2秒后的“ Segment1.mp4”等)。

但是,在许多情况下,这可能变得太不精确:媒体段的持续时间可能可变,服务器在生成媒体段时可能会有延迟,它可能希望删除太旧以至于无法节省空间的段…

作为客户端,您想请求最新的分片,只要它们可用,同时仍避免在尚未生成细分市场时过早请求它们(这将导致404 HTTP错误)。

通常通过使用传输协议(有时也称为流媒体协议)解决此问题。

传输协议

对于本文,深入解释不同的传输协议可能太冗长。我们只说其中大多数具有相同的核心概念:Manifest。

Manifest是描述哪些段可用的服务器上的文件。

借助它,您可以Manifest中了解到的大多数信息:

  • 内容在服务器上可用的语言以及在服务器上的可用位置(例如,“在哪个URL”)

  • 提供不同的音频和视频质量

  • 当然,在直播流媒体的情况下,哪些细分可用

Web中使用的最常见的传输协议是:

DASH

YouTube,Netflix或 Amazon Prime Video(及许多其他公司)使用的 DASH。DASH 的清单称为“Media Presentation Description”(或MPD),是其基本XML。

DASH规范具有极大的灵活性,它允许MPD支持大多数用例(音频描述,父控制)并且与编解码器无关。

HLS

由Apple开发,并由DailyMotion,Twitch.tv和许多其他公司使用。HLS清单称为播放列表,格式为m3u8(它们是m3u播放列表文件,以UTF-8编码)。

Smooth Streaming

由Microsoft开发,被多个Microsoft产品和MyCanal使用。在“平滑流传输”中,清单称为……Manifests,并且基于XML。

当前 Web 播放现状

如您所见,网络视频背后的核心概念在于在 JavaScript 中动态添加的媒体分片。

这种行为很快变得非常复杂,因为视频播放器必须支持许多功能:

  • 它必须下载并解析某种清单文件

  • 它必须猜测当前的网络状况

  • 它需要注册用户首选项(例如,首选语言)

  • 它必须至少根据前两个要点知道要下载哪个段

  • 它必须管理一个段管道以在正确的时间顺序下载正确的段(同时下载每个段的效率很低:您需要最早的一个比下一个要早)

  • 它也必须处理字幕,通常完全由 JS 管理

  • 一些视频播放器还管理缩略图轨道,将鼠标悬停在进度条上时通常可以看到

  • 许多服务也需要 DRM 管理

还有很多其他事情。复杂的,与Web兼容的视频播放器的核心仍然都是基于 MediaSource 和 SourceBuffers。

这就是为什么这些任务通常由第三方库执行的原因。

通常,这些库甚至都没有定义用户界面。它们主要提供丰富的API,以清单和各种首选项作为参数,并在正确的时间在正确的源缓冲区中添加正确的缓冲区。

当设计媒体网站和 Web 应用程序时,这将实现更大的模块化和灵活性,而本质上讲,它们将是复杂的前端。

开源的播放器

今天有许多网络视频播放器可以完成本文所解释的工作。以下是各种开源示例:

  • rx-player:可配置的DASH和 Smooth Streaming 播放器。用 TypeScript 编写—我是开发人员之一。

  • dash.js:播放DASH内容,支持多种DASH功能。由DASH行业论坛(DASH Industry Forum)撰写,旨在促DASH传输协议的互操作性指南。

  • hls.js:久负盛名的 HLS播放器。在生产中由多个知名品牌使用,例如Dailymotion,Canal +,Adult Swim,Twitter,VK等。

  • shaka-player:DASH和HLS播放器。由Google维护。

相关文章


  1. GitHub中文排行榜,帮助你发现高分优秀中文项目

  2. 9 种提高国内访问 GitHub 速度的方案

  3. 天秀!GitHub 硬核项目:动漫生成器让照片秒变手绘日漫风!!!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

后记


总结一下这三次面试下来我的经验是:

  1. 一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!

  2. 代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!

  3. 尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!

  4. 多刷面经!

我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】获取。祝大家早日拿到自己心怡的工作!

篇幅有限,仅展示部分内容



涵盖了95%以上前端开发知识点,真正体系化!**

[外链图片转存中…(img-Dm8Ty0ZE-1712985452840)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

后记


总结一下这三次面试下来我的经验是:

  1. 一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!

  2. 代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!

  3. 尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!

  4. 多刷面经!

我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】获取。祝大家早日拿到自己心怡的工作!

篇幅有限,仅展示部分内容



  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值