文档反馈
文档反馈

媒体补充增强信息

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

功能描述

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

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);
}
×

反馈成功

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