项目背景: 由于业务需要,我们需要实现连麦的功能。考虑到连麦对实时性要求比较高,为了能够快速上线,体验产品的效果。我们选择了接入一个第三方连麦sdk来完成连麦需求。目前我们已有音视频采集、处理、推流模块,最理想的接入方式是直接将编码后的音视频packet传给第三方sdk负责推流。这种维护成本最小,而且对项目的改动最小。然而,第三方SDK并没有提供这种接入方式,但是支持提供了外部的音视频源数据推流的方式。所以,现在问题就是讲我们目前系统中采集、处理好的音视频数据不经过编码直接推给SDK处理。主要用到的第三方sdk接口:
booleanpushExternalVideoFrame(...)
booleanpushExternalAudioFrame(...)
沿着这个思路,在实现的过程中,还是遇到一些问题和踩过的坑的。本文就这些问题和坑做一下总结,方便后续的接入者。
视频美颜处理
SDK有两种推送外部视频帧的方法,分别是基于TEXTURE_2D和基于 NV21的。由于我们的视频图像的处理是基于TEXTURE_2D的,转换成YUV buffer效率太低,因此采用了基于TEXTURE_2D的方式。
这里还有一个问题需要解决:Camera得到的预览数据是OES格式的,我们需要转换成标准的GL_TEXTURE_2D格式才能进行处理。
设置外部视频源的代码如下:
mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P_10,true);
下图是处理过程

视频这块,整个自定义预处理数据的流程如下图

音频特效处理

SDK提供了混入伴奏的方法,我们这边还需要对音频数据进行特效处理。特效是针对人声和伴奏分别处理的,SDK提供的音频数据接口中没有分离出人声和伴奏。音频这块可以完全由我们自己的采集及特效处理(采用模式1)。处理完的字节流数据push给SDK进行编码发送。
项目中,设置外部音频源代码如下
mRtcEngine.setExternalAudioSource(true,44100,2);
mRtcEngine.setMixedAudioFrameParameters(44100,1024);
伴奏混入卡顿问题
由于人声和伴奏的混入及特效处理是在native层完成的,需要把处理后的byte传到java层,因此每隔20ms左右就会有一次音频数据回传jni调用,每次jni调用花费的时间不等,大部分是1ms内能完成数据的回传,但也有几十ms的情况,这就造成了这几十毫秒内SDK那边没有音频数据,从而表现出卡顿的现象。主要从以下两个方面解决这个问题:
a)数据的处理和回传是一个生产者-消费者问题,可以把特效处理后的音频数据放在一个队列中,开一个单独的线程来进行数据的回传,数据的回传和处理不在一个线程能加快数据的处理速度
b)将音频数据处理的时间戳记录下来,将这个值而不是回传的时间戳传给SDK,由SDK去同步
观众端旁路拉流导致的画面和UI不统一显示的问题
同前段时间非常火爆的在线答题的需求类似,主播和嘉宾在连麦过程中,房间内其他的观众是拉的一个旁路流,会有几秒钟的延迟。这样问题就来了,主播和嘉宾在连麦的成功后,需要通知观众调整UI布局,使右下角的连麦嘉宾位置空出来。方案一是通过socket信令消息来通知用户改变UI布局,但这种方案会导致信令消息和画面不同步。最好的解决方案就是讲这个信令消息嵌入在视频中。这样就不会出现信令消息和画面不同步的问题了。方案二是通过读取视频帧里面的SEI数据来实现。
SDK有接口可以控制往视频旁路流中填SEI数据(和视频相关的补充信息),由于视频帧中的SEI信息会比服务器下发的消息更能实时反映当前主播和连麦嘉宾的页面布局信息,因此观众端播放器中sei数据的获取还是很有必要的。我们项目中拉取的流是RTMP格式的,H.264的裸流是由一个一个NALU(数据传输单元)组成,结构如下图所示:

NALU之间是通过startcode分隔的,起始码分:0x000001(3Byte)和0x00000001(4Byte)两种。但cdn厂商的flv格式拉流地址的的nalu中并没有startcode,具体格式如下图

我们可以从type为6的nalu中获取我们所需要的sei信息,下面是解析sei数据的具体代码,参照了标准文档,具体见 https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-200305-S!!PDF-E&type=items中的sei数据章节
void FFMPEGVideoDecoder::parseSeiData(uint8_t *buffer, int len){
uint8_t *bytes = buffer;
int offset = 4;
offset++;
int payloadType = 0;
while (bytes[offset] == 0xFF) {
payloadType += bytes[offset];
offset ++;
}
payloadType +=bytes[offset];
offset++;
int payloadSize =0;
while (bytes[offset] == 0xFF){
payloadSize += 255;
offset ++;
}
payloadSize += bytes[offset];
offset +=1;
memset(seiData,0,SEI_DATA_LEN);
int copyLen = payloadSize;
if (copyLen > (SEI_DATA_LEN - 1)) {
copyLen = SEI_DATA_LEN - 1;
}
memcpy(seiData,buffer+offset,copyLen);
if (mUploaderCallback != NULL) {
mUploaderCallback->seiDataCallback(seiData,copyLen);
}
}
解出来的SEI数据会包含uid(SDK设置的)及布局信息,有了这些信息便能灵活在页面上加入一些业务展示元素了。
旁路推流音频采样率的设置
最开始采用的是旧版本sdk,在主播进行从我们的推流模式转换到SDK模式时,连麦嘉宾及第三方普通观众会crash掉,追及原因SDK默认的旁路推流采样率是32000的、单声道的, 如下图

由于解码器没有重启,仍是按照之前读到的配置去读两个声道的数据,直接crash掉了。新版本的sdk可以通过LiveTranscoding类单独设置旁路推流的采样率及channel,可以设置成我们所需要的值。
transcoding.lowLatency = true;
transcoding.audioSampleRate = LiveTranscoding.AudioSampleRateType.TYPE_44100;
transcoding.audioChannels = 2;
以上便是我们接入SDK实现连麦遇到的一些问题,希望本文能给有相应业务需求的同行给予一点帮助,有问题一起探讨。




