引言
本文由zlmediakit
核心开发者 monktan(老衲不出家)
编写,夏楚
审阅修订;文章主要记录了作者在对接亚马逊Alexa
设备时遇到的一些经验教训,希望前人趟过的坑后人无需再趟。
一、背景
因业务发展,需要在亚马逊Alexa
设备上实现与访客视频对讲;调研发现亚马逊Lambad Alexa Skill平台
(以下简称亚马逊平台)支持WebRTC
和RTSP
两种方式接入,由于需要实现双向对讲,只能采用WebRTC
方式与Alexa设备对接;至于门铃设备端,硬件资源有限且不带屏幕,我们采用的私有协议方式接入。为了便于读者理解,我们省去了发现、认证等流程,整体架构流程图如下:
二、开始趟坑
研究Alexa WebRTC
接入相关文档,发现其视频支持H264
编码格式,音频则支持Opus/PCMU/PCMA/AAC
:
由于WebRTC
协议通常不支持AAC
,为了节省时间,我们直接采用更简单的PCMU
(而不是Opus
)来测试,然而测试发现Alexa
设备竟然无法播放,于是我们对比了之前对接过的web demo
,发现竟然是通的,其架构方式也基本一致:
三、趟坑之路
由于Alexa
设备死活无法播放门铃的音视频流而web demo却一切正常,我做了大量的努力和尝试,包括sdp的分析对比,rtp的分析对比、变换音频编码格式(因为单视频模式有播放成功的案例,原因是单视频模式请求链路时间不一样,时间更短)、音频编码切片长度、音视频时间戳同步、分析设备日志等工作。
3.1 趟坑之路一,分析对比SDP
- Alexa设备Offer
v=0
o=- 3889820441 3889820441 IN IP4 0.0.0.0
s=a 2 z
c=IN IP4 0.0.0.0
t=0 0
a=group:BUNDLE audio0 video0
m=audio 1 UDP/TLS/RTP/SAVPF 96 0 8
a=candidate:1 1 UDP 2013266431 **** 53179 typ host
a=candidate:2 1 TCP 1015021823 **** 9 typ host tcptype active
a=candidate:3 1 TCP 1010827519 **** 58004 typ host tcptype passive
a=candidate:1 2 UDP 2013266430 **** 49423 typ host
a=candidate:2 2 TCP 1015021822 **** 9 typ host tcptype active
a=candidate:3 2 TCP 1010827518 **** 51167 typ host tcptype passive
a=setup:actpass
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtpmap:96 opus/48000/2
a=rtcp:9 IN IP4 0.0.0.0
a=rtcp-mux
a=sendrecv
a=mid:audio0
a=ssrc:724561565 cname:user2420442903@host-e8501a47
a=ice-ufrag:E9vw
a=ice-pwd:CWbMx5SvmNls7LJ23gJJUk
a=fingerprint:sha-256 2D:A0:F3:7D:0A:58:7E:B9:CC:79:C7:10:FB:BB:F9:F7:7D:EE:92:84:F5:08:D2:BC:25:76:C7:75:FF:8B:DB:75
m=video 1 UDP/TLS/RTP/SAVPF 99
a=candidate:1 1 UDP 2013266431 **** 53179 typ host
a=candidate:3 1 TCP 1010827519 **** 58004 typ host tcptype passive
a=candidate:2 1 TCP 1015021823 **** 9 typ host tcptype active
a=candidate:1 2 UDP 2013266430 **** 49423 typ host
a=candidate:2 2 TCP 1015021822 **** 9 typ host tcptype active
a=candidate:3 2 TCP 1010827518 **** 51167 typ host tcptype passive
b=AS:2500
a=setup:actpass
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtpmap:99 H264/90000
a=rtcp:9 IN IP4 0.0.0.0
a=rtcp-mux
a=sendrecv
a=mid:video0
a=rtcp-fb:99 nack
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 ccm fir
a=ssrc:3568304867 cname:user2420442903@host-e8501a47
a=ice-ufrag:E9vw
a=ice-pwd:CWbMx5SvmNls7LJ23gJJUk
a=fingerprint:sha-256 2D:A0:F3:7D:0A:58:7E:B9:CC:79:C7:10:FB:BB:F9:F7:7D:EE:92:84:F5:08:D2:BC:25:76:C7:75:FF:8B:DB:75
- 平台回复Answer
v=0
o=- 0 0 IN IP4 127.0.0.1
s=webrtc_core
t=0 0
a=ice-lite
a=group:BUNDLE audio0 video0
a=rtcp-mux
a=msid-semantic: WMS alexa_test
m=audio 9 UDP/TLS/RTP/SAVPF 0
a=rtcp:9 IN IP4 0.0.0.0
c=IN IP4 0.0.0.0
a=ice-ufrag:93b7543b756a8408
a=ice-pwd:b97ec11486ce7a693d060e80
a=fingerprint:sha-256 4D:1A:F7:3D:CD:5E:E3:24:E5:30:40:F5:E4:1A:9B:E4:14:C6:83:A8:B3:EE:33:0D:D7:62:84:CE:14:DA:C0:8C
a=setup:passive
a=sendrecv
a=mid:audio0
a=msid:alexa_test MainAudio
a=rtcp-mux
a=rtpmap:0 PCMU/8000
a=ssrc:2159555873 cname:webrtccore
a=ssrc:2159555873 msid:alexa_test MainAudio
a=ssrc:2159555873 mslabel:alexa_test
a=ssrc:2159555873 label:MainAudio
a=candidate:foundation 1 udp 100 **** 8000 typ srflx raddr **** rport 8000 generation 0
m=video 9 UDP/TLS/RTP/SAVPF 99
a=rtcp:9 IN IP4 0.0.0.0
c=IN IP4 0.0.0.0
a=ice-ufrag:93b7543b756a8408
a=ice-pwd:b97ec11486ce7a693d060e80
a=fingerprint:sha-256 4D:1A:F7:3D:CD:5E:E3:24:E5:30:40:F5:E4:1A:9B:E4:14:C6:83:A8:B3:EE:33:0D:D7:62:84:CE:14:DA:C0:8C
a=setup:passive
a=sendrecv
a=mid:video0
a=msid:alexa_test MainVideo
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:99 H264/90000
a=rtcp-fb:99 nack
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 ccm fir
a=fmtp:99 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=420015
a=ssrc-group:FID 28521173 3056259
a=ssrc:28521173 cname:webrtccore
a=ssrc:28521173 msid:alexa_test MainVideo
a=ssrc:28521173 mslabel:alexa_test
a=ssrc:28521173 label:MainVideo
a=ssrc:3056259 cname:webrtccore
a=ssrc:3056259 msid:alexa_test MainVideo
a=ssrc:3056259 mslabel:alexa_test
a=ssrc:3056259 label:MainVideo
a=candidate:foundation 1 udp 100 101.33.240.139 8000 typ srflx raddr 101.33.240.139 rport 8000 generation 0
这里咋一看好像没啥问题;仔细发现,Alexa PCMU和PCMA在SDP中没有出现a=rtpmap
,可能导致协商不成功,于是我修改了SDP,添加了:a=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\n
,然而发现并没什么用,还是对接Alexa
设备无输出。
3.2 趟坑之路二,抓包分析
因web demo
推流,Alexa
设备可以正常播放,但是拉取门铃设备流无法播放,分别对起流进行抓包分析对比:
对比发现两个流同样都是PCMU
数据,但是数据长度不一样,上面的能播放,下面的无法播放语音,导致我初步怀疑是因为上面启用了RTP扩展
导致可以播放,分析SRTP包发现,web端推流确实多了RTP扩展,所以长度多了8个字节。
此时的我虽然不太相信是由于RTP扩展引起Alexa设备无法播放语音,但是对于Alexa黑盒来说,只有尽力一试了,通过修改服务端代码,终于做成与web推断流数据包一模一样了;然而,结果并没有什么不一样,web端推流和设备推流到底问题在哪里,分析了数据长度,数据发送频率,音频时间间隔,时间戳增量,甚至尝试过NTP时间发送
,都是没有任何效果,依然是播放不出来的。
把数据分析数据发给其他WebRTC
领域专家们分析,他们也看不出什么问题,建议我使用opus
编码尝试一下,毕竟它在Alexa
官网是preferred codec
,鉴于此决定先用opus尝试。
3.3 趟坑之路三,换Opus编码
opus编码在FFmpeg
中直接采用AV_CODEC_ID_OPUS
方式查找的解码器,找到的是内置opus编码器
,实测发现编码延时很大,达到了普遍350ms~450ms
的延迟(编码机器linux cvm 8c16g主机),下面是FFmpeg内部源码:
AVCodec ff_opus_encoder = {
.name = "opus",
.long_name = NULL_IF_CONFIG_SMALL("Opus"),
.type = AVMEDIA_TYPE_AUDIO,
.id = AV_CODEC_ID_OPUS,
.defaults = opusenc_defaults,
.priv_class = &opusenc_class,
.priv_data_size = sizeof(OpusEncContext),
.init = opus_encode_init,
.encode2 = opus_encode_frame,
.close = opus_encode_end,
.caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_INIT_CLEANUP,
.capabilities = AV_CODEC_CAP_EXPERIMENTAL | AV_CODEC_CAP_SMALL_LAST_FRAME | AV_CODEC_CAP_DELAY,
.supported_samplerates = (const int []){ 48000, 0 },
.channel_layouts = (const uint64_t []){ AV_CH_LAYOUT_MONO,
AV_CH_LAYOUT_STEREO, 0 },
.sample_fmts = (const enum AVSampleFormat[]){ AV_SAMPLE_FMT_FLTP,
AV_SAMPLE_FMT_NONE },
};
最后定位内置的opusenc.c
(注意, 不是libopusenc.c
)设置的frame_size
是120帧
, opus采样率48000也就是2.5ms一帧,理论采样延时大概300ms
;从我测试的情况来看, 编码延时很高(400+ms
):
由于内置opusenc.c编码器延迟实在太大,显然不适合WebRTC低延时场景,开始有点不知所以,后来在朋友的指导下,发现FFmpeg
还有个libopus
编码器,于是决定使用libopus来编码,FFmpeg中libopus信息如下:
AVCodec ff_libopus_encoder = {
.name = "libopus",
.long_name = NULL_IF_CONFIG_SMALL("libopus Opus"),
.type = AVMEDIA_TYPE_AUDIO,
.id = AV_CODEC_ID_OPUS,
.priv_data_size = sizeof(LibopusEncContext),
.init = libopus_encode_init,
.encode2 = libopus_encode,
.close = libopus_encode_close,
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_SMALL_LAST_FRAME,
.sample_fmts = (const enum AVSampleFormat[]){ AV_SAMPLE_FMT_S16,
AV_SAMPLE_FMT_FLT,
AV_SAMPLE_FMT_NONE },
.supported_samplerates = libopus_sample_rates,
.priv_class = &libopus_class,
.defaults = libopus_defaults,
.wrapper_name = "libopus",
};
替换libopus
编码后,编码延时在40~50ms
内,效果符合预期的;然而在做了音视频同步后,Alexa
依然播放不出opus
语音,此时已经快到了黔驴技穷的边缘了。
3.4 趟坑之路四,分析Alexa日志
对于Alexa这个黑盒,我们极度缺乏调试手段,只能通过给Amazon提交工单,很遗憾,工单并未得到相应回复,通过内部关系找到Alexa中国区负责对接人,对方需要提供公司对接商务信息,才能予以支持,且需要走商务流程;没办法,只能抓取Android端Alexa智能这个APP日志看能否找到相应线索。
于是搭建Android adb环境,抓取com.amazon.dee.app:alexa
包日志:
# 查看进程号
adb shell ps
# 抓取日志, xxx为进程号
adb logcat xxx
不出意外,你将会得到一堆无用的日志信息:
3.5 趟坑之路五,柳暗花明
经过长时间的尝试,始终无法攻克这个问题,后续差点绝望到想放弃,实在没办法,于是只能在逐字逐句的查看文档,看看能不能得到点线索,看到这里:
终于顿悟!看描述本意是,Alexa
设备发起offer请求后,需要在6s内回复相应的Answer SDP
,然而最后实测发现这个6s是需要包含音视频数据的,如果6s内没有音视频数据发送,Alexa建立连接失败,但是 不会有任何提示,不会有任何提示,不会有任何提示!。
-
经过一番调整,终于完美播放:
四、总结
经过这番折腾,最后复盘下事情的来龙去脉,开始死活不通的原因如下:
亚马逊的服务器部署在海外,整个信令交互延时很高,大大降低了在6秒钟内完成交互的成功率,这也是一直失败的最大原因。
门铃设备的唤醒、控制延时较高,加大了整个链路的的延时。
门铃设备音频采集、编码、输出时间比视频晚几百毫秒,导致单视频成功率较高,但是复合流时成功率很低,从而产生音频数据是否有问题的误导,浪费很多时间花在排查音频切片(确保20ms一个包)、编码、时间戳等问题上。
Alexa设备是个封闭的黑盒设备,无法获取准确的失败原因;另外,其文档描述也不准确;这些坑必须一个一个趟出来,没有前人指导,很难注意到这些问题,而国内在这方面的实践较少,相关技术文章不多。
最后,感谢整个过程一直支持我的小伙伴们,感谢他们的悉心指导,遇事不要气馁,自己短期解决不了的问题,不要死磕牛角尖,尽量集思广益,从不同角度去尝试,最终你会发现,这可能根本就不是一个技术问题!