这篇文章记录了你用flv.js播放监控视频时踩过的各种坑虽然官网给出的入门只有短短几行代码,轻松运行一个可以播放视频的demo,但是播放过程中的各种异常会让你怀疑人生。
原因是,一方面,GitHub上的文档晦涩难懂,解释简单;另一方面,由于“视频播放”思维的影响,对stream的认识不够,缺乏处理stream的经验。
下面我就详细总结一下我踩过的坑,以及我在踩坑过程中补充的相关知识。
大纲预览
本文介绍的内容包括以下几个方面:
直播与点播静态数据与流数据为什么选 flv?协议与基础实现细节处理要点样式定制
点播和直播
什么是直播?什么是点播?
不用说,直播在Tik Tok很受欢迎,每个人都知道直播是干什么的。其实点播就是视频播放,和看视频是一样的。通过发布事先制作好的视频,称之为点播。
对于我们前端来说,点播就是取一个mp4的链接地址,放在视频标签里。浏览器将帮助我们处理一些事情,如视频解析和播放。我们可以拖动进度条来选择任何我们想要观看的时间。
但是直播不一样。直播有两个特点:
获取的是流数据要求实时性
让我们来看看什么是流数据。大部分没做过音视频的前端同学,我们经常接触的数据都是ajax从接口获取的json数据,尤其是文件上传。这些数据的特点是都属于一次性数据。我们通过一个请求和一个响应获得完整的数据。
但是小溪不一样。流数据采集是一帧一帧的,你可以理解为一块一块的。和直播流的数据一样,它不是一个完整的视频片段,它只是一个很小的二进制数据,需要你一点一点拼接,才能输出一个视频。
看它的实时性能。如果是点播,我们会直接把完整的视频存储在服务器上,然后返回链接,前端用视频或者播放器播放。但是直播的实时性决定了数据源不在服务器上,而是在某个客户端上。
数据源在客户端,那么它如何到达其他客户端呢?
对于这个问题,请看下面的流程图:
如图,发起直播的客户端向上连接流媒体服务器,直播产生的视频流会实时推送到服务器。这个过程被称为推流式传输。其他客户端也连接到这个流媒体服务器,但不同的是,它们是播放终端,会实时拉直播客户端的视频流。这个过程称为流。
推流->:服务器->;Pull,是目前比较流行的标准直播解决方案。看到没,直播的整个过程都是流数据传输,数据处理面对的是二进制,比点播系统复杂几个数量级。
我们业务中的摄像头实时监控预览其实和上面一模一样,只不过发起直播的客户端是摄像头,观看直播的客户端是浏览器。
静态数据和流数据
文字,json,图片等。,我们经常接触的,都是静态数据。前端从ajax接口请求的数据是静态数据。
如上所述,直播产生的视频和音频属于流数据。数据流是一帧一帧的,本质是二进制数据。因为小,所以数据像水一样源源不断,非常适合实时传输。
静态数据,在前端代码中有对应的数据类型,比如string、json、array等等。那么流数据(二进制数据)的数据类型是什么呢?如何在前端存储?如何操作?
首先明确一点,前端是可以存储和操作二进制的。最基本的二进制对象是ArrayBuffer,它表示固定长度,例如:
let buffer = new ArrayBuffer(16) // 创建一个 16 字节 的 buffer,用 0 填充alert(buffer.byteLength) // 16
ArrayBuffer仅用于存储二进制数据。如果要操作,需要使用视图对象。
对象,不存储任何数据,用来结构化的处理ArrayBuffer的数据,这样我们就可以方便的操作这些数据。说白了就是操作二进制数据的接口。
这些视图包括:
Uint8Array
:每个 item 1 个字节
Uint16Array
:每个 item 2 个字节
Uint32Array
:每个 item 4 个字节
Float***Array
:每个 item 8 个字节
根据上述标准,一个16字节的ArrayBuffer,一个可转换的view对象,其长度为:
Uint8Array:长度 16Uint16Array:长度 8Uint32Array:长度 4Float***Array:长度 2
这里只是简单介绍一下流数据是如何存储在前端的,为了防止你在浏览器里看到一个很长的ArrayBuffer而不知道是什么。记住必须是二进制数据。
为什么是flv?
前面说过,直播需要实时,延迟越短越好。当然,决定传输速度的因素有很多,其中之一就是视频数据本身的大小。
点播场景我们最常见的mp4格式与前端的兼容性最好。但是mp4的体积比较大,分析起来会比较复杂。这是mp4在直播场景下的劣势。
Flv不一样。它的头文件很小,结构简单,解析也很简单。在直播的实时性要求下有很大的优势,因此成为最常用的直播方案之一。
当然除了flv还有其他格式。对应直播协议,我们来一一对比一下:
RTMP
: 底层基于 TCP,在浏览器端依赖 Flash。
HTTP-FLV
: 基于 HTTP 流式 IO 传输 FLV,依赖浏览器支持播放 FLV。
WebSocket-FLV
: 基于 WebSocket 传输 FLV,依赖浏览器支持播放 FLV。
HLS
: Http Live Streaming,苹果提出基于 HTTP 的流媒体传输协议。HTML5 可以直接打开播放。
RTP
: 基于 UDP,延迟 1 秒,浏览器不支持。
其实早期常用RTMP,兼容性也不错,但是依赖Flash。目前浏览器中Flash默认是禁用的,已经被时代淘汰了,所以不考虑。
HLS协议也很常见,对应的视频格式是m3u8。是苹果推出的,支持***很好,但致命缺点是延迟高(10~30秒),所以不考虑。
浏览器不支持RTP就不用说了,只剩下flv了。
但是flv分为HTTP-FLV和WebSocket-FLV。他们看起来像兄弟。有什么区别?
我们前面说过,直播是实时传输,连接创建后是不会断的,所以需要持续的推拉流媒体。在这种需要长连接的场景下我们想到的第一个解决方案自然是WebSocket,因为WebSocket是长连接实时传输的技术。
但是随着js原生能力的扩展,出现了比ajax更强的fetch这样的黑科技。它不仅支持对我们更友好的Promise,还能自然地处理流数据,性能不错。而且使用起来足够简单,对于我们这些开发者来说比较方便,于是就有了http版的flv方案。
综上所述,flv最适合浏览器直播,但flv不是万能的。它的缺点是前端视频标签不能直接播放,需要处理。
解决方案就是我们今天的主角:flv.js
协议和基本实现
我们之前说过,flv同时支持WebSocket和HTTP。幸运的是,flv.js也支持这两种协议。
选择http或ws。其实功能和性能差别不大。关键看后端同学给我们什么协议。我这里选择的是http,前后都方便处理。
接下来,我们来介绍一下flv.js官网的具体访问流程在这里。
现在假设有一个直播地址:
http://test.stream.com/fetch-media.flv,第一步根据官网的快速入门搭建一个试玩:
import flvjs from 'flv.js'if (flvjs.isSupported()) { var videoEl = document.getElementById('videoEl') var flvPlayer = flvjs.createPlayer({ type: 'flv', url: 'http://test.stream.com/fetch-media.flv' }) flvPlayer.attachMediaElement(videoEl) flvPlayer.load() flvPlayer.play()}
首先安装flv.js,第一行代码是检查浏览器是否支持flv.js,其实大部分浏览器都是支持的。下一步是获取视频标签的DOM元素。Flv会将处理后的flv流输出到视频元素,然后在video上播放视频流。
接下来,关键点是创建flvjs。玩家对象,我们称之为玩家实例。播放器实例由flvjs.createPlayer函数创建,参数为配置对象,常用如下:
type:媒体类型,flv 或 mp4,默认 flvisLive:可选,是否是直播流,默认 truehasAudio:是否有音频hasVideo:是否有视频url:指定流地址,可以是 https(s) or ws(s)
上面是否有音频或视频配置取决于流地址处是否有音频或视频。例如,如果监控流中只有视频流而没有音频,那么即使配置hasAudio: true也不可能有声音。
创建播放器实例后,接下来的三个步骤是:
挂载元素:flvPlayer.attachMediaElement(videoEl)加载流:flvPlayer.load()播放流:flvPlayer.play()
基本流程就说这么多,下面说说流程中的细节和关键点。
细节处理要点
基本上,演示正在运行,但如果您想进入生产环境,您仍然需要处理一些关键问题。
暂停并播放
很容易暂停并按需播放。播放器下方会有一个播放/暂停按钮。你可以随时暂停。当您再次点按“播放”时,您将从上次暂停的地方继续播放。但是直播上就不一样了。
正常情况下,直播应该没有播放/暂停按钮和进度条。因为我们看的是实时信息,你暂停视频,当你再次点击播放时,你无法从暂停的地方继续播放。为什么?因为你是实时的,当你再次点击播放的时候,你应该会得到最新的实时流,播放最新的视频。
具体到技术细节,前端视频标签默认有进度条和暂停按钮。flv.js将直播流输出到视频标签。这时,如果你点击暂停按钮,视频也会停止,这和点播逻辑是一致的。但是如果你再点击播放,视频会从暂停处继续播放,这是错误的。
所以让我们从另一个角度重新审视一下直播的播放/暂停逻辑。
为什么直播需要暂停?以我们的视频监控为例。一页上会有几个摄像头的监控视频。如果每个玩家都保持连接服务器,持续拉流,就会造成大量的连接和消耗,白花花的钱就全没了。
我们能不能这样做:当我们进入网页,找到我们想要观看的摄像机,点击播放,然后拉流?不想看的时候,点暂停,断开播放器。这样会节省无用的流量消耗吗?
所以直播中播放/暂停的核心逻辑是流/断。
此时,我们的解决方案应该是隐藏视频的暂停/播放按钮,然后自己实现播放和暂停的逻辑。
或者以上面的代码为例,播放器实例(上面的flvPlayer变量)不需要更改,播放/暂停代码如下:
const onClick = isplay => { // 参数 isplay 表示当前是否正在播放 if (isplay) { // 在播放,断流 player.unload() player.detachMediaElement() } else { // 已断流,重新拉流播放 player.attachMediaElement(videoEl.current) player.load() player.play() }}
异常处理
在用flv.js访问直播流的过程中,会出现各种各样的问题,有些是后端数据流的问题,有些是前端处理逻辑的问题。因为流是实时获取的,flv也是实时转换成输出的,所以一旦出现错误,浏览器控制台会循环连续打印异常。
如果用react和ts,全屏不正常,就没法再开发了。直播可能会有很多例外,所以错误处理非常重要。
官方异常处理的解释并不明显。让我简单总结一下:
首先,flv.js的异常分为两级,可以看作是一级异常和二级异常。
而且,flv.js还有一个特殊的特性,就是它的事件和错误都是用枚举来表示的,如下:
flvjs.Events:表示事件flvjs.ErrorTypes:表示一级异常flvjs.ErrorDetails:表示二级异常
下面描述的异常和事件基于上面的枚举,您可以将其理解为枚举下的一个键值。
一级异常有三种类型:
NETWORK_ERROR:网络错误,表示连接问题MEDIA_ERROR:媒体错误,格式或解码问题OTHER_ERROR:其他错误
通常使用三种类型的次级异常:
NETWORK_STATUS_CODE_INVALID:HTTP 状态码错误,说明 url 地址有误NETWORK_TIMEOUT:连接超时,网络或后台问题MEDIA_FORMAT_UNSUPPORTED:媒体格式不支持,一般是流数据不是 flv 的格式
知道了这一点,我们**播放器实例上的异常:
// **错误事件flvPlayer.on(flvjs.Events.ERROR, (err, errdet) => { // 参数 err 是一级异常,errdet 是二级异常 if (err == flvjs.ErrorTypes.MEDIA_ERROR) { console.log('媒体错误') if(errdet == flvjs.ErrorDetails.MEDIA_FORMAT_UNSUPPORTED) { console.log('媒体格式不支持') } } if (err == flvjs.ErrorTypes.NETWORK_ERROR) { console.log('网络错误') if(errdet == flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID) { console.log('http状态码异常') } } if(err == flvjs.ErrorTypes.OTHER_ERROR) { console.log('其他异常:', errdet) }}
此外,要定制播放/暂停逻辑,您还需要知道加载状态。您可以通过以下方法监控视频流加载的完成情况:
player.on(flvjs.Events.METADATA_ARRIVED, () => { console.log('视频加载完成')})
风格定制
为什么会有风格定制?前面说过,直播流的播放/暂停逻辑和点播不同,所以我们要隐藏视频的动作栏元素,通过自定义元素来实现相关功能。
首先,要隐藏播放/暂停按钮、进度条和音量按钮,只需使用css:
/* 所有控件 */video::-webkit-media-controls-enclosure { display: none;}/* 进度条 */video::-webkit-media-controls-timeline { display: none;}video::-webkit-media-controls-current-time-display { display: none;}/* 音量按钮 */video::-webkit-media-controls-mute-button { display: none;}video::-webkit-media-controls-toggle-closed-captions-button { display: none;}/* 音量的控制条 */video::-webkit-media-controls-volume-slider { display: none;}/* 播放按钮 */video::-webkit-media-controls-play-button { display: none;}
上面已经描述了播放暂停的逻辑。您可以在样式中自定义按钮。除此之外,我们可能还需要一个全屏按钮。我们来看看全屏逻辑怎么写:
const fullPage = () => { let dom = document.querySelector('.video') if (dom.requestFullscreen) { dom.requestFullscreen() } else if (dom.webkitRequestFullScreen) { dom.webkitRequestFullScreen() }}
其他的自定义样式,比如你想做一个弹幕,只要在视频上面覆盖一层元素就可以自己实现了。
本文来自笑醉生梦投稿,不代表舒华文档立场,如若转载,请注明出处:https://www.chinashuhua.cn/24/626125.html