写点什么

一种“在 Android 设备上,播放视频的同时,获取实时音频流”的有效方案

用户头像
小驰嘻嘻
关注
发布于: 2021 年 05 月 15 日
一种“在Android 设备上,播放视频的同时,获取实时音频流”的有效方案

这篇文章将会按照一般的需求开发流程,从需求、分析、开发,到总结,来给大家讲解一种“在 Android 设备上,播放视频的同时,获取实时音频流”的有效方案

一、需求

在车载产品上,有这样一种需求,比如我把我的 Android 设备通过 usb 线连接上车机,这时我希望我在我 Android 手机上的操作,能同步到车机大屏上进行显示。

现在很多车机基本都是 Android 系统了,市场上也有类似 CarPlayCarLife 这种专门做手机投屏的软件了。不过呢,还有一部分的车子,他们的车机用的是 Linux 系统,这时如何实现 Android 设备和 linux 设备之间的屏幕信息同步呢?

百度Carlife、苹果Carplay


接下来的文章,我们只介绍其中的一种场景,就是我手机播放视频的时候,视频内容和视频的声音,都同步到 linux 系统的车机上。而且这篇文章,我们只介绍音频同步的部分。

二、分析

两个设备之间的音频同步,那就是把一个设备中的音频数据同步到另一个设备上,一方作为发送端,另一方作为接收端,发送端不停的发生音频流,接收端接收到音频流后,进行实时的播放,即可实现我们想要的效果。

说到设备之间的通信,相信很多同学会想到 tcp、udp 这些协议了。是的,考虑到 tcp 协议传输的有序性,而 udp 是无序的,我们传输的音频数据也是需要有序的,所有音频数据的传输,我们采用 tcp 协议。

接下来我们再了解下,在 Android 系统上,声音的播放流程是怎样的呢?这对我们如何去获取视频播放时候的音频流,很有帮助。

我们先看下关于视频的播放、录音,Android 都给我们提供了哪些 API?

MediaRecorder

