摘要: 利用 SEI 解决数据流录制回放过程中的音画不同步问题。
文|即构 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 返回的信息,但该 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;
}
}
在下面的源码中,_parseAVCVideoData 办法中解析了 SEI 信息,tagPosition 参数是用于标识以后读取片段的地位,在触发 Events.GET_SEI_INFO 回调的地位,裸露该参数,用 tagPosition 除以视频资源的总字节长度 totalLength,失去读取地位的百分比,即可算出该 SEI 对应的大抵地位。
如果想要晓得更精确的 SEI 地位,能够每次读取更小的片段,从而使得计算更为精确,当然这也会减少肯定的性能耗费。
4、利用 SEI 存储的工夫戳校对视频进度
利用 SEI 能够存储数据的个性,在 SEI 内存储视频流播放地位的工夫戳,依据这个数据作为一个播放时长基准。
思路如下:
步骤一:计算以后 SEI 记录的地位,比方是第 10s 返回的 SEI;
步骤二:依据计算出的 SEI 地位,找出以后 SEI 地位对应的帧节点,并将以后 SEI 记录的工夫戳保留在帧节点数据中;
步骤三:依据工夫戳和开始播放工夫,计算出以后帧该视频的基准进度,如果视频进度和基准进度相差大于肯定阈值则校对回基准进度。
上面咱们以一个例子,了解上述思路:
上图是回放播放器某段区域的时间轴,假如回放播放器开始播放时的工夫戳记录为 T1:
- 回放播放器播放至第 7s 时,有一条视频流进来,此时是从进度 0 的地位开始播放;
- 回放播放器播放至第 10s 时,该条视频流以后播放到了第 3s;
- 而在第 10s 的地位,此时帧节点中保留有 SEI 信息,记录的工夫戳为 T2;
- 依据 T2 – T1 – 7s,失去该视频流的基准播放进度为 C;
- 如果 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();});
问题二:跳转至未缓存区域的解决
当用户将视频进度调整至未缓存区域时,视频画面会呈现一个 loading 图标,并会进行在以后进度,无奈失常跳转和播放,视频处于 waiting 状态,如下图所示:
咱们能够通过上面的操作来防止这个问题:
步骤一:设置 lazyLoad 属性
const videoElement = document.getElementById('videoElement');
const player = new HJPlayer({
type: 'flv',
url: 'http://xxx.xxx.com/xxxx.flv',
}, {
lazyload: false,
...user config
}
);
设置 lazyLoad 属性为 false,表明当视频缓存足够长时,不会断开 HTTP 链接。但如果加载的是比拟长的视频时,缓存到肯定进度还是会进行往后加载;
步骤二:监听缓存进度,并将其挂载在 player 实例上
videoElement.addEventListener('process', () => {
const len = video.buffered.length;
if (len) {player.process = video.buffered.end(len - 1);
}
});
从缓存进度的监听回调中,记录以后视频的缓存进度。
步骤三:调整跳转进度办法
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);
}
通过定时器,轮询以后缓存进度,如果以后的缓存进度小于指标进度,则将以后的播放进度调整至缓存进度差不多的地位,此时就能被动触发申请缓存资源,直至缓存到指标进度。
至此,跳转至未缓存区域问题已处理完毕。
四、总结
数据流录制是将教育企业的自研技术进行优化加码所造成的一套便捷高效、接入即用的标准化 PaaS 计划,突破传统录制服务,实现 100% 录制还原成果。
以上就是本篇文章对于补充加强信息(SEI)的解读及利用,即构科技利用 FLV 音视频携带的 SEI,携带一些校验信息,校验音视频的基准播放时长,利用 SEI 实现多个回放画面的实时同步,最高水平的还原了直播现场,晋升录制回看的品质。
更多对于数据流录制的详细信息,可查看即构科技官网文档,点击查看:https://doc-zh.zego.im/articl…