Sonic 的 v1.3.1-releasse 已经发布啦!其中有一个功能是远程音频传输,备受用户期待和好评,今天我们来揭开它的神秘面纱吧!
Sonic 官网
效果图:
为什么需要远程传输音频呢?这是因为 Sonic 云真机平台的用户还有涉及游戏和音视频方向的团队在使用,特别是某些音视频的测试需要听取设备的音频是否达标,是否出现在相应位置等等场景。游戏就不用说了,虽然现在 Sonic 可以横屏游戏,但是没有声音是缺少灵魂的。
最终需求就是,能够在 web 浏览器上听到远程真机的设备音频。
以往做远程音频传输,有两个方案。
获取 audiorecord 的开源项目有 sndcpy,他的处理方式比较粗暴,直接将 audiorecord 获取到的 pcm(16bit)音频流暴露给 pc 本地,然后 pc 本地用 vlc 软件进行播放。这种方式会有两个地方不太符合 Sonic 的需求。
在我们组织内部商量了之后,决定:
安卓端将 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
};
}
}
这种方式可以减少了数据传输大小,一帧压缩到了 500b,并提高了音频实时效率(实测延迟降低到 1 ~ 1.5s)
过程中还是踩到不少坑的。例如给压缩后的每帧数据加上 ACC 头,初始化解码器的回调出现粘包,解析数据后播放器无法播放等等。特别是数据处理的逻辑,搭配 Agent 的运行,绕过用户手动配具体权限。
我们成员接触过音视频经验的非常少,因此大家花了很长时间预研,试验,测试,都经历了一段时间的互相配合,可以说是不容易了。
就这样,远程音频就做好啦~
感谢这段时间大家对 Sonic 的支持,Sonic 会将继续沉淀做精品,感谢~