前端实时播放摄像头RTSP流(H.265)解决方案

5,088 阅读8分钟
  • 需求:动态识别摄像头RTSP流编码格式(H.264/H.265)并实时播放
  • 方案:String.fromCharCode动态识别编码格式+借助Chrome HEVC(H.265) 硬解播放

1.背景介绍

一些机器视觉场景中常常需要在浏览器中支持实时播放摄像头监控视频,目前常用的摄像头例如海康大华等一般支持以下视频编码格式:H.264、H.265、...,特别是H.265,提供了更高的压缩效率,可以在相同的视频质量下提供更小的文件大小,是目前的主流选择。但H.265的硬件支持和浏览器兼容性不如H.264,需要前端自行开发实现解码,例如以前的软解等方式(该方案不在本文讲述)。

好在Chrome 从 107 版本开始正式支持H.265硬解,感兴趣可以看看这篇专访:【专访】 Chrome HEVC 硬解背后的字节开源贡献者)。但H.265硬解码对软硬件有一定要求:点击快速了解Chrome浏览器支持硬解的要求

Chrome 107 以上支持HEVC编解码的API: image.png

本文主要介绍借助String.fromCharCode动态识别编码格式+MSE硬解H.265视频流的方案。

2.相关概念介绍

2.1 视频编解码

下图为当前场景下视频的编解码过程: 视频编解码.png

原视频 → 可播放的视频:原视频文件通过编码来压缩文件大小(视频编码),再将压缩视音频、字幕封装到一个容器内(视频封装),即为通俗意义的视频文件。

2.2 Media Source Extensions API(MSE)

原本播放视频和音频架构过于简单(video和audio),只能满足一次播放整个曲目的需要,无法实现拆分/合并数个缓冲文件。流媒体只能使用 Flash 进行服务,以及通过 RTMP 协议进行视频串流的 Flash 媒体服务器。MSE 使我们可以把通常的单个媒体文件的 src 值替换成引用 MediaSource 对象(一个包含即将播放的媒体文件的准备状态等信息的容器),以及引用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。使媒体串流能够通过 JavaScript 创建,并能通过使用 和 元素进行播放。通俗理解就是video本不支持流式播放,但可经过MSE分段接受流媒体数据转换后,再给video播放。

video目前支持的常见视频封装格式:mp4(H.264)、webm、ogg等,这里主要考虑mp4。但传统MP4是一个单一的、连续的文件,不适合于流媒体传输(流媒体数据+传输协议)应用。

与之对应的FMP4(Fragmented MPEG-4), 是基于 MPEG-4 Part 12 的流媒体格式,其优点在于使用 DASH 或 HLS 进行流传输时,播放器仅需要下载观众想要看的片段,且能通过分段来实现自适应比特率流和低延迟播放。

下图是它们之间的关系描述:普通的 MP4 文件不能和 MSE 一起使用,需要将 MP4 进行 fragment 化。 MP4封装格式.png

本文就是借助FMP4+MSE实现H.265流播放

2.3 MP4和FMP4(Fragment MP4)

传统的 MP4 是嵌套结构的,客户端需要从头加载一个 MP4 文件,才能够完整播放,不能从中间一段开始播放。

FMP4 ,基于 MPEG-4 Part 12 的流媒体格式,由一系列的片段(fragment)组成,这些片段可以独立的进行请求到客户端进行播放,而不需要加载整个文件。

MP4是由一个个box组成的,大box中存放小box,一级嵌套一级来存放媒体信息,一个MP4文件有可能包含非常多的box。

下面是 简化的常见MP4文件结构 和 重要box存放信息的说明 :*图片来自网络

image.png image.png

下面是两者通过 mp4parser(Online MPEG4 Parser )分析后的截图:左图是FMP4,右图为MP4

image.png 从上图可以看到 mp4的最顶层 box 类型非常少,而 fmp4 增加了一个moof box来描述视频分片信息,文件分为了多个Fragments,每个Fragment中包含moofmdat,它们已经包含了足够的 metadata 信息与数据,可以直接请求到这个位置开始播放。

2.4 FMP4打包H.264和H.265 的区别

image.png

如上图:fmp4封装H265,主要是stsd box的区别,H.264的stsd-avc1 box说明H.264视频封装格式为avc1,H.265需要把stsd box->avc1 box替换成hvc1 box(hvc1 box的封装格式与avc1类似,只是把avcC子box替换程hvcC)。此部分的差异,可以用作后续前端动态识别H.264和H.265的标识。

具体打包格式详解可以参考:fmp4打包H264详解fmp4打包H265视频流

3.方案设计和实现

目标:前端页面播放器控件实现动态识别摄像头输出的H.264/H.265并实时播放。

大体流程:H.264/H.265 → FMP4 → WebSocket → MediaSource+Video,技术方案如下图:

h265方案-硬解方案.png

主要介绍标红框的两个部分。其中FMP4的打包输出是由后端服务videoServer完成的,以及WS推流均不在这里展开介绍。

3.1 前端判断视频格式,以自适应h.264和h.265播放

这里涉及上述 FMP4打包H.264和H.265 的区别 部分:

ws → ArrayBuffer → Uint8Array → String.fromCharCode

String.fromCharCode(num1,num2,...)  静态方法返回由指定的 UTF-16 码元序列创建的字符串。

        const AVCCodecs = 'video/mp4; codecs="avc1.4d0029"' // h.264
        const HEVCodecs = 'video/mp4; codecs="hev1.1.6.L120.90"' // h.265 
        ... 
        private onWebSocketMessage = (data: ArrayBuffer) => { 
            if (!this.isCreateMSE) { 
                // step1:识别视频格式 
                const unit = new Uint8Array(data) 
                let unitCharCode = '' 
                unit.forEach((u) => { 
                    unitCharCode += String.fromCharCode(u) 
                }) 
                if (unitCharCode.indexOf('moov') > -1) { 
                    if (unitCharCode.indexOf('hev') > -1) { 
                        // 查找265的编码信息 
                        this.videoMimeCodec = HEVCodecs 
                    } else { 
                        this.videoMimeCodec = AVCCodecs 
                    } 
                    // step2:创建MSE 
                    this.createMediaSource() 
                    this.isCreateMSE= true 
                } 
            } 
            ... 
        }

