web 端视频播放开发总结

1,743 阅读10分钟

背景:业务需求需要做海康摄像头的web端视频播放,差不多也告一段落了...

从rtsp讲起

RTSP(Real Time Streaming Protocol)是一种网络控制协议,用于在互联网上控制音视频数据的实时数据流。

目前的摄像头设备导出可用的视频播放协议基本都是rtsp协议的,这种协议是基于TCP/UDP之上的一种流媒体传输协议,但很不幸的是,浏览器并不能直接播放这种协议的视频。

为什么呢?因为浏览器支持的与服务器传递数据的格式大致为http、web Socket、web RTC,而rtsp协议不属于上述,而且浏览器也不提供套接字的API,因此,想要实现直接在web端播放rtsp协议的视频是不可能的。

套接字(Socket)是一种通信端点的抽象表示,它为主机之间进程的通信提供了一种机制。

可以简单理解提供直接与服务器建立TCP/UDP的连接的能力,连接之上发送的内容就可以由开发者自己定义了

那要怎么做呢?这时候就需要后端对rtsp的视频进行转流播放,也就是转成浏览器可以支持的视频播放协议进行播放

从视频的制作过程讲起

下面的讲述并不严谨,只是一个大致过程 假使我们需要制作一段视频并发送给另外的设备进行播放...

第一步,我们需要一个音频采集设备和画面采集设备采集到原始的音频信息和视频信息,但原始的音频信息和画面信息通常是非常大的,不利于数据的传输和处理,因此通常需要对其进行压缩

第二步,我们会使用相应的编码技术对画面信息和音频信息分别进行压缩,例如,使用h264、h265对画面信息进行压缩,使用AAC对音频信息进行压缩

第三步,要使得音频和画面可以同步地被播放,我们需要将音频和画面以约定的封装格式进行组合,例如,封装成mp4、flv、ts等

至此,就完成了一个视频的制作,大致流程就是采集-->编码-->封装,这个过程中,编码需要的计算量是远大于封装的

后端的转流工作

github.com/ZLMediaKit/…

前面讲到,浏览器是不能直接播放rtsp的视频格式的,因此需要转成浏览器可以支持的播放格式的,我这边主要接触到的有mp4、flv、hls

mp4,可以有.mp4用于回放,.live.mp4进行直播播放,好处是浏览器可以直接支持播放,不需要额外再寻找播放器,直接video标签填上src就可以使用了,坏处是失去了很多对播放过程的控制和信息的获取

flv(flash video),旧时代的残党,flv本身跟mp4一样只是一个封装格式,并不是一个流媒体传输协议,因此还需要基于http或者web socket进行传输,也就是http-flv和ws-flv这两种格式,坏处是仅能支持直播,且使用flv的人越来越少

hls(HTTP Live Streaming),苹果公司开发,将视频分隔成若干的ts视频片段进行传输,并提供一个m3u8的索引文件表示视频的播放位置,在苹果系的设备上可以直接用video标签播放,非苹果系的设备上则需要自行处理,直播跟回放都可以支持

后端只需要选取其中一种做为播放协议返回给前端即可,转流相当于将以rtsp协议封装好的媒体内容解封装,再按照流媒体协议需要的格式重新封装,这部分的计算量并不大

从视频的播放过程讲起

视频制作的大致流程就是采集-->编码-->封装,而视频播放的大致流程就是解封装-->解码-->硬件播放。当然,我们不会细致地接触到每个流程,浏览器会提供给我们一些API用于视频播放

video标签:最基本的视频播放能力,只要浏览器支持(通常情况下只有mp4),都可以直接将播放链接填入src上,就可以播放了

MediaSource:对于浏览器不支持的视频封装格式,我们可以使用MediaSource API,将视频数据重新封装成浏览器支持的格式,再喂给video标签实现播放,这个过程就是大部分前端播放器的工作原理,“将 MPEG2-TS 流转换为 ISO BMFF(分段 MP4)段,然后通过媒体源扩展 API 将 mp4 段馈送到 HTML5 元素中。”。

如果没有太大的问题,这个过程就跑通了,视频也就播放成功了,这个播放会非常丝滑🎉

MediaSource的处理

以下是一个大致的处理过程,前端播放器的主要工作就是将传输的媒体数据解封装成音频数据和画面数据,这个过程还需要获取到对应的编码格式,再将对应的buffer数据喂给MediaSource,所以整个过程也是在进行解封装和再封装

const mse = new  MediaSource()

// 这部分需要在 mse 的 sourceopen 事件触发后开始执行
const videoBuffer = mse.addSourceBuffer('video/mp4;codecs=hev1.1.6.L93.B0')
const audioBuffer = mse.addSourceBuffer('audio/xxx') // 指定一个音频的编码

// 获取到流媒体协议上传输的视频数据和音频数据
videoBuffer.appendBuffer(buffer)
audioBuffer.appendBuffer(buffer)

从h265的编码讲起

你可以通过MediaSource.isType.isTypeSupported('video/mp4;codecs=hev1.1.6.L93.B0') 来判断浏览器是否支持对应的编码

上述的视频播放过程被称为硬解码播放,程序的处理不涉及解码,解码的操作是交给浏览器去进行的。但浏览器做为一个播放器时,它并不能支持所有的编码格式,而遇到不支持的编码格式时,你可能就会遇到如下错误,从而无法播放

The type provided ('video/mp4;codecs=hev1.1.6.L93.B0') is unsupported.

h265(hevc)编码是一种视频编码格式,比现在常用的h264编码有着更高的压缩率,但发布的较晚,还没有被所有的设备支持,而我们的部分视频用的就是265编码,就有可能会因为编码不支持而出现视频无法播放的情况

不过h265的编码支持情况是越来越好了~

h265编码的支持情况

windows的情况下

  • chrome(>107)开启图形加速后支持
  • edge 开启硬件加速
    • win11(>22h2)支持,系统应用里就默认安装了对应的插件
    • win10 (> 1709),安装特定插件(需要付费)后可支持

以下是可以参考的资料

使用软解码播放

那如果对应的编码格式浏览器不支持该怎么办呢?对于前端而言最好的办法是后端进行转码,这样浏览器就可以支持播放了,但这样对于服务器的压力是比较大的,因为转码的计算量是远大于转流的,而对于后端而言最好的办法就是前端可以兼容播放,也就是软解码播放

软解码播放是相对于硬解码播放而言的,我们的程序现在不仅要解封装,还要进行解码。对于音频的软解码不甚了解,这里仅以画面的软解码播放为例

对于视频数据的处理,就需要使用到著名软件ffmpeg,在浏览器上,我们会使用ffmpeg.wasm来调用,处理步骤大致如下

  1. 获取服务器传输过来的媒体数据,解封装为画面数据和音频数据,音频数据直接使用AudioContextAPI进行播放
  2. 使用ffmpeg.wasm对画面数据进行解码,获取原始的rgb或yuv格式的数据,
  3. 创建一个canvas画布,将原始的rgb或yuv格式的数据不断绘制在上面,形成播放的效果

这样视频还是可以播放的,但问题就是这样的效率极低,尤其是在画面分辨率较大的情况下,播放过程中的延迟是很大的,实际的播放效果并不理想,而且出问题之后也很难排查,但产品嘛,总是想要求这么做的

因为软解码播放的速率跟不上视频的传输速率,可以预见的是,随着时间的推移,累计待播放的视频数据会越来越大,网页占用的内层也就越来越大...

第三方播放器推荐

站在前人的肩膀上,很多功能并不需要我们手动开发

还可能遇到的问题

Q:http-flv进行播放时,无法同时播放六路视频

A: http1.1的特点是存在队头阻塞问题,必须按顺序发送/接收请求,必须在前一个请求回来之后,第二个请求才会发送,而http-flv属于长连接的http协议,会一直占用http的请求通道,如果同时播放多个视频,就会有多个请求一直占用,不会结束 因此当http1.1+flv进行视频播放的时候,每播放一路就会占用一个连接,而浏览器对于同源的TCP连接最大数就是6个,因此会有6个的限制

知乎参考

Q:这个视频在VLC上可以正常播放,在我们的播放器上有花屏等等的问题

A:硬解码播放器只做了解封装和再封装的工作,不涉及编码,也就不会操作到媒体数据,这些问题就是浏览器与对应编码的媒体数据的兼容问题,前端程序其实是做不了什么的

加一个视频录制的功能

使用浏览器提供的API,录制视频本身其实很简单

// $0 可以是video元素也可以是canvas元素
const stream=$0.captureStream()
const recorder = new MediaRecorder(stream,{
    mimeType: 'video/webm'
    // 这个值设置的太小会导致录出来的视频卡顿,太大则会录制失败
    videoBitsPerSecond: 6 * 1000 * 1000,
})
recorder.addEventListener('dataavailable',(e)=>{
	// e.data 就是blob数据,保存就好了
	const blob = new Blob([e.data], { type: mediaRecorder.mimeType })
})
recorder.start()
setTimeout(()=>{
    recorder.stop()
},5000)
  • 录制视频的视频如果video的muted属性是为true,那么是无法正常录制的
  • MediaRecorder的mimeType一般设置成video/webm就可以了,或许你想要设置成video/mp4,但那样是会录制失败的,事实上,实际录制的是mkv的格式,你可以通过mediaRecorder.mimeType属性查看
    • 不过产品想要录制的一般是mp4,可以在保存文件的时候强行把后缀改成mp4,毕竟一般的播放软件是能识别出来的
  • 浏览器直接录制出来的视频是没有播放进度条的,也就是只能以直播的形式观看,而不是回放,需要手动修复它