首页 > 其他分享 >技术教程 —— 如何利用 SEI 实现音画同步?

技术教程 —— 如何利用 SEI 实现音画同步?

时间:2023-11-02 17:03:02浏览次数:26  
标签:音画 视频 教程 缓存 const player 进度 SEI


 摘要:利用 SEI 解决数据流录制回放过程中的音画不同步问题。

 

技术教程 —— 如何利用 SEI 实现音画同步?_缓存

技术教程 —— 如何利用 SEI 实现音画同步?_缓存_02

文|即构 Web SDK 开发团队

今年 6 月, ZEGO 即构科技推出了行业内首套数据流录制 PaaS 方案,打破传统录制服务传统,实现 100% 录制还原效果(点击查看方案介绍文章)。

在实现数据流录制回放的过程中,我们需要将音视频画面和白板画面组合成一个回放画面,模拟成播放器进行同步播放。在此过程中,有时会因为网络抖动等原因,导致录制的音视频出现卡顿,如果不及时进行处理,将会出现回放进度和录制过程、音视频画面和其他画面等不同步的现象。

那么,面对这种情况,我们该如何处理?

本篇文章我们将从 SEI 的基础概念出发,结合数据流录制回放的需求和应用场景,带大家了解一下 ZEGO 即构科技 是如何利用 SEI 去解决音画不同步的问题,以及开发过程中可能踩到的坑。

一、什么是 SEI

1、SEI 简介

SEI,即补充增强信息(Supplemental Enhancement Information),属于码流范畴,它提供了向视频码流中加入额外信息的方法,是 H.264/H.265 这些视频压缩标准的特性之一。

在 H264/AVC 编码格式中 NAL uint 中的头部, 有 type 字段指明 NAL uint 的类型, 当 “type = 6” 时,该 NAL uint 携带的信息即为 补充增强信息(SEI)。

在视频内容的生成端传输过程中,都可以插入 SEI 信息。

2、SEI 基本特征

  • 并非解码过程的必须选项。也就是说,SEI 对解码过程无直接影响。
  • 可能对解码过程(容错、纠错)有帮助,可以根据 SEI 中插入的信息在解码过程中编写逻辑。
  • 集成在视频码流中,从码流中去读取。

3、SEI 应用

利用 SEI 可以存储数据的特性,还可以实现如下功能:

  • 传递编码器参数
  • 传递视频版权信息
  • 传递摄像头参数
  • 传递内容生成过程中的剪辑事件
  • 传递自定义消息

企业可以根据自身业务场景需求,利用 SEI 的特性去实现业务功能。

二、如何使用 SEI 实现业务逻辑

 

下面我们将以 web端 为切入点,带大家了解一下 SEI 的读取过程及其应用。

1、在视频码流中插入 SEI

在实现读取 SEI 之前,必须要在音视频码流中插入 SEI。大家可以了解一下SEI 的插入方式及规则,具体操作步骤可在网络进行搜索了解。

2、在 Web 平台进行读取

hjplayer.js 是一款音视频插件,它能够将 FLV 文件流和 HLS 的 TS 文件流经过解码和转码,转换为 Fragmented MP4,然后通过 Media Source Extensions API 将 mp4 片段填充到 HTML5,它提供了 SEI 信息的回调方法。

插件初始化:

const videoElement = document.getElementById('videoElement');
const player = new HJPlayer({
    type: 'flv',
    url: 'http://xxx.xxx.com/xxxx.flv',
});
player.on(HJPlayer.Events.GET_SEI_INFO, (e) => {
    console.log(e); // SEI Message
});

技术教程 —— 如何利用 SEI 实现音画同步?_缓存_03

该回调方法提供了读取到的 SEI 返回的信息,但该 SEI 信息并不是对应当前视频播放进度,而是当前视频缓存读取的进度。也就是说,当前回调返回的不是当前播放帧的 SEI,而是未来帧的SEI,此时我们就需要知道返回的这条 SEI 对应着哪一帧。

3、获取当前 SEI 返回的位置

要获取 SEI 返回位置,需要根据 hjplayer.js 的源码进行改造。

在改造之前我们需要了解 SEI 读取的原理

  • 首先 hjplayer.js 基于 flv.js 封装。其工作原理是:将 FLV 文件流转码复用成 ISO BMFF(MP4 碎片)片段,然后通过 Media Source Extensions,将 MP4 片段设置到原生的 HTML5 Video 标签中,进行播放;
  • 然后,在 FLV 文件流转码复用的过程中,会对该 MP4 片段进行解析,通过解析 NALU 携带的信息,就可以拿到 SEI 信息。

