EasyPusher實現安卓Android手機直播推送同步錄像功能(源碼解析)

EasyPusher是一款非常棒的推送客戶端。穩定、高效、低延遲,音視頻同步等都特別好。裝在安卓上可作爲一款單兵設備來用。說到單兵,在項目中通常都需要邊傳邊錄的功能,因此後來EasyPusher也加入了該特性。該文章將結合代碼來闡述下這個功能是如何實現的。

EasyPusher在設置裏增加了相關選項來方便開啓和關閉本地錄像功能,如下圖所示,在設置裏勾選後就可以推送的同時進行錄像了。

EasyPusher的本地錄像功能

EasyPusher用來實現錄像的類叫做EasyMuxer,該類對安卓系統的MediaMuxer進行了一些封裝,專門對從MediaCodec編碼出來的音視頻數據進行錄像,同時實現了錄像自動分段存儲的功能。其構造函數如下:

public EasyMuxer(String path, long durationMillis) {
        mFilePath = path;
        this.durationMillis = durationMillis;
        Object mux = null;
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                mux = new MediaMuxer(path + "-" + index++ + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            mMuxer = (MediaMuxer) mux;
        }
    }

構造函數有兩個參數,第一個參數爲本次錄像的文件路徑;第二個參數爲單個文件最大時長,用來做錄像分段(由於MediaMuxerJELLY_BEAN_MR2以上才加入,因此這裏做了判斷,低於該版本的系統直接忽略了)。函數內部創建了一個MediaMuxer對象,用來實現真正的錄像存儲功能。

MediaMuxer在錄像之前需要加入Video Track和Audio Track,EasyMuxuer進行了如下封裝。

    public synchronized void addTrack(MediaFormat format, boolean isVideo) {
        // now that we have the Magic Goodies, start the muxer
        if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1)
            throw new RuntimeException("already add all tracks");

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            int track = mMuxer.addTrack(format);
            if (VERBOSE)
                Log.i(TAG, String.format("addTrack %s result %d", isVideo ? "video" : "audio", track));
            if (isVideo) {
                mVideoFormat = format;
                mVideoTrackIndex = track;
                if (mAudioTrackIndex != -1) {
                    if (VERBOSE)
                        Log.i(TAG, "both audio and video added,and muxer is started");
                    mMuxer.start();
                    mBeginMillis = System.currentTimeMillis();
                }
            } else {
                mAudioFormat = format;
                mAudioTrackIndex = track;
                if (mVideoTrackIndex != -1) {
                    mMuxer.start();
                    mBeginMillis = System.currentTimeMillis();
                }
            }
        }
    }

這個函數有兩個參數,第一個爲要加入的媒體流的MediaFormat,可通過MediaCodec獲取到,第二個表示加入的是Video還是Audio。從代碼可以看到,在音頻和視頻都添加成功後會自動啓動錄像,這樣做的好處是應用層的音視頻的處理完全可以獨立,不用操心錄像的事情,減少了代碼耦合性。同時,錄像啓動的時候會記錄下來開始時間,後面需要用這個開始時間來計算文件時長。

錄像啓動後,接下來就應該保存數據了。我們再繼續看下面的函數封裝。


    public synchronized void pumpStream(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo, boolean isVideo) {
        if (mAudioTrackIndex == -1 || mVideoTrackIndex == -1) {
            Log.i(TAG, String.format("pumpStream [%s] but muxer is not start.ignore..", isVideo ? "video" : "audio"));
            return;
        }
        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
            // The codec config data was pulled out and fed to the muxer when we got
            // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
        } else if (bufferInfo.size != 0) {
            if (isVideo && mVideoTrackIndex == -1) {
                throw new RuntimeException("muxer hasn't started");
            }

            // adjust the ByteBuffer values to match BufferInfo (not needed?)
            outputBuffer.position(bufferInfo.offset);
            outputBuffer.limit(bufferInfo.offset + bufferInfo.size);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                mMuxer.writeSampleData(isVideo ? mVideoTrackIndex : mAudioTrackIndex, outputBuffer, bufferInfo);
            }
            if (VERBOSE)
                Log.d(TAG, String.format("sent %s [" + bufferInfo.size + "] with timestamp:[%d] to muxer", isVideo ? "video" : "audio", bufferInfo.presentationTimeUs / 1000));
        }

        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            if (VERBOSE)
                Log.i(TAG, "BUFFER_FLAG_END_OF_STREAM received");
        }

        if (System.currentTimeMillis() - mBeginMillis >= durationMillis) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                if (VERBOSE)
                    Log.i(TAG, String.format("record file reach expiration.create new file:" + index));
                mMuxer.stop();
                mMuxer.release();
                mMuxer = null;
                mVideoTrackIndex = mAudioTrackIndex = -1;
                try {
                    mMuxer = new MediaMuxer(mFilePath + "-" + index++ + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
                    addTrack(mVideoFormat, true);
                    addTrack(mAudioFormat, false);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

需要注意的是音視頻的處理分別是在不同的線程中進行的,因此上面這兩個函數都是需要加鎖的。

pumpStream方法會把從MediaCodec得到的音視頻數據,保存到錄像文件中。第一個參數表示媒體數據;第二個參數表示幀格式信息;第三個參數表示是音頻還是視頻。我們之前說到,音視頻可能在不同的線程進行採集的,因此可能會有不同步的情況,比如音頻已經開始了,視頻還沒出來。那這時候的音頻數據,實際上是在錄像開始之前pump,那它就不會保存下來;當錄像開始後,muxer會通過writeSampleData方法來寫入媒體數據。
同時,這個函數會判斷是否已經達到了最大文件時長,是的話,會更換文件再存儲。這裏先把muxer關閉,再創建新的,並使用之前的MediaFormat來加入音視頻軌道信息。爲什麼可以這樣?因爲這裏音視頻的媒體信息沒有發生更改,所以完全是可以複用的。

最後,錄像結束後,需要調用release來釋放資源並生產最後一個完整的MP4文件。

    public synchronized void release() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            if (mMuxer != null) {
                if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1) {
                    if (VERBOSE)
                        Log.i(TAG, String.format("muxer is started. now it will be stoped."));
                    try {
                        mMuxer.stop();
                        mMuxer.release();
                    } catch (IllegalStateException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

release函數很簡單,無需再介紹了。
結合音視頻線程,函數調用的流程圖如下:

Created with Raphaël 2.1.0視頻視頻MuxerMuxer音頻音頻創建EasyMuxer將EasyMuxer傳遞給音頻獲取到了MediaFormatAddTrack獲取到了MediaFormatAddTrack啓動錄像pumpStreampumpStream更換文件線程結束線程結束Release

經測試,EasyMuxer非常穩定、好用。一個小時的測試,基本沒有出現錄像丟失和文件損壞的情況。詳細代碼參見Github。

項目地址:
https://github.com/EasyDarwin/EasyPusher_Android

APP下載:
https://fir.im/EasyPusher

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章