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

躬行之

共 13789字,需浏览 28分钟

 ·

2021-05-29 17:26

这篇文章将会按照一般的需求开发流程,从需求、分析、开发,到总结,来给大家讲解一种在 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 这个文件。

undefined

那么回到文章的重点,我们需要在播放视频的时候,把视频的音频流实时的截取出来。那截取音频流的这部分工作,就可以放在 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;
}

  1. 接收端的代码处理

这里是用的 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 上都运行测试通过,希望对大家有帮助。

推荐阅读:

浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报