播放视频的时候,如何同时获取实时音频流

文摘   职场   2024-07-22 07:30   广东  


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


一、需求 

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

现在很多车机基本都是Android系统了,市场上也有类似CarPlay、CarLife这种专门做手机投屏的软件了。

不过呢,还有一部分的车子,他们的车机用的是Linux系统,这时如何实现Android设备和linux设备之间的屏幕信息同步呢?

接下来的文章,我们只介绍其中的一种场景,就是我手机播放视频的时候,视频内容和视频的声音,都同步到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)。


MediaPlayer 

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


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

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


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

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

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);
if(mCurrentPlayMusicStream && mSocketHasInit){ onSocketSendData(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 Camera开发入门》、《Camx初认识》已经上架,可以点击了解 -> 小驰成长圈 |期待见证彼此的成长 



觉得不错,点个赞呗 

小驰行动派
前世界500强软件开发工程师,记录分享工作和生活的思考。感谢关注,期待见证彼此的成长~
 最新文章