<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一種“在Android設備上,播放視頻時同時獲取實時音頻流”的有效方案

          共 13789字,需瀏覽 28分鐘

           ·

          2021-05-29 17:26

          ?

          ?

          這篇文章將會按照一般的需求開發(fā)流程,從需求、分析、開發(fā),到總結(jié),來給大家講解一種在 Android 設備上,播放視頻的同時,獲取實時音頻流的有效方案。

          一、需求

          在車載產(chǎn)品上,有這樣一種需求,比如我把我的 Android 設備通過 usb 線連接上車機,這時我希望我在我 Android 手機上的操作,能同步到車機大屏上進行顯示?,F(xiàn)在很多車機基本都是 Android 系統(tǒng)了,市場上也有類似 CarPlay、CarLife 這種專門做手機投屏的軟件了。不過呢,還有一部分的車子,他們的車機用的是 Linux 系統(tǒng),這時如何實現(xiàn) Android 設備和 linux 設備之間的屏幕信息同步呢?

          接下來的文章,我們只介紹其中的一種場景,就是我手機播放視頻的時候,視頻內(nèi)容和視頻的聲音,都同步到 linux 系統(tǒng)的車機上。而且這篇文章,我們只介紹音頻同步的內(nèi)容。

          二、分析

          兩個設備之間的音頻同步,那就是把一個設備中的音頻數(shù)據(jù)同步到另一個設備上,一方做為發(fā)送端,另一方做為接收端,發(fā)送端不停的發(fā)生音頻流,接收端接收到音頻流,進行實時的播放,即可實現(xiàn)我們想要的效果。

          說到設備之間的通信,相信很多同學會想到 tcp、udp 這些協(xié)議了。是的,考慮到 tcp 協(xié)議傳輸?shù)挠行蛐?,?udp 是無序的,我們傳輸?shù)囊纛l數(shù)據(jù)也是需要有序的,所有音頻數(shù)據(jù)的傳輸,我們采用 tcp 協(xié)議。

          接下來我們再了解下,在Android系統(tǒng)上,聲音的播放流程是怎樣的?這對我們?nèi)绾稳カ@取視頻播放時候的音頻流,很有幫助。

          我們先看下關于視頻的播放、錄音,Android給我們提供了哪些API?

          MediaRecorder

          接觸過Android錄像、錄音的同學,應該對MediaRecorder 這個API不會感到陌生。是的,在Android系統(tǒng)上,我們可以通過MediaRecorder API來很容易的實現(xiàn)錄像、錄音功能,下面是關于MediaRecorder 狀態(tài)圖,具體的使用,感興趣的可以查看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音頻系統(tǒng)的框架圖。

          從上面的音頻系統(tǒng)框架圖(看畫紅線的部分),我們可以知道,應用上調(diào)用 MediaPlayer、MediaRecorder 來播放、錄音,在 framewrok 層會調(diào)用到 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;

          三、實現(xiàn)

          前面分析了一通,我們的方案也比較明朗了,就是在 framework 層的 AudioTrack.cpp 文件中,通過 socket,把音頻流實時的發(fā)送出來。

          另一個就是接收端,不停的接收發(fā)送出來的socket數(shù)據(jù),這個 socket 數(shù)據(jù)就是實時的 pcm 流,接收方,在實時播放 pcm 流,就能實現(xiàn)音頻的實時同步了。

          關于視頻流,是如何實現(xiàn)同步的,大家也可以猜猜?

          1. AudioTrack.cpp 中的代碼實現(xiàn):

          #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);
                  //我們添加的代碼:把音頻流實時的發(fā)送出去
                 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 設備調(diào)試,如果是 linux 系統(tǒng),思路是同樣的。

          接收端的處理邏輯如下:

          1. 設置socket監(jiān)聽
          2. 循環(huán)監(jiān)聽socket端口數(shù)據(jù)
          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監(jiān)聽
           */

          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 數(shù)據(jù)
                              new Thread(new TcpServerThread(socket,mTcpListener)).start();
                          }
                      } catch (Exception e) {
                          Log.e("TcpServer""" + e.toString());
                      }
                  }
              }.start();
          }

          /**
           * 停止socket監(jiān)聽
           */

           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);
              }
          }

          四、總結(jié)

          剛開始接到這個開發(fā)需求,也是思考了良久才想到這個方案。也再次驗證了,熟悉了解 framework 層,可以給我們提供很多實現(xiàn)問題的思路。中間調(diào)試的時候,也是遇到了不少的問題。不過欣喜的是結(jié)果還不錯,最后都給跑通了。

          該方案,我在 Android 5.0 和 Android 7.0 上都運行測試通過,希望對大家有幫助。

          推薦閱讀:

          ?
          ?
          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩一级免费 | 日韩一区二区免费视频 | 有码一区二区三区四区 | 成人色色在线 | 中国成人一级电影 |