接触过 Android 录像、录音的同学,应该对 MediaRecorder 这个 API 不会感到模式。是的,在 Android 系统上,我们可以通过 MediaRecorder API 来很容易的实现录像、录音功能,下面是关于 MediaRecorder 状态图,具体接口的使用,感兴趣的可以查看 Android 官方文档(https://developer.android.google.cn/guide/topics/media/mediarecorder?hl=zh_cn)。

Android MediaRecorder接口


MediaPlayer

另外,用于播放视频的,Android 为我们提供了 MediaPlayer 的接口(https://developer.android.google.cn/guide/topics/media/mediaplayer?hl=en)。


了解了上面的 2 个 API,我们再来看下 Android 音频系统的框架图。

Android音频系统框架


从上面的音频系统框架图(看画红线的部分),我们可以知道,应用上调用 MediaPlayer、MediaRecorder 来播放、录音,在 framewrok 层都会调用到 AudioTrack.cpp 这个文件。


那么回到我们这篇文章的重点,我们需要在播放视频的时候,把视频的音频流实时的截取出来。那截取音频流的这部分工作,就可以放在 AudioTrack.cpp 中进行处理。


我们来看下 AudioTrack.cpp 里面比较重要的方法:

// 播放视频时,播放的音频流会调用到AudioTrack.cpp的write方法ssize_t AudioTrack::write(const void* buffer, size_t userSize, bool blocking){    if (mTransfer != TRANSFER_SYNC) {        return INVALID_OPERATION;    }
if (isDirect()) { AutoMutex lock(mLock); int32_t flags = android_atomic_and( ~(CBLK_UNDERRUN | CBLK_LOOP_CYCLE | CBLK_LOOP_FINAL | CBLK_BUFFER_END), &mCblk->mFlags); if (flags & CBLK_INVALID) { return DEAD_OBJECT; } }
if (ssize_t(userSize) < 0 || (buffer == NULL && userSize != 0)) { // Sanity-check: user is most-likely passing an error code, and it would // make the return value ambiguous (actualSize vs error). ALOGE("AudioTrack::write(buffer=%p, size=%zu (%zd)", buffer, userSize, userSize); return BAD_VALUE; }
size_t written = 0; Buffer audioBuffer;
while (userSize >= mFrameSize) { audioBuffer.frameCount = userSize / mFrameSize;
status_t err = obtainBuffer(&audioBuffer, blocking ? &ClientProxy::kForever : &ClientProxy::kNonBlocking); if (err < 0) { if (written > 0) { break; } if (err == TIMED_OUT || err == -EINTR) { err = WOULD_BLOCK; } return ssize_t(err); }
size_t toWrite = audioBuffer.size; memcpy(audioBuffer.i8, buffer, toWrite);
mBuffer = malloc(toWrite); memcpy(mBuffer,buffer,toWrite); buffer = ((const char *) buffer) + toWrite; userSize -= toWrite; written += toWrite;
releaseBuffer(&audioBuffer); }
if (written > 0) { mFramesWritten += written / mFrameSize; } return written;}
复制代码

三、实现

前面分析了一通,我们的方案也比较明朗了,就是在 framework 层的 AudioTrack.cpp 文件中,通过 socket,把音频流实时的发送出来。另一个就是接收端,不停的接收发送出来的 socket 数据,这个 socket 数据就是实时的 pcm 流,接收方,在实时播放 pcm 流,就能实现音频的实时同步了。


关于视频流,是如何实现同步的,大家也可以猜猜?


1)AudioTrack.cpp 中的代码实现

#define DEST_PORT 5046#define DEST_IP_ADDRESS "192.168.7.6"
int mSocket;bool mSocketHasInit;bool mCurrentPlayMusicStream;struct sockaddr_in mRemoteAddr;
ssize_t AudioTrack::write(const void* buffer, size_t userSize, bool blocking){ ...... size_t toWrite = audioBuffer.size; memcpy(audioBuffer.i8, buffer, toWrite);
mBuffer = malloc(toWrite); memcpy(mBuffer,buffer,toWrite); //我们添加的代码:把音频流实时的发送出去 if(mCurrentPlayMusicStream && mSocketHasInit){ onSocketSendData(toWrite); } ......}
int AudioTrack::onSocketSendData(uint32_t len){ assert(NULL != mBuffer); assert(-1 != len);
if(!mSocketHasInit){ initTcpSocket(); }
unsigned int ret = send(mSocket, mBuffer,len, 0); free(mBuffer); return 0;}
复制代码


2) 接收端的代码处理

(我这里是用的 Android 设备调试,如果是 linux 系统,思路是同样的)


接收端的处理逻辑流程图如下:

1、设置 socket 监听;

2、循环监听 socket 端口数据;

3、接收到 pcm 流;

4、播放 pcm 流;

接收端处理流程


----------- PlayActivity.java ---------------------------------------
private ServerSocket mTcpServerSocket = null; private List<Socket> mSocketList = new ArrayList<>(); private MyTcpListener mTcpListener = null;
private boolean isAccept = true; /** * 设置socket监听 */ public void startTcpService() { Log.v(TAG,"startTcpService();"); if(mTcpListener == null){ mTcpListener = new MyTcpListener(); }
new Thread() { @Override public void run() { super.run(); try { mTcpServerSocket = new ServerSocket(); mTcpServerSocket.setReuseAddress(true); InetSocketAddress socketAddress = new InetSocketAddress(AndroidBoxProtocol.TCP_AUDIO_STREAM_PORT); mTcpServerSocket.bind(socketAddress);
while (isAccept) { Socket socket = mTcpServerSocket.accept(); mSocketList.add(socket); //开启新线程接收socket 数据 new Thread(new TcpServerThread(socket,mTcpListener)).start(); } } catch (Exception e) { Log.e("TcpServer", "" + e.toString()); } } }.start(); }/** * 停止socket监听 */private void stopTcpService(){
isAccept = false; if(mTcpServerSocket != null){ new Thread() { @Override public void run() { super.run(); try { for(Socket socket:mSocketList) { socket.close(); } mTcpServerSocket.close(); } catch (IOException e) { e.printStackTrace(); } } }.start(); } }
/** * 播放pcm 实时流 * @param buffer */ private void playPcmStream(byte[] buffer) { if (mAudioTrack != null && buffer != null) { mAudioTrack.play(); mAudioTrack.write(buffer, 0, buffer.length); } }
private Handler mUiHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case HANDLER_MSG_PLAY_PCM: playPcmStream((byte[]) msg.obj); break; default: break; } } };
private class MyTcpListener implements ITcpSocketListener{ @Override public void onRec(Socket socket, byte[] buffer) { sendHandlerMsg(HANDLER_MSG_PLAY_PCM,0,buffer); } }
复制代码

四、总结

刚开始接到这个开发需求,也是思考了良久才想到这个方案。也再次验证了,熟悉了解 framework 层,可以给我们提供很多实现问题的思路。中间调试的时候,也是遇到了不少的问题。不过欣喜的是结果还不错,最后都给跑通了。

该方案,我在 Android 5.0 和 Android 7.0 上都运行测试通过,希望对大家有帮助。

发布于: 2021 年 05 月 15 日阅读数: 459
用户头像

小驰嘻嘻

关注

公众号: 小驰笔记 2021.03.30 加入

14年开始从事Android Camera相关软件开发 做过车载、手机、执法记录仪~

评论 (1 条评论)

发布
用户头像
欢迎大家关注我的微信公众号“小驰笔记”,多多交流学习~
2021 年 05 月 18 日 12:16
回复
没有更多了
一种“在Android 设备上,播放视频的同时,获取实时音频流”的有效方案