Android AVDemo(9):視頻封裝,采集編碼 H.264/H.265 并封裝 MP4丨音視頻工程示例

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時(shí)獲得最新的音視頻技術(shù)文章。

iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā),最絲滑的方式是對(duì)音視頻基礎(chǔ)概念知識(shí)有一定了解后,再借助 iOS/Android 平臺(tái)的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程,并借助音視頻工具來分析和理解對(duì)應(yīng)的音視頻數(shù)據(jù)。

塞尚《靜物》

音視頻工程示例這個(gè)欄目,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺(tái)上手音視頻開發(fā)。

這里是 Android 第九篇:Android 視頻封裝 Demo。這個(gè) Demo 里包含以下內(nèi)容:

  • 1)實(shí)現(xiàn)一個(gè)視頻采集模塊;
  • 2)實(shí)現(xiàn)一個(gè)視頻編碼模塊,支持 H.264/H.265;
  • 3)實(shí)現(xiàn)一個(gè)視頻封裝模塊;
  • 4)串聯(lián)視頻采集、編碼、封裝模塊,將采集到的視頻數(shù)據(jù)輸入給編碼模塊進(jìn)行編碼,再將編碼后的數(shù)據(jù)輸入給 MP4 封裝模塊封裝和存儲(chǔ);
  • 5)詳盡的代碼注釋,幫你理解代碼邏輯和原理。

在本文中,我們將詳解一下 Demo 的具體實(shí)現(xiàn)和源碼。讀完本文內(nèi)容相信就能幫你掌握相關(guān)知識(shí)。

不過,如果你的需求是:1)直接獲得全部工程源碼;2)想進(jìn)一步咨詢音視頻技術(shù)問題;3)咨詢音視頻職業(yè)發(fā)展問題??梢愿鶕?jù)自己的需要考慮是否加入『關(guān)鍵幀的音視頻開發(fā)圈』。

1、視頻采集模塊

在這個(gè) Demo 中,視頻采集模塊 KFVideoCapture 的實(shí)現(xiàn)與《Android 視頻采集 Demo》中一樣,這里就不再重復(fù)介紹了,其接口如下:

KFIVideoCapture.java

public interface KFIVideoCapture {
    ///< 視頻采集初始化。
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
    ///< 釋放采集實(shí)例。
    public void release();

    ///< 開始采集。
    public void startRunning();
    ///< 關(guān)閉采集。
    public void stopRunning();
    ///< 是否正在采集。
    public boolean isRunning();
    ///< 獲取 OpenGL 上下文。
    public EGLContext getEGLContext();
    ///< 切換攝像頭。
    public void switchCamera();
}

2、視頻編碼模塊

同樣的,視頻編碼模塊 KFByteBufferCodec、KFVideoSurfaceEncoder 的實(shí)現(xiàn)與《Android 視頻編碼 Demo》中一樣,這里就不再重復(fù)介紹了,其接口如下:

KFMediaCodecInterface.java

public interface KFMediaCodecInterface {
    public static final int KFMediaCodecInterfaceErrorCreate = -2000;
    public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
    public static final int KFMediaCodecInterfaceErrorStart = -2002;
    public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
    public static final int KFMediaCodecInterfaceErrorParams = -2004;

    public static int KFMediaCodeProcessParams = -1;
    public static int KFMediaCodeProcessAgainLater = -2;
    public static int KFMediaCodeProcessSuccess = 0;

    ///< 初始化 Codec,第一個(gè)參數(shù)需告知使用編碼還是解碼。
    public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
    ///< 釋放 Codec。
    public void release();

    ///< 獲取輸出格式描述。
    public MediaFormat getOutputMediaFormat();
    ///< 獲取輸入格式描述。
    public MediaFormat getInputMediaFormat();
    ///< 處理每一幀數(shù)據(jù),編碼前與編碼后都可以,支持編解碼 2 種模式。
    public int processFrame(KFFrame frame);
    ///< 清空 Codec 緩沖區(qū)。
    public void flush();
}

3、視頻封裝模塊

視頻編碼模塊即 KFMP4Muxer,復(fù)用了《Android 音頻封裝 Demo》中介紹的 muxer,這里就不再重復(fù)介紹了,其接口如下:

KFMP4Muxer.java

public class KFMP4Muxer {
    public KFMP4Muxer(KFMuxerConfig config, KFMuxerListener listener); ///< 根據(jù)配置與回調(diào)初始化。
    public void start(); ///< 開始。
    public void stop(); ///< 停止。
    public void setVideoMediaFormat(MediaFormat mediaFormat); ///< 設(shè)置視頻數(shù)據(jù)格式描述。
    public void setAudioMediaFormat(MediaFormat mediaFormat); ///< 設(shè)置音頻數(shù)據(jù)格式描述。
    public void writeSampleData(boolean isVideo, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo); ///< 寫入音視頻數(shù)據(jù)(編碼后數(shù)據(jù))。
    public void release(); ///< 釋放。
}

4、采集視頻數(shù)據(jù)進(jìn)行 H.264/H.265 編碼以及 MP4 封裝和存儲(chǔ)

我們還是在一個(gè) MainActivity 中來實(shí)現(xiàn)采集視頻數(shù)據(jù)進(jìn)行 H.264/H.265 編碼以及 MP4 封裝和存儲(chǔ)的邏輯。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private KFIVideoCapture mCapture; ///< 視頻采集。
    private KFVideoCaptureConfig mCaptureConfig; ///< 視頻采集配置。
    private KFRenderView mRenderView; ///< 渲染視圖。
    private KFGLContext mGLContext; ///< OpenGL 上下文。

    private KFVideoEncoderConfig mEncoderConfig; ///< 編碼配置。
    private KFMediaCodecInterface mEncoder; ///< 編碼。
    private KFMP4Muxer mMuxer; ///< 封裝器。
    private KFMuxerConfig mMuxerConfig; ///< 封裝配置。

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ///< 申請(qǐng)采集、存儲(chǔ)權(quán)限。
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        ///< 創(chuàng)建 GL 上下文。
        mGLContext = new KFGLContext(null);
        ///< 創(chuàng)建渲染視圖。
        mRenderView = new KFRenderView(this,mGLContext.getContext());

        WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
        Rect outRect = new Rect();
        windowManager.getDefaultDisplay().getRectSize(outRect);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
        addContentView(mRenderView,params);

        FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
        startParams.gravity = Gravity.CENTER_HORIZONTAL;
        Button startButton = new Button(this);
        startButton.setTextColor(Color.BLUE);
        startButton.setText("開始");
        startButton.setVisibility(View.VISIBLE);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mEncoder == null) {
                    mEncoder = new KFVideoSurfaceEncoder();
                    MediaFormat mediaFormat = KFAVTools.createVideoFormat(mEncoderConfig.isHEVC,mEncoderConfig.size, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,mEncoderConfig.bitrate,mEncoderConfig.fps,mEncoderConfig.gop / mEncoderConfig.fps,mEncoderConfig.profile,mEncoderConfig.profileLevel);
                    mEncoder.setup(true,mediaFormat,mVideoEncoderListener,mGLContext.getContext());
                    mMuxer = new KFMP4Muxer(mMuxerConfig,mMuxerListener);
                    mMuxer.start();
                    ((Button)view).setText("停止");
                } else {
                    mEncoder.release();
                    mEncoder = null;
                    mMuxer.stop();
                    mMuxer.release();
                    mMuxer = null;
                    ((Button)view).setText("開始");
                }
            }
        });
        addContentView(startButton, startParams);

        ///< 創(chuàng)建采集配置。
        mCaptureConfig = new KFVideoCaptureConfig();
        mCaptureConfig.cameraFacing = LENS_FACING_FRONT;
        mCaptureConfig.resolution = new Size(720,1280);
        mCaptureConfig.fps = 30;
        ///< 使用 Camera1 攝像頭還是 Camera2 攝像頭。
        boolean useCamera2 = false;
        if (useCamera2) {
            mCapture = new KFVideoCaptureV2();
        } else {
            mCapture = new KFVideoCaptureV1();
        }
        mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext());
        mCapture.startRunning();

        ///< 創(chuàng)建編碼配置 & 封裝配置。
        mEncoderConfig = new KFVideoEncoderConfig();
        mMuxerConfig = new KFMuxerConfig(Environment.getExternalStorageDirectory().getPath() + "/test.mp4");
    }

    private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() {
        @Override
        public void cameraOnOpened(){}

        @Override
        public void cameraOnClosed() {
        }

        @Override
        public void cameraOnError(int error,String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onFrameAvailable(KFFrame frame) {
            ///< 采集數(shù)據(jù)回調(diào)進(jìn)入渲染與編碼。
            mRenderView.render((KFTextureFrame) frame);
            if (mEncoder != null) {
                mEncoder.processFrame(frame);
            }
        }
    };

    private KFMediaCodecListener mVideoEncoderListener = new KFMediaCodecListener() {
        @Override
        public void onError(int error, String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void dataOnAvailable(KFFrame frame) {
            ///< 編碼回調(diào)數(shù)據(jù)進(jìn)入封裝器。
            if (mMuxer != null) {
                if ((((KFBufferFrame)frame).bufferInfo.flags & BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    mMuxer.setVideoMediaFormat(mEncoder.getOutputMediaFormat());
                } else {
                    mMuxer.writeSampleData(true,((KFBufferFrame)frame).buffer,((KFBufferFrame)frame).bufferInfo);
                }
            }
        }
    };

    private KFMuxerListener mMuxerListener = new KFMuxerListener() {
        @Override
        ///< 封裝器出錯(cuò)回調(diào)。
        public void muxerOnError(int error, String errorMsg) {
            Log.e("KFMuxer","error:" + error + "msg:" +errorMsg);
        }
    };
}

上面是 MainActivity 的實(shí)現(xiàn),其中主要包含這幾個(gè)部分:

  • 1)創(chuàng)建 OpenGL 上下文。
    • 創(chuàng)建上下文 mGLContext,這樣好處是采集與預(yù)覽可以共享,提高擴(kuò)展性。
  • 2)創(chuàng)建采集實(shí)例。
    • 這里需要注意的是,我們通過開關(guān) useCamera2 選擇 CameraCamera2
    • 參數(shù)配置 mCaptureConfig,可自定義攝像頭方向、幀率、分辨率。
  • 3)采集數(shù)據(jù)回調(diào)中獲取紋理數(shù)據(jù)輸入給渲染模塊與編碼模塊。
    • KFVideoCaptureListeneronFrameAvailable 回調(diào)中實(shí)現(xiàn)。
  • 4)在編碼模塊的數(shù)據(jù)回調(diào)中獲取編碼后的 H.264/H.265 數(shù)據(jù),并將數(shù)據(jù)交給封裝器 KFMP4Muxer 進(jìn)行封裝。
    • KFMediaCodecListenerdataOnAvailable 回調(diào)中實(shí)現(xiàn)。