因为是以片段为单位进行解析,所以我们无法准确知道每一条 SEI 的具体位置,但是可以知道含 SEI 片段的具体位置,算出该片段的具体位置,即可得到该 SEI 的大致的位置。

下面我们通过改造 hjplayer.js 的源码,获取该包含 SEI 片段的位置。话不多说,让我们一起看下改造后的源码:

// HJPlayer/src/Codecs/FLVCodec/Demuxer/FLVDemuxer.ts
_parseAVCVideoData(
    arrayBuffer: ArrayBuffer,
    dataOffset: number,
    dataSize: number,
    tagTimestamp: number,
    tagPosition: number,
    frameType: number,
    cts: number
) {
    const le = this._littleEndian;
    const v = new DataView(arrayBuffer, dataOffset, dataSize);
    const units: Array<NALUnit> = [];
    let length = 0;
    let offset = 0;
    const lengthSize = this._naluLengthSize;
    const dts = this._timestampBase + tagTimestamp;
    let isKeyframe = frameType === 1; // from FLV Frame Type constants
    while(offset < dataSize) {
        if(offset + 4 >= dataSize) {
            Log.warn(
                this.Tag,
                `Malformed Nalu near timestamp ${dts}, offset = ${offset}, dataSize = ${dataSize}`
            );
            break; // data not enough for next Nalu
        }
        // Nalu with length-header (AVC1)
        let naluSize = v.getUint32(offset, !le); // Big-Endian read
        if(lengthSize === 3) {
            naluSize >>>= 8;
        }
        if(naluSize > dataSize - lengthSize) {
            Log.warn(this.Tag, `Malformed Nalus near timestamp ${dts}, NaluSize > DataSize!`);
            return;
        }
        const unitType = v.getUint8(offset + lengthSize) & 0x1f;
        if(unitType === 5) {
            // IDR
            isKeyframe = true;
        }
        const data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize);
        const unit: NALUnit = { type: unitType, data };
        if(unit.type === 6) {
            // 获取到SEI信息
            try {
                const unitArray: Uint8Array = data.subarray(lengthSize);
                // 新增 tagPosition 回调参数,返回当前读取片段的位置
                this.eventEmitter.emit(Events.GET_SEI_INFO, { sei: unitArray, tagPosition });
            } catch (e) {
                Log.log(this.Tag, 'parse sei info error!');
            }
        }
        units.push(unit);
        length += data.byteLength;
        offset += lengthSize + naluSize;
    }
    if(units.length) {
        const track = this._videoTrack;
        const avcSample: AvcSampleData = {
            units,
            length,
            isKeyframe,
            dts,
            cts,
            pts: dts + cts
        };
        if(isKeyframe) {
            avcSample.fileposition = tagPosition;
        }
        track.samples.push(avcSample);
        track.length += length;
    }
}

技术教程 —— 如何利用 SEI 实现音画同步?_缓存_04

在上面的源码中,_parseAVCVideoData 方法中解析了 SEI 信息,tagPosition 参数是用于标识当前读取片段的位置,在触发 Events.GET_SEI_INFO 回调的位置,暴露该参数,用 tagPosition 除以视频资源的总字节长度 totalLength,得到读取位置的百分比,即可算出该 SEI 对应的大致位置。

如果想要知道更准确的 SEI 位置,可以每次读取更小的片段,从而使得计算更为准确,当然这也会增加一定的性能消耗。

4、利用 SEI 存储的时间戳校正视频进度

利用 SEI 可以存储数据的特性,在 SEI 内存储视频流播放位置的时间戳,根据这个数据作为一个播放时长基准。

思路如下:

步骤一:计算当前 SEI 记录的位置,比如是第 10s 返回的 SEI;

步骤二:根据计算出的 SEI 位置,找出当前 SEI 位置对应的帧节点,并将当前 SEI 记录的时间戳保存在帧节点数据中;

步骤三:根据时间戳和开始播放时间,计算出当前帧该视频的基准进度,如果视频进度和基准进度相差大于一定阈值则校正回基准进度。

下面我们以一个例子,理解上述思路:

技术教程 —— 如何利用 SEI 实现音画同步?_音视频_05

技术教程 —— 如何利用 SEI 实现音画同步?_ide_06

