文档反馈
文档反馈

媒体补充增强信息

通过 NERTC SDK,您可以将包括音量信息在内的自定义信息作为 SEI 的一部分,封装在视频码流中,并将其发送至远端用户解码查看,可用于音画同步等场景。

功能描述

在音视频流媒体应用中,用户的消息分发通道和音视频或直播通道是分开的,通常情况下难以保证消息与视频数据的同步性。NERTC 支持将时间戳等自定义数据作为流媒体补充增强信息 (SEI Supplemental Enhancement Information)的一部分,通过流媒体通道将其与视频内容打包在一起,发送给远端用户,以此实现文本数据与音视频内容的精准同步的目的。

直播场景中,互动直播 2.0 会自动将音视频房间中参与互动直播成员的 SEI 信息进行重新封装,通过网易云信自定义的 SEI 类型打包,推流至播放端,您可以通过网易云信播放器 SDK 自动解析视频流 SEI 中封装的自定义数据。

SEI 一般用于以下场景:

注意事项

实现方法

音视频直播场景

本端:

  1. 本端加入房间后,通过 enableLocalVideo 开启视频流。
  2. 视频流成功开启后,调用 sendSEIMsg 接口发送 SEI 信息。

远端:

  1. 注册一个 NERtcEngineVideoSEIObserver 观测器用于接收远端流的 SEI 内容回调。
  2. 远端接收到本端发送的 SEI 信息后,触发 onNERtcEngineRecvSEIMsg 回调。

纯音频通话场景

纯音频通话场景下,如果需要发送 SEI 信息到远端,SDK 会自动采取 Fake Video 方案。实现过程如下:

  1. 本端调用「sendSEIMsg」发送SEI信息。

    此时SDK内部会自动生成一个 Fake Video 的视频流,并携带 SEI 信息发送至远端。Fake Video 的分辨率为 16×16,画面为纯黑色。

  2. 远端接收到纯音频流下发送的 Fake Video 的信令通知,通过 NERtcConstants.VideoProfile 中的 kVideoProfileFake 字段判断该视频流为 Fake Video。

    此时需要订阅该视频流,但无需展示该视频流画面。

  3. 远端注册一个 NERtcEngineVideoSEIObserver 观测器用于接收远端流的 SEI 内容回调。

  4. 远端接收到本端发送的SEI信息后,触发 onNERtcEngineRecvSEIMsg 回调。

当您在纯音频通话场景下,通过发送 Fake Video 的方式实现 SEI 帧发送后,如果需要开启视频流正常发送视频数据,可以通过 enableLocalVideo 开启视频流,并调用 sendSEIMsg 继续发送 SEI 信息。此时SDK会自动判断之前是否开启了 Fake Video,如果开启了就会关闭 Fake Video,然后再开启视频。

示例代码

Android

// 发送SEI , 默认通过主流来发送
//int ret = NERtcEx.getInstance().sendSEIMsg(seiMsg);
//或者可以指定是通过主流还是辅流来发送SEI
int ret = NERtcEx.getInstance().sendSEIMsg(seiMsg, nERtcVideoStreamType);
if (ret !=  NERtcConstants.ErrorCode.OK) {
    showToast("SEI 发送失败 , ret : " + ret);
}

//接收SEI, NERtcCallbackEx#onRecvSEIMsg(long userID, String seiMsg)
public class NERtcCallbackImpl implements NERtcCallbackEx{

//todo SDK 其他回调方法

@Override
public void onRecvSEIMsg(long userID, String seiMsg) {
  }
}

iOS

/**
 NERtcEngine 扩展回调
 */
@protocol NERtcEngineDelegateEx <NERtcEngineDelegate, NERtcEngineVideoFrameObserver, NERtcEngineAudioSessionObserver,NERtcEngineLiveStreamObserver, NERtcEngineVideoSEIObserver>
@end


