Sonic的v1.3.1-releasse已经发布啦!其中有一个功能是远程音频传输,备受用户期待和好评,今天我们来揭开它的神秘面纱吧!
效果图:
背景
为什么需要远程传输音频呢?这是因为Sonic云真机平台的用户还有涉及游戏和音视频方向的团队在使用,特别是某些音视频的测试需要听取设备的音频是否达标,是否出现在相应位置等等场景。游戏就不用说了,虽然现在Sonic可以横屏游戏,但是没有声音是缺少灵魂的。
最终需求就是,能够在web浏览器上听到远程真机的设备音频。
方案选取
以往做远程音频传输,有两个方案。
- app开启麦克风权限,通过麦克风录制设备音频发送到后台处理。听着好像不错,但是你想想看群控的时候,基本机架上的手机都在进行测试、远控。如果开启麦克风,会把其他设备的杂音一并录制进去,体验非常不好。
- app获取安卓的audiorecord接口,直接获取设备内置声卡的音频。但是兼容性不太好,只能兼容安卓10或以上。
综合考虑了一下,毕竟低端机很少用于音视频测试,于是选了方案二就准备开工了。
具体实现
获取audiorecord的开源项目有 sndcpy,他的处理方式比较粗暴,直接将audiorecord获取到的pcm(16bit)音频流暴露给pc本地,然后pc本地用vlc软件进行播放。这种方式会有两个地方不太符合Sonic的需求。
- 用户需要额外安装vlc在pc本地,这肯定是增加了门槛。需要用前端播放器进行播放。
- pcm裸流数据量会偏大,vlc解析之后延迟会达到约2s
在我们组织内部商量了之后,决定:
-
安卓端将pcm流实时压缩成ACC格式,通过localserversocket的方式传递给Agent端。
mMediaCodec.setCallback(new MediaCodec.Callback() { @Override public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int i) { ByteBuffer codecInputBuffer = mediaCodec.getInputBuffer(i); int capacity = codecInputBuffer.capacity(); byte[] buffer = new byte[capacity]; int readBytes = audioRecord.read(buffer, 0, buffer.length); if (readBytes > 0) { codecInputBuffer.put(buffer, 0, readBytes); mediaCodec.queueInputBuffer(i, 0, readBytes, mPresentationTime[0], 0); totalBytesRead[0] += readBytes; mPresentationTime[0] = 1000000L * (totalBytesRead[0] / 2) / 44100; } } @Override public void onOutputBufferAvailable(@NonNull MediaCodec codec, int outputBufferIndex, @NonNull MediaCodec.BufferInfo mBufferInfo) { if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { Logger.i("AudioService", "AAC的配置数据"); } else { byte[] oneADTSFrameBytes = new byte[7 + mBufferInfo.size]; ADTSUtil.addADTS(oneADTSFrameBytes); ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex); outputBuffer.get(oneADTSFrameBytes, 7, mBufferInfo.size); if (outputStream!=null){ try { outputStream.write(oneADTSFrameBytes,0,oneADTSFrameBytes.length); outputStream.flush(); } catch (IOException e) { stopSelf(); e.printStackTrace(); } } } codec.releaseOutputBuffer(outputBufferIndex, false); } });
-
然后Agent端通过websocket发送给前端解析。
audioSocket = new Socket("localhost", appListPort); inputStream = audioSocket.getInputStream(); int len = 1024; while (audioSocket.isConnected() && !Thread.interrupted()) { byte[] buffer = new byte[len]; int realLen; realLen = inputStream.read(buffer); if (buffer.length != realLen && realLen >= 0) { buffer = AgentTool.subByteArray(buffer, 0, realLen); } if (realLen >= 0) { ByteBuffer byteBuffer = ByteBuffer.allocate(buffer.length); byteBuffer.put(buffer); byteBuffer.flip(); AgentTool.sendByte(session, byteBuffer); } }
-
前端使用jmuxer进行音频解析并播放。
initWebSocket(url) { const that = this this.ws = new Socket({ url, binaryType: 'arraybuffer', isErrorReconnect: false, onmessage: function(event) { var data = that.parse(event.data); data && that.jmuxer.feed(data); } }); } /** * 音频解析 * @param {*} data AAC Buffer 视频流 * @returns */ parse(data) { let input = new Uint8Array(data) return { audio: input }; } }
踩坑感受
过程中还是踩到不少坑的。例如给压缩后的每帧数据加上ACC头,初始化解码器的回调出现粘包,解析数据后播放器无法播放等等。特别是数据处理的逻辑,搭配Agent的运行,绕过用户手动配具体权限。
我们成员接触过音视频经验的非常少,因此大家花了很长时间预研,试验,测试,都经历了一段时间的互相配合,可以说是不容易了。
结语
就这样,远程音频就做好啦~
感谢这段时间大家对Sonic的支持,Sonic会将继续沉淀做精品,感谢~