unitCharCode对比:

image.png

3.2 创建MediaSource进行解码和流式播放

3.2.1 MSE API介绍:点击查看API详情

image.png

其中有两个比较关键的API:MediaSource.isTypeSupported(mimeType)  、MediaSource.addSourceBuffer(mimeType)

   const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; 
   ... 
   if ("MediaSource" in window && MediaSource.isTypeSupported(mimeCodec)) { 
       ... 
   } 
   ... 
   const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); 
   ...

这里涉及到了媒体类型(MIME),简单介绍下:

3.2.1.1 媒体类型(mimeType='video/mp4; codecs="avc1.42E01E, mp4a.40.2" ')

由两部分组成,分号前是MIME 类型,分号后codecs参数是MIME 类型的可选参数。格式:type/subtype;parameter=value

MIME 类型:是指示文件类型的会与文件同时发送出去的字符串,描述了内容的格式(例如,一个声音文件可能被标记为 audio/ogg ,一个图像文件可能是 image/png)。点击查看MIME 类型列表

image.png

但仅用MIME类型例如video/mp4描述MPEG-4文件中的视频并不能说明其中的实际媒体采用什么格式,因此常常将codecs参数添加到描述媒体内容的MIME类型中。

codecs:可以提供容器特定信息,可能包括视频编解码器的配置文件、用于音轨的类型等。

image.png

avc1.xxxx 即告诉解码器这是个H.264编码的媒体资源。点击查看Video相关mimeType列表

3.2.1.2 H.265 支持的Profile (codecs)介绍

  1. Main Profile (hev1.1.6.L93.B0) :

    • 这是HEVC编码的基本配置文件,适用于广泛的应用场景,包括视频流媒体和视频下载。
    • 它支持8位色深的视频内容。
  2. Main 10 Profile (hev1.2.4.L93.B0) :

    • 这个配置文件扩展了Main Profile,支持高达10位色深的视频内容。
    • 主要用于需要更高色彩精度的应用,如4K视频和专业视频编辑。
  3. Main still-picture Profile (hvc1.3.E.L93.B0) :

    • 这个配置文件专门用于静态图像的编码,例如照片。
    • 它可能在一些特定的图像处理或存储应用中使用。
  4. Range extensions Profile (hvc1.4.10.L93.B0) :

    • 这个配置文件提供了额外的编码功能,如支持更广泛的色度采样格式(例如4:2:2和4:4:4)。
    • 它可能用于需要更高精度色度采样的视频应用。

     这些配置文件的命名约定如下:

  • hev1 表示这是一个HEVC编码的配置文件。
  • 第一个数字(如 1 或 2)表示主要配置文件的版本。
  • 第二个数字(如 1 或 4)表示配置文件的类型(Main、Main 10、Main still-picture、Range extensions)。
  • .E 或 .10 表示色深(8位或10位)。
  • L93 表示最大亮度级别,这里的 93 表示最大亮度为93尼特。
  • .B0 表示配置文件的兼容性级别。

这些配置文件的选择取决于视频内容的需求、目标平台的兼容性以及所需的视频质量。

例如,如果你需要编码4K视频,可能会选择Main 10 Profile以获得更高的色彩精度。如果你需要编码静态图像,可能会选择Main still-picture Profile。

3.2.2 关键代码示意

Chrome 107版本正式宣布支持HEVC(H.265) 硬解,我们可以借助isTypeSupported(mimeType)判断当前浏览器是否支持H.265,并通过addSourceBuffer(mimeType)实现硬解。

    /** 
      * 初始化MediaSource, 并与video绑定 
      */ 
    private createMediaSource = () => { 
        if ('MediaSource' in window && MediaSource.isTypeSupported(this.videoMimeCodec)) { 
            if (this.options.onMimeCodecCheck) { 
                this.options.onMimeCodecCheck(true) 
            } 
        
            // 新建MediaSource对象 
            this.mediaSource = new MediaSource() 
            // 给video.src赋值之后触发 
            this.mediaSource.onsourceopen = this.onMediaSourceOpen 
            this.mediaSource.onsourceclose = this.onMediaSourceClose 
        
            if (this.videoElem) { 
                // 创建URL对象, 当不再需要时,需调用 URL.revokeObjectURL(objectURL)来释放 
                this.videoElem.src = URL.createObjectURL(this.mediaSource) 
            } 
        } else { 
            console.error('Unsupported MIME type or codec') 
        } 
    } 
        
    /** 
      * 给video.src赋值之后触发 
      * 创建SourceBuffer实例,用于操作视频流 
      * 初始化WebSocket实例 
      */ 
    private onMediaSourceOpen = () => { 
        if (this.videoElem) { 
            // 释放通过URL.createObjectURL()创建的对象URL 
            URL.revokeObjectURL(this.videoElem.src) 
            if (!this.sourceBuffer && this.mediaSource) { 
                // 创建 SourceBuffer 对象 
                this.sourceBuffer = this.mediaSource.addSourceBuffer(this.videoMimeCodec)
                // appendBuffer 或 remove结束时触发 
                this.sourceBuffer.onupdateend = this.onSourceBufferUpdateend 
                this.sourceBuffer.onerror = () => { 
                    console.error('xxxx')
                } 
            } 
        } 
    }