5、用工具播放 MP4 文件

完成 Demo 后,可以將 sdcard 文件夾下面的 test.mp4 文件拷貝到電腦上,使用 ffplay播放來驗(yàn)證一下效果是否符合預(yù)期:

$ ffplay -I test.mp4

關(guān)于播放 MP4 文件的工具,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具《可視化音視頻分析工具》第 3.5 節(jié) VLC 播放器。

我們還可以用《可視化音視頻分析工具》第 3.1 節(jié) MP4Box.js 等工具來查看它的格式:

Demo 生成的 MP4 文件結(jié)構(gòu)

- 完 -

推薦閱讀

《Android AVDemo(8):視頻編碼》

《Android AVDemo(7):視頻采集》

《Android AVDemo(6):音頻渲染》

《Android AVDemo(5):音頻解碼》

《Android AVDemo(4):音頻解封裝》

《Android AVDemo(3):音頻封裝》

《Android AVDemo(2):音頻編碼》

《Android AVDemo(1):音頻采集》

《iOS AVDemo(7):視頻采集》

《iOS 音頻處理框架及重點(diǎn) API 合集》

《iOS AVDemo(6):音頻渲染》

《iOS AVDemo(5):音頻解碼》

《iOS AVDemo(4):音頻解封裝》

《iOS AVDemo(3):音頻封裝》

《iOS AVDemo(2):音頻編碼》

《iOS AVDemo(1):音頻采集》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容