//实现SEI的回调注册
- (NERtcEngine *)coreEngine {
    if (!_coreEngine) {
        _coreEngine = [NERtcEngine sharedEngine];

        //video codec
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        NSDictionary *keyMap = NTESToNERtcSettingsKeyMap();
        fillNERtcParams(params, keyNRTCDemoPreferHWEncode, keyMap);
        fillNERtcParams(params, keyNRTCDemoPreferHWDecode, keyMap);
        [_coreEngine setParameters:params];

        //context
        NERtcEngineContext *context = [[NERtcEngineContext alloc] init];
        context.engineDelegate = self;
        //
        NSString *settingsKey = [NTESDemoSettings stringForKey:keyNRTCDemoAppKey];
        NSString *appKey = settingsKey.length > 5 ? settingsKey : [NTESDemoConfig sharedConfig].appKey;
        context.appKey = appKey;
        //
        NERtcLogSetting *logSetting = [[NERtcLogSetting alloc] init];
        logSetting.logDir = [NTESDemoConfig sharedConfig].rootDir;
        if (![NTESDemoSettings boolForKey:keyNRTCDemoAppLogEnabled defaultVal:YES]) {
            logSetting.logLevel = kNERtcLogLevelOff;
        }
        else {
            logSetting.logLevel = kNERtcLogLevelDetailInfo;
        }

        context.logSetting = logSetting;
        [_coreEngine setupEngineWithContext:context];
    }

    return _coreEngine;
}


//发送SEI:
- (void)onMenuSendSEI:(id)sender {
    static int index = 0;
    NSString *msg = [NSString stringWithFormat:@"index:%d,msg:%.f", ++index, [[NSDate date] timeIntervalSince1970]];
    NSData *data = [msg dataUsingEncoding:NSUTF8StringEncoding];

    NERtcStreamChannelType type = kNERtcStreamChannelTypeMainStream;
    if ([NTESDemoSettings objectForKey:keyNRTCDemoLocalSEISendPrefer]) {
        type = (NERtcStreamChannelType)([NTESDemoSettings integerForKey:keyNRTCDemoLocalSEISendPrefer]);
    }

    [[[NTESDemoLogic sharedLogic] getCoreEngine] sendSEIMsg:data streamChannelType:type];
}


//接受SEI
- (void)onNERtcEngineRecvSEIMsg:(uint64_t)userID message:(NSData *)message {
    NSString *msg = [[NSString alloc] initWithData:message encoding:NSUTF8StringEncoding];
    NSString *seiMsg = [NSString stringWithFormat:@"userID:%llu,message:%@",userID, msg];
    MakeToast(seiMsg);
}

Windows

//string到UTF8的格式转换
std::string StringToUTF8(const std::string &str) { 
    int nwLen = ::MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, NULL, 0);
    wchar_t *pwBuf = new wchar_t[nwLen + 1]; 
    ZeroMemory(pwBuf, nwLen * 2 + 2); 
    ::MultiByteToWideChar(CP_ACP, 0, str.c_str(), str.length(), pwBuf, nwLen); 
    int nLen = ::WideCharToMultiByte(CP_UTF8, 0, pwBuf, -1, NULL, NULL, NULL, NULL); 
    char *pBuf = new char[nLen + 1]; 
    ZeroMemory(pBuf, nLen + 1); 
    ::WideCharToMultiByte(CP_UTF8, 0, pwBuf, nwLen, pBuf, nLen, NULL, NULL); 

    std::string strRet(pBuf); 
    delete[] pwBuf; 
    delete[] pBuf; 
    pwBuf = NULL; 
    pBuf = NULL; 

    return strRet; 
} 

// 首先获取到引擎对象 engine
nertc::IRtcEngineEx* nrtc_engine_ = ...

//subStream 为true表示辅流,false表示主流
NERtcStreamChannelType streamChannelType = subStream ? kNERtcStreamChannelTypeSubStream : kNERtcStreamChannelTypeMainStream; 

//SEI message,需要转换成UTF8格式
std::string strSEIInfo = StringToUTF8(strSEIMsg); 

//发送SEI消息
int res = nrtc_engine_->sendSEIMsg(reinterpret_cast<const char*>(strSEIInfo.c_str()), strSEIInfo.size()+1, streamChannelType); 

//消息打印
ShowLogInfo("ChannelType:%s, Send SEI msg:%s\n", subStream ? "kNERtcStreamChannelTypeSubStream" : "kNERtcStreamChannelTypeMainStream", strSEIInfo.c_str());

macOS

