文章

Android AVDemo(9):视频封装,代码开源并提供解析

介绍 Android 视频封装流程和原理,并提供 Demo 源码和解析。

Android AVDemo(9):视频封装,代码开源并提供解析

本文转自微信公众号 关键帧Keyframe,推荐您关注来获取音视频、AI 领域的最新技术和产品信息

微信公众号 微信扫码关注我们

您还可以加入知识星球 关键帧的音视频开发圈 来一起交流工作中的技术难题、职场经验

知识星球 微信扫码加入星球

iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染过程,并借助音视频实用工具来分析和理解对应的音视频数据。

音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。

这里是 Android 第九篇:Android 视频封装 Demo。这个 Demo 里包含以下内容:

  • 1)实现一个视频采集模块;
  • 2)实现一个视频编码模块,支持 H.264/H.265;
  • 3)实现一个视频封装模块;
  • 4)串联视频采集、编码、封装模块,将采集到的视频数据输入给编码模块进行编码,再将编码后的数据输入给 MP4 封装模块封装和存储;
  • 5)详尽的代码注释,帮你理解代码逻辑和原理。

在本文中,我们将详解一下 Demo 的具体实现和源码。读完本文内容相信就能帮你掌握相关知识。

不过,如果你的需求是:1)直接获得全部工程源码;2)想进一步咨询音视频技术问题;3)咨询音视频职业发展问题。可以根据自己的需要考虑是否加入『关键帧的音视频开发圈』。

长按识别二维码→加入我们 长按识别二维码→加入我们

1、视频采集模块

在这个 Demo 中,视频采集模块 KFVideoCapture 的实现与《Android 视频采集 Demo》中一样,这里就不再重复介绍了,其接口如下:

KFIVideoCapture.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface KFIVideoCapture {
    ///< 视频采集初始化。
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
    ///< 释放采集实例。
    public void release();

    ///< 开始采集。
    public void startRunning();
    ///< 关闭采集。
    public void stopRunning();
    ///< 是否正在采集。
    public boolean isRunning();
    ///< 获取 OpenGL 上下文。
    public EGLContext getEGLContext();
    ///< 切换摄像头。
    public void switchCamera();
}

2、视频编码模块

同样的,视频编码模块 KFByteBufferCodecKFVideoSurfaceEncoder 的实现与《Android 视频编码 Demo》中一样,这里就不再重复介绍了,其接口如下:

KFMediaCodecInterface.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,第一个参数需告知使用编码还是解码。
    public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
    ///< 释放 Codec。
    public void release();

    ///< 获取输出格式描述。
    public MediaFormat getOutputMediaFormat();
    ///< 获取输入格式描述。
    public MediaFormat getInputMediaFormat();
    ///< 处理每一帧数据,编码前与编码后都可以,支持编解码 2 种模式。
    public int processFrame(KFFrame frame);
    ///< 清空 Codec 缓冲区。
    public void flush();
}

3、视频封装模块

视频编码模块即 KFMP4Muxer,复用了《Android 音频封装 Demo》中介绍的 muxer,这里就不再重复介绍了,其接口如下:

KFMP4Muxer.java

1
2
3
4
5
6
7
8
9
public class KFMP4Muxer {
    public KFMP4Muxer(KFMuxerConfig config, KFMuxerListener listener); ///< 根据配置与回调初始化。
    public void start(); ///< 开始。
    public void stop(); ///< 停止。
    public void setVideoMediaFormat(MediaFormat mediaFormat); ///< 设置视频数据格式描述。
    public void setAudioMediaFormat(MediaFormat mediaFormat); ///< 设置音频数据格式描述。
    public void writeSampleData(boolean isVideo, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo); ///< 写入音视频数据(编码后数据)。
    public void release(); ///< 释放。
}

4、采集视频数据进行 H.264/H.265 编码以及 MP4 封装和存储

我们还是在一个 MainActivity 中来实现采集视频数据进行 H.264/H.265 编码以及 MP4 封装和存储的逻辑。

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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);

        ///< 申请采集、存储权限。
        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);
        }

        ///< 创建 GL 上下文。
        mGLContext = new KFGLContext(null);
        ///< 创建渲染视图。
        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);

        ///< 创建采集配置。
        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();

        ///< 创建编码配置 & 封装配置。
        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) {
            ///< 采集数据回调进入渲染与编码。
            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) {
            ///< 编码回调数据进入封装器。
            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
        ///< 封装器出错回调。
        public void muxerOnError(int error, String errorMsg) {
            Log.e("KFMuxer","error:" + error + "msg:" +errorMsg);
        }
    };
}

上面是 MainActivity 的实现,其中主要包含这几个部分:

  • 1)创建 OpenGL 上下文。
    • 创建上下文 mGLContext,这样好处是采集与预览可以共享,提高扩展性。
  • 2)创建采集实例。
    • 这里需要注意的是,我们通过开关 useCamera2 选择 CameraCamera2
    • 参数配置 mCaptureConfig,可自定义摄像头方向、帧率、分辨率。
  • 3)采集数据回调中获取纹理数据输入给渲染模块与编码模块。
    • KFVideoCaptureListeneronFrameAvailable 回调中实现。
  • 4)在编码模块的数据回调中获取编码后的 H.264/H.265 数据,并将数据交给封装器 KFMP4Muxer 进行封装。
    • KFMediaCodecListenerdataOnAvailable 回调中实现。

5、用工具播放 MP4 文件

完成 Demo 后,可以将 sdcard 文件夹下面的 test.mp4 文件拷贝到电脑上,使用 ffplay 播放来验证一下效果是否符合预期:

1
$ ffplay -i test.mp4

关于播放 MP4 文件的工具,可以参考《FFmpeg 工具》第 2 节 ffplay 命令行工具《可视化音视频分析工具》第 3.5 节 VLC 播放器

我们还可以用《可视化音视频分析工具》第 3.1 节 MP4Box.js 等工具来查看它的格式:

Demo 生成的 MP4 文件结构 Demo 生成的 MP4 文件结构

本文由作者按照 CC BY-NC-ND 4.0 进行授权