本文来源公众号:程序员成功本文记录了使用flv.js播放时踩过的各种坑监控录像。虽然官网给出的GettingStarted只有几行代码,跑一个能播放视频的demo轻而易举,但各种异常会让你怀疑人生。究其原因,一方面,GitHub上的文档比较晦涩,说明也比较简单;另一方面,受“视频播放”思维的影响,对流的理解不够,处理流的经验也不足。下面我将详细总结一下自己踩过的坑以及在踩坑过程中补充的相关知识。大纲预览本文介绍的内容包括以下几个方面:直播点播静态数据和流数据为什么选择flv?协议和基本实现细节处理要点风格定制点播和直播什么是直播?什么是按需?直播就不用说了,随着抖音的火爆,大家都知道直播是干嘛的了。点播其实就是视频播放,和在哔哩哔哩看视频完全一样。就是发布预制视频,这叫点播。对于我们的前端来说,视频点播就是取一个mp4链接地址,放到video标签里面。浏览器会帮我们处理视频分析、播放等一系列事情。我们可以拖动进度条来选择任何时间观看。但是直播就不一样了。直播有两个特点:流式数据的获取需要实时性。我们先来看看什么是流数据。大部分没做过音视频的前端同学,我们经常接触到的数据都是ajax从接口获取的json数据,尤其是文件上传。这些数据的特点是都是一次可以得到的数据。一请求一响应,我们就得到了完整的数据。但是流是不同的。流数据是逐帧获取的,可以理解为小块。像直播数据一样,它不是一个完整的视频片段,它是非常小的二进制数据,需要一点一点拼接起来才能输出视频。看看它的实时性能。如果是点播,我们直接把完整的视频存到服务器上,然后返回链接,前端可以用视频或者播放器播放。但是,直播的实时性决定了数据源不可能在服务端,而是在某个客户端。数据源在客户端,如何到达其他客户端?对于这个问题,请看下面的流程图:如图所示,发起直播的客户端向上连接流媒体服务器,直播产生的视频流会实时推送到服务器时间。这个过程称为流式传输。其他客户端也连接到这个推流服务器,不同的是他们是播放端,会实时拉取直播客户端的视频流,这个过程叫做推流。推流->服务器->拉流,这是目前流行的标准直播方案。你看,直播整个过程都是流式数据传输,数据处理直接二进制,比点播复杂好几个数量级。具体来说,我们业务中对摄像头的实时监控和预览其实和上面完全一样,只是发起直播的客户端是摄像头,观看直播的客户端是浏览器。静态数据和流式数据我们经常接触到的文本、json、图片等都是静态数据。前端使用ajax向接口请求的数据是静态数据。前面说了,直播产生的视频和音频属于流数据。流式数据是一帧一帧的,其本质是二进制数据。因为体积小,数据像流水一样源源不断,非常适合实时传输。对于静态数据,在前端代码中都有相应的数据类型,如string、json、array等。那么流数据(二进制数据)的数据类型是什么?它在前端是如何存储的?如何操作?首先明确前端可以存储和操作二进制。最基本的二进制对象是ArrayBuffer,它代表一个固定的长度,比如:letbuffer=newArrayBuffer(16)//创建一个16字节的缓冲区,并填充0alert(buffer.byteLength)//16ArrayBuffer只被使用对于存储二进制数据,如果要操作,就需要用到视图对象。View对象不存储任何数据,它们的作用是将ArrayBuffer的数据结构化,以便我们操作这些数据。说白了就是操作二进制数据的接口。视图对象包括:Uint8Array:每个项目1个字节Uint16Array:每个项目2个字节Uint32Array:每个项目4个字节Float64Array:每个项目8个字节根据上述标准,一个16字节的ArrayBuffer,可转换的视图对象及其长度为:Uint8Array:length16Uint16Array:length8Uint32Array:length4Float64Array:length2这里只是简单介绍一下流数据在前端是如何存储的,以免大家在浏览器中看到一个长长的ArrayBuffer不知道是什么,记住了必须是二进制数据。为什么选择flv?前面说了,直播需要实时性,当然延迟越短越好。当然,决定传输速度的因素有很多,其中之一就是视频数据本身的大小。我们最常用的点播场景的mp4格式,对前端的兼容性是最好的。但是相对来说,mp4的体积比较大,解析起来会比较复杂。这就是mp4在直播场景下的劣势。flv是不同的。它的头文件很小,结构简单,易于解析。在直播的实时性要求下非常有优势,因此成为最常用的直播解决方案之一。当然除了flv还有其他格式,对应直播协议,我们一一比较:RTMP:底层基于TCP,浏览器端依赖Flash。HTTP-FLV:基于HTTP流IO传输FLV,依赖浏览器支持播放FLV。WebSocket-FLV:基于WebSocket传输FLV,依赖浏览器支持播放FLV。HLS:HttpLiveStreaming,苹果公司提出的一种基于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也不是万能的。它的缺点是前端video标签不能直接播放,需要处理。解决方案就是我们今天的主角:flv.js协议和基本实现。前面我们说过,flv同时支持WebSocket和HTTP两种传输方式。幸运的是,flv.js也支持这两种协议。选择使用http还是ws,其实功能和性能上没有太大区别,关键看后端同学给我们什么协议。我这里选择的是http,前后端处理比较方便。下面介绍一下flv.js的具体接入过程。官网假设有直播地址:http://test.stream.com/fetch-media.flv。第一步根据官网快速开始搭建。演示:从'flv.js'导入flvjsif(flvjs.isSupported()){varvideoEl=document.getElementById('videoEl')varflvPlayer=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元素,然后在video上播放视频流。下一个关键点是创建flvjs.Player对象,我们称之为播放器实例。播放器实例是通过flvjs.createPlayer函数创建的。参数为配置对象,常用如下:type:媒体类型,flv或mp4,默认flvisLive:可选,是否为直播流,默认truehasAudio:是否有音频hasVideo:是否有视频url:指定流地址,可以是https(s)或ws(s)上是否有音频或视频配置,还是要看流地址是有音频还是视频。比如监控流只有视频流,没有音频,即使配置了hasAudio:true,也不可能有声音。播放器实例创建完成后,接下来就是三步:挂载元素:flvPlayer.attachMediaElement(videoEl)加载流:flvPlayer.load()播放流:flvPlayer.play()基本的实现过程就这么多,下面说说下面的处理详细信息和处理要点。详细处理的要点上面已经提到了基本用法,下面讨论实践中的关键问题。暂停和播放点播中的暂停和播放非常简单,播放器下方会有一个播放/暂停按钮,你可以随时暂停,当你再次点击播放时,它会从上次暂停的地方继续播放时间。但是直播就不一样了。一般情况下,直播中不应该有播放/暂停按钮和进度条。因为我们看的是实时信息,如果暂停视频,再次点击播放时,将无法从暂停点继续播放。为什么?因为你是实时的,所以当你再次点击播放时,你应该会得到最新的直播并播放最新的视频。至于技术细节,前端video标签默认有进度条和暂停按钮。flv.js将直播流输出到视频标签。如果此时点击暂停按钮,视频也会停止,这与点播逻辑是一致的。.但是如果再次点击播放,视频会从暂停点继续播放,这是错误的。那我们换个角度,重新审视一下直播的播放/暂停逻辑。为什么直播需要暂停?以我们的视频监控为例,一个页面会显示几个摄像头的监控视频。如果每个玩家一直连接服务器,不停的拉流,这样会造成大量的连接和消耗,钱会全部打光。.那我们是不是可以这样:进入网页的时候,找到你要看的摄像头,点击播放,然后拉流。不想看的时候点暂停,播放器就会断开,这样会不会省掉无用的流量消耗?因此,直播中播放/暂停的核心逻辑是拉流/停止。明白了这一点,我们的解决方案应该是隐藏视频的暂停/播放按钮,然后自己实现播放和暂停的逻辑。仍然以上面的代码为例,播放器实例(上面的flvPlayer变量)不用改,播放/暂停代码如下:constonClick=isplay=>{//参数isplay表示是否currentlyplayingif(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.NETWORK_ERROR){console.log('网络错误')if(errdet==flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID){console.log('http状态代码异常')}}if(err==flvjs.ErrorTypes.OTHER_ERROR){console.log('Otherexceptions:',errdet)}}另外,自定义播放/暂停逻辑,还需要知道加载状态,可以监听完成视频流加载方式如下:player.on(flvjs.Events.METADATA_ARRIVED,()=>{console.log('Videoloadingcomplete')})为什么会有stylecustomizationStylecustomization?前面我们说过,直播的播放/暂停逻辑与点播不同,所以我们需要隐藏视频操作栏元素,通过自定义元素实现相关功能。首先需要隐藏播放/暂停键、进度条、音量键,可以用css实现:/*allcontrols*/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;}/*Playbutton*/video::-webkit-media-controls-play-button{display:none;}播放逻辑上面说了pausing,这里的style自定义一个button就可以了。另外,我们可能还需要一个全屏按钮,来看看全屏的逻辑怎么写:constfullPage=()=>{letdom=document.querySelector('.video')if(dom.requestFullscreen){dom.requestFullscreen()}elseif(dom.webkitRequestFullScreen){dom.webkitRequestFullScreen()}}其他自定义样式,比如你要做弹幕,可以自己实现,在上面覆盖一层元素视频。为了更好的保护原创,我想学习更多,下一篇我会发在微信公众号前端樵夫。本公众号仅原创,每周至少一篇优质文章,关注前端工程与架构、BFF层的前端边界探索、集成开发与交付等实践与思考。另外,我还建了一个微信群,为对这个方向感兴趣的同学提供交流和学习。群里有大厂的大老板,有掘金lv6的高手,也有更多想研究这个方向的同学。一起交流、分享、学习吧~如果你也有兴趣想了解更多,欢迎加我微信ruidoc拉你进群~