上图是回放播放器某段区域的时间轴,假设回放播放器开始播放时的时间戳记录为 T1:

  1. 回放播放器播放至第 7s 时,有一条视频流进来,此时是从进度 0 的位置开始播放;
  2. 回放播放器播放至第 10s 时,该条视频流当前播放到了第 3s;
  3. 而在第 10s 的位置,此时帧节点中保存有 SEI 信息,记录的时间戳为 T2;
  4. 根据 T2 - T1 - 7s,得到该视频流的基准播放进度为 C;
  5. 如果 C 减去当前视频流进度 3s(即 c - 3s),大于 0.5s 的话则将当前的视频流进度调整为 C,确保当前视频流画面和其他非视频流画面同步展示。

以上就是利用 SEI 存储的时间戳,校正视频进度的过程,保证了回放的过程中的音画同步。

三、hjplayer.js 踩坑及填坑技巧

在使用 hjplayer.js 插件获取 SEI 的同时,我们还会用它来进行一些音视频的基本操作,例如播放、快进快退等。

在使用的过程中会出现以下常见的问题,下面将针对具体的情况进行讲解。

 

问题一:waiting 状态的处理

当用户将视频进度调整至未缓存区域之后,当前视频会出现 waiting 状态,导致视频显示 loading 并无法正常播放和跳转,这时就需要调用 player 实例的 unload、load 方法进行视频的重新加载。

示例代码如下:

const videoElement = document.getElementById('videoElement');
const player = new HJPlayer({
    type: 'flv',
    url: 'http://xxx.xxx.com/xxxx.flv',
    }, {
      ...user config
      }
);
player.attachMediaElement(videoElement);
player.load();
player.play();

// ...

videoElement.addEventListener('waiting', () => {
    player.unload();
    player.load();
});

技术教程 —— 如何利用 SEI 实现音画同步?_缓存_07

问题二:跳转至未缓存区域的处理

当用户将视频进度调整至未缓存区域时,视频画面会出现一个 loading 图标,并会停止在当前进度,无法正常跳转和播放,视频处于 waiting 状态,如下图所示:

技术教程 —— 如何利用 SEI 实现音画同步?_ide_08

技术教程 —— 如何利用 SEI 实现音画同步?_ide_09

 

我们可以通过下面的操作来避免这个问题:

 

步骤一:设置 lazyLoad 属性

const videoElement = document.getElementById('videoElement');
const player = new HJPlayer({
    type: 'flv',
    url: 'http://xxx.xxx.com/xxxx.flv',
    }, {
      lazyload: false,
      ...user config
      }
);

技术教程 —— 如何利用 SEI 实现音画同步?_缓存_10

设置 lazyLoad 属性为 false,表明当视频缓存足够长时,不会断开 HTTP 链接。但如果加载的是比较长的视频时,缓存到一定进度还是会停止往后加载;

 

步骤二:监听缓存进度,并将其挂载在 player 实例上

videoElement.addEventListener('process', () => {
    const len = video.buffered.length;
    if (len) {
        player.process = video.buffered.end(len - 1);
    }
});

技术教程 —— 如何利用 SEI 实现音画同步?_ide_11

从缓存进度的监听回调中,记录当前视频的缓存进度。

 

步骤三:调整跳转进度方法

function seek(targetTime) {
    if (player.task) return;
    player.task = setInterval(() => {
        const process = player.process;
        
        if (targetTime > process) {
           videoElement.currentTime = process - 2;
        } else {
           videoElement.currentTime = targetTime;
           clearInterval(player.task);
           player.task = null;
        }
    }, 100);
}

技术教程 —— 如何利用 SEI 实现音画同步?_缓存_12

通过定时器,轮询当前缓存进度,如果当前的缓存进度小于目标进度,则将当前的播放进度调整至缓存进度差不多的位置,此时就能主动触发请求缓存资源,直至缓存到目标进度。

至此,跳转至未缓存区域问题已处理完毕。

 

四、总结

数据流录制是将教育企业的自研技术进行优化加码所形成的一套便捷高效、接入即用的标准化PaaS方案,打破传统录制服务,实现 100% 录制还原效果。

以上就是本篇文章关于补充增强信息(SEI)的解读及应用,即构科技利用 FLV 音视频携带的 SEI ,携带一些校验信息,校验音视频的基准播放时长,利用 SEI 实现多个回放画面的实时同步,最高程度的还原了直播现场,提升录制回看的质量。

更多关于数据流录制的详细信息,可查看即构科技官方文档,点击了解:Web 数据流录制示例源码下载 - 开发者中心 - ZEGO即构科技

 

