一種“在Android設備上,播放視頻時同時獲取實時音頻流”的有效方案
這篇文章將會按照一般的需求開發(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)同步的,大家也可以猜猜?
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;
}
接收端的代碼處理
這里是用的 Android 設備調(diào)試,如果是 linux 系統(tǒng),思路是同樣的。
接收端的處理邏輯如下:
設置socket監(jiān)聽 循環(huán)監(jiān)聽socket端口數(shù)據(jù) 接收到pcm流 播放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 上都運行測試通過,希望對大家有幫助。
推薦閱讀:
