揭秘Sonic云真机远程音频传输功能

581 阅读3分钟

Sonic的v1.3.1-releasse已经发布啦!其中有一个功能是远程音频传输,备受用户期待和好评,今天我们来揭开它的神秘面纱吧!
效果图:

背景

为什么需要远程传输音频呢?这是因为Sonic云真机平台的用户还有涉及游戏和音视频方向的团队在使用,特别是某些音视频的测试需要听取设备的音频是否达标,是否出现在相应位置等等场景。游戏就不用说了,虽然现在Sonic可以横屏游戏,但是没有声音是缺少灵魂的。
最终需求就是,能够在web浏览器上听到远程真机的设备音频。

方案选取

以往做远程音频传输,有两个方案。

  1. app开启麦克风权限,通过麦克风录制设备音频发送到后台处理。听着好像不错,但是你想想看群控的时候,基本机架上的手机都在进行测试、远控。如果开启麦克风,会把其他设备的杂音一并录制进去,体验非常不好。
  2. app获取安卓的audiorecord接口,直接获取设备内置声卡的音频。但是兼容性不太好,只能兼容安卓10或以上。
    综合考虑了一下,毕竟低端机很少用于音视频测试,于是选了方案二就准备开工了。

具体实现

获取audiorecord的开源项目有 sndcpy,他的处理方式比较粗暴,直接将audiorecord获取到的pcm(16bit)音频流暴露给pc本地,然后pc本地用vlc软件进行播放。这种方式会有两个地方不太符合Sonic的需求。

  1. 用户需要额外安装vlc在pc本地,这肯定是增加了门槛。需要用前端播放器进行播放。
  2. pcm裸流数据量会偏大,vlc解析之后延迟会达到约2s

在我们组织内部商量了之后,决定:

  1. 安卓端将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);
                    }
                });
    
  2. 然后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);
                            }
                        }
    
  3. 前端使用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
    		};
           }
    }
    

    这种方式可以减少了数据传输大小,一帧压缩到了500b,并提高了音频实时效率(实测延迟降低到1 ~ 1.5s)

踩坑感受

过程中还是踩到不少坑的。例如给压缩后的每帧数据加上ACC头,初始化解码器的回调出现粘包,解析数据后播放器无法播放等等。特别是数据处理的逻辑,搭配Agent的运行,绕过用户手动配具体权限。
我们成员接触过音视频经验的非常少,因此大家花了很长时间预研,试验,测试,都经历了一段时间的互相配合,可以说是不容易了。

结语

就这样,远程音频就做好啦~
感谢这段时间大家对Sonic的支持,Sonic会将继续沉淀做精品,感谢~