技术教程 —— 如何利用 SEI 实现音画同步?_ide_13

 



标签:音画,视频,教程,缓存,const,player,进度,SEI
From: https://blog.51cto.com/u_14794264/8153305

相关文章

  • Rhino(犀牛) 7.4 下载及安装教程
    本文所提供的安装教程均来自互联网,仅供大家学习使用,不可用于商业用途,否则本作者不负责,如本文提供的信息涉及侵权,请联系作者删除,谢谢大家配合。 软件介绍:Rhino7.4是一款3D建模软件,由美国公司McNeel&Associates开发。它是一款功能强大的CAD工具,可以用于产品设计、建筑设计、工业......
  • 无涯教程-H2数据库 - 更新数据(Update)
    UPDATE查询用于更新或修改表中的现有记录,无涯教程可以将WHERE子句与UPDATE查询一起使用来更新所选行,否则所有行都会受到影响。Update-语法以下是UPDATE查询的基本语法。UPDATEtableName[[AS]newTableAlias]SET{{columnName={DEFAULT|expression}}[,...]}......
  • ZEGO 教程 | RTC + AI 视觉的最佳实践(移动端)
    ​ ​摘要:帮助开发者在音视频场景中快速获得AI视觉功能——美颜、滤镜、背景抠图等。文|即构 NativeSDK开发团队Z世代作为社会新的消费主力,追求个性、热爱新奇事物,青睐与酷炫新奇的玩法、紧跟娱乐潮流。AI+音视频的结合,作为在音频和视频之外第三个场景玩法创新的支撑点......
  • 无涯教程-H2数据库 - 插入数据(Insert)
    SQLINSERT语句用于将新的数据行添加到数据库中。Insert-语法以下是INSERTINTO语句的基本语法。INSERTINTOtableName{[(columnName[,...])]{VALUES{({DEFAULT|expression}[,...])}[,...]|[DIRECT][SORTED]select}}|{SET{columnNa......
  • 无涯教程-H2数据库 - 安装步骤
    H2是用Java编写的数据库,无涯教程可以使用JDBC轻松将此数据库嵌入到无涯教程的应用程序中,无涯教程可以在许多不同的平台或任何版本的JavaRuntimeEnvironment上运行它,但是,在安装数据库之前,应在系统中安装Java。验证Java安装如果系统中安装了JDK,请尝试使用以下命令来验证Java版......
  • 无涯教程-H2数据库 - 简介
    H2是开源的轻量级Java数据库,它可以嵌入Java应用程序中或以客户端(Client)-服务器(Server)模式运行,H2数据库主要可以配置为内存数据库运行,这意味着数据将不会持久存储在磁盘上,由于具有嵌入式数据库,因此它不用于生产开发,而主要用于开发和测试。可以在嵌入式模式或服务器模式下使用......
  • Thinkpad 智能控温系统TPFanControl软件安装教程
    由于原来的tpfancontrol.com已经下线了,现在的TPFanControl可以到https://thinkwiki.de/TPFanControl里面进行下载,这里面复制了之前TPFanControl.com的页面,直接拉到下面点击下载: 下载安装后如果发现乱码,可以在页面FAQ下找到解决乱码的方案:问题描述是,在远东地区的windows......
  • Windows10下用Anaconda3安装TensorFlow教程
    安装好了Anaconda3—后,运行开始菜单—>Anaconda3—>AnacondaPrompt##CPUpip3installtensorflow-ihttps://pypi.tuna.tsinghua.edu.cn/simple/##GPUpip3installtensorflow-gpu-ihttps://pypi.tuna.tsinghua.edu.cn/simple/##TESTimporttensorflowastfhello=......
  • BSP视频教程第28期:CANopen主从机组网实战,CAN词典工具使用方法以及吃透PDO玩法
    视频教程汇总帖:https://www.armbbs.cn/forum.php?mod=viewthread&tid=110519 本期视频的实战性较强,涉及到的内容比较多,特别是PDO同步周期和同步非周期玩法要吃透。视频教程:https://www.bilibili.com/video/BV1zy4y1A7HS/视频提纲:参考资料:CANopen主从机代码,CAN词......
  • django搭建平台实战教程三:接口编写及权限校验(前后端分离)
    自定义权限校验注册增加group_id字段...@api_view(['POST'])defregister(request:Request):ifDUser.objects.filter(username=request.data["username"]).count()>0:returnResponse({"code":400,&qu......