// 首先获取到引擎对象 engine
nertc::IRtcEngineEx* engine = ...

// 准备发送的数据 msg
NSString* msg = ...
int len = (int)strlen([msg UTF8String]);

// 接着区分要使用哪一个通道:主流or辅流
BOOL mainStream = ...

// 正式发送SEI数据
int errCode = nertc::kNERtcNoError;

if (mainStream) {
    errCode = engine->sendSEIMsg([msg UTF8String], len, nertc::kNERtcStreamChannelTypeMainStream);
    NSLog(@"发送SEI 主流:%@ | error code: %d", msg, errCode);
} else {
    errCode = engine->sendSEIMsg([msg UTF8String], len, nertc::kNERtcStreamChannelTypeSubStream);
    NSLog(@"发送SEI 辅流:%@ | error code: %d", msg, errCode);
}

互动直播视频流的 SEI 格式

在互动直播场景中,互动直播 2.0 服务端转码推流时,在转码后的 H264/H265 的 SEI 中增加当前视频的编码信息,其中包含客户端 SDK 上传的自定义 SEI 信息,封装类型为网易云信自定义的 SEI 类型。您可以通过网易云信播放器 SDK 自动解析视频流 SEI 中封装的自定义数据。

SEI 格式

SEI 的格式为 JSON 格式的字符串,例如:

{
    "canvas":{
        "w":640,
        "h":360,
        "bgnd":"#000000"
    },
    "regions":[
        {
            "uid":1,
            "alpha":1,
            "zorder":1,
            "volume":50,
            "x":0,
            "y":0,
            "w":320,
            "h":360
        },
        {
            "uid":2,
            "alpha":1,
            "zOrder":1,
            "volume":89,
            "x":320,
            "y":0,
            "w":320,
            "h":360
        }
    ],
    "rtc_sei":[
        {
            "uid":1,
            "mainSei":"xxxxx",
            "subSei":"xxxxx"
        },
        {
            "uid":2,
            "mainSei":"xxxxx",
            "subSei":"xxxxx"
        }
    ],
    "ver":"20190611",
    "ts":1535385600000,
    "app_data":""
}

字段说明

参数 概述
canvas - 画布信息。
w 画布的宽度,单位为像素。其值为推流端在 NERtcLiveStreamLayout 结构体中设置的 width 参数。
h 画布的高度,单位为像素。其值为推流端在 NERtcLiveStreamLayout 结构体中设置的 height 参数。
bgnd bgnd:画布的背景颜色,格式为 RGB 定义下的十六进制整数。其值为推流端在 NERtcLiveStreamLayout 结构体中设置的 background_color 参数。
regions - 所有参与互动直播房间成员的信息(包含布局信息),为 region 的列表。 其值为推流端在 NERtcLiveStreamLayout 结构体中设置的 users 参数。
uid 该区域对应成员的 ID。其值为推流端在 NERtcLiveStreamUserTranscoding 结构体中设置的 uid 参数。
alpha 预留参数,暂未启用。
zorder 预留参数,暂未启用。
volume 该区域对成员的音量。
x 该区域在画布中对应的 x 坐标。其值为推流端在 NERtcLiveStreamUserTranscoding 结构体中设置的 x 参数。
y 该区域在画布中对应的 y 坐标。其值为推流端在 NERtcLiveStreamUserTranscoding 结构体中设置的 y 参数。
w 该区域的宽度,单位为像素。其值为推流端在 NERtcLiveStreamUserTranscoding 结构体中设置的 width 参数。
h 该区域的高度,单位为像素。其值为推流端在 NERtcLiveStreamUserTranscoding 结构体中设置的 height 参数。
rtc_sei - 参与互动直播的房间成员上传的 SEI。即其客户端 SDK 在 sendSEIMsg 中设置的 data 数据。
uid 发送 SEI 的用户 ID。
mainSei 客户端 SDK 在主流通道中封装的 SEI。
subSei 客户端 SDK 在辅流通道中封装的 SEI。
ver 预留参数,暂未启用。
ts 生成该信息时的 Unix 时间戳,单位为毫秒。
app_data 预留参数,暂未启用。
×

反馈成功

非常感谢您的反馈,我们会继续努力做得更好。