在音视频流媒体应用中,除了可以通过流媒体通道推拉音视频内容外,还可以使用流 SEI(Supplemental Enhancement Information,媒体补充增强信息)通过流媒体通道将文本信息与音视频内容打包在一起,从主播端(推流端)推出,并从观众端(拉流端)接收,以此实现文本数据与音视频内容的精准同步的目的。
一般可用于视频画面的精准布局、远端歌词同步、直播答题等应用场景。
SEI 的相关概念及原理请参考 如何理解和使用 SEI(媒体补充增强信息)。
在实现 SEI 功能之前,请确保:
发送与接收 SEI 信息功能,需要在推流端发送 SEI 信息,在拉流端接收 SEI 信息,如下图所示:
推流端:
拉流端:
由于 SDK 默认使用 ZEGO 自行定义的 SEI(nalu type = 6,payload type = 243)类型打包,且此类型是 SEI 标准未规定的类型,因此跟视频编码器或者视频文件中的 SEI 不存在冲突。
但当开发者需要使用第三方解码器解码时(如 FFmpeg),会导致无法解出正确的 SEI,此时需要在推流 startPublishingStream 时指定发送的 SEI 类型为 UserUnregister 的 SEI(nalu type = 6, payload type = 5),且在推流前和拉流前调用 setSEIConfig 接口设置 uuid(UserUnregisterID)来区分是视频编码器自身产生的 SEI 还是业务 SEI。
App 在发送此类型 SEI 时,可以填写业务特定的 uuid(长度为16字节)。接收方使用 SDK 解析 payload type 为 5 的 SEI 时,会根据设置的过滤字符串过滤出 uuid 相符的 SEI 抛给业务,如果没有设置过滤字符串,SDK 会把所有收到的 SEI 都抛给开发者。
仅当开发者使用第三方解码器解码 SEI 时需要执行该步骤。
let appID = ;
let server = "";
// 初始化
const zg = new ZegoExpressEngine(appID, server);
zg.setSEIConfig({
// 自定义的特定的字符串,以过滤出业务 SEI
unregisterSEIFilter: "zegozegozegozego"
});
发送 SEI 信息的接口需要在推流成功之后调用。
let appID = ;
let server = "";
// 初始化
const zg = new ZegoExpressEngine(appID, server);
// 用户 ID,自定义
let userID = "user_" + new Date().getTime();
// roomID,自定义
let roomID = "0001";
// 鉴权 token
let token = "";
// 推流 ID
let publishStreamID = "00001";
// 登录房间
zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(result => {
if (result == true) {
console.log("login success")
}
});
// 创建本地流预览
const localStream = await zg.createZegoStream();
zg.on("publisherStateUpdate", async result => {
if (result.state === "PUBLISHING") {
// 发送的 SEI 内容,示例如下:
// 时间戳
const ts = Math.ceil(new Date().getTime() / 1000);
// 转换为 bytes
const u = new Uint8Array(4);
u[0] = (ts >> 24) & 0xff;
u[1] = (ts >> 16) & 0xff;
u[2] = (ts >> 8) & 0xff;
u[3] = ts & 0xff;
// 推流成功后,发送 SEI
zg.sendSEI(streamID, u);
}
});
// 推流
zg.startPublishingStream(publishStreamID, localStream, {
roomID,
// 开启发送 SEI
isSEIStart: true,
// 默认为0,代表 payload type = 243
SEIType: 0
});
接收 SEI 信息的回调接口需要在拉流成功之后触发。
let appID = ;
let server = "";
// 初始化
const zg = new ZegoExpressEngine(appID, server);
// 用户 ID,自定义
let userID = "user_" + new Date().getTime();
// roomID,自定义
let roomID = "0001";
// 鉴权 token
let token = "";
// 拉流 ID,一般通过 roomStreamUpdate 回调获取
let playStreamID = "00001";
// 监听 SEI 回调
zg.on("playerRecvSEI", (streamID, uintArray) => {
let offset = 0;
// 接收到的 SEI 内容的前 4 bytes 代表发送的 SEI 类型, 将其转换为 number,
// 1004 代表 payload type = 5, 1005 代表 payload type = 243
let mediaSideInfoType = 0;
mediaSideInfoType = uintArray[offset++] << 24;
mediaSideInfoType |= uintArray[offset++] << 16;
mediaSideInfoType |= uintArray[offset++] << 8;
mediaSideInfoType |= uintArray[offset++];
// 根据发送的 SEI, 解出 SEI 的内容,示例如下:
const view = new DataView(uintArray.buffer);
let i = 4;
let ts = 0;
ts = view.getUint8(i++) << 24;
ts |= view.getUint8(i++) << 16;
ts |= view.getUint8(i++) << 8;
ts |= view.getUint8(i++);
console.log("recv " + streamID + " " + mediaSideInfoType + " " + ts);
});
// 登录房间
zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(result => {
if (result == true) {
console.log("login success")
}
});
// 拉流
zg.startPlayingStream(playStreamID, {
// 开启解析 SEI
isSEIStart: true
}).then(stream => {
}).catch(err => {
});
联系我们
文档反馈