文章

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

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

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

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

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

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

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

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

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

这里是 Android 第十二篇:Android 视频解码 Demo。这个 Demo 里包含以下内容:

  • 1)实现一个视频解封装模块;
  • 2)实现两个视频解码模块 ByteBufferSurface
  • 3)串联视频解封装和解码模块,将解封装的 H.264/H.265 数据输入给解码模块进行解码,并存储解码后的 YUV 数据与纹理数据渲染;
  • 4)详尽的代码注释,帮你理解代码逻辑和原理。

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

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

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

1、视频解封装模块

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

KFMP4Demuxer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class KFMP4Demuxer {
    public KFMP4Demuxer(KFDemuxerConfig config, KFDemuxerListener listener); ///< 构造方法:配置 & 回调。
    public void release(); ///< 释放解封装器实例。
    public boolean hasVideo(); ///< 是否包含视频。
    public boolean hasAudio(); ///< 是否包含音频。
    public int duration(); ///< 文件时长。
    public int rotation(); ///< 视频旋转角度。
    public boolean isHEVC(); ///< 是否为 H265。
    public int width(); ///< 视频宽度。
    public int height(); ///< 视频高度。
    public int samplerate(); ///< 音频采样率。
    public int channel(); ///< 音频声道数。
    public int audioProfile(); ///< 音频 profile。
    public int videoProfile(); ///< 视频 profile。
    public MediaFormat audioMediaFormat(); ///< 音频格式描述。
    public MediaFormat videoMediaFormat(); //< 视频格式描述。
    public ByteBuffer readAudioSampleData(MediaCodec.BufferInfo bufferInfo); ///< 读取音频帧。
    public ByteBuffer readVideoSampleData(MediaCodec.BufferInfo bufferInfo); ///< 读取视频帧。
}

2、视频 ByteBuffer 解码模块

接下来,我们来实现一个视频解码模块 KFByteBufferCodec,解码模块 KFByteBufferCodec 的实现与 《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();
}

上面是 KFByteBufferCodec 接口的设计,与视频编码对比区别如下:

  • 1)外层使用构造方法时配置参数修改:
    • setup 接口 mInputMediaFormat 需要设置视频解码的格式描述,isEncoder 设置为解码 false

3、视频 Surface 解码模块

接下来,我们来实现一个视频解码模块 KFVideoSurfaceDecoder,在这里输入解封装后的编码数据,输出解码后的数据,同样也需要实现接口 KFMediaCodecInterface,参考模块 KFByteBufferCodec

KFVideoSurfaceDecoder.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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
public class KFVideoSurfaceDecoder implements  KFMediaCodecInterface {
    private static final String TAG = "KFVideoSurfaceDecoder";
    private KFMediaCodecListener mListener = null; ///< 回调。
    private MediaCodec mDecoder = null; ///< 解码器。
    private ByteBuffer[] mInputBuffers; ///< 解码器输入缓存。
    private MediaFormat mInputMediaFormat = null; ///< 输入格式描述。
    private MediaFormat mOutMediaFormat = null; ///< 输出格式描述。
    private KFGLContext mEGLContext = null; ///< OpenGL 上下文。
    private KFSurfaceTexture mSurfaceTexture = null; ///< 纹理缓存。
    private Surface mSurface = null; ///< 纹理缓存,对应 Surface。
    private KFGLFilter mOESConvert2DFilter; ///< 特效。

    private long mLastInputPts = 0; ///< 输入数据最后一帧时间戳。
    private List<KFBufferFrame> mList = new ArrayList<>();
    private ReentrantLock mListLock = new ReentrantLock(true);

    private HandlerThread mDecoderThread = null; ///< 解码线程。
    private Handler mDecoderHandler = null;
    private HandlerThread mRenderThread = null; ///< 渲染线程。
    private Handler mRenderHandler = null;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主线程。

    public KFVideoSurfaceDecoder() {

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void setup(boolean isEncoder,MediaFormat mediaFormat,KFMediaCodecListener listener, EGLContext eglShareContext) {
        mInputMediaFormat = mediaFormat;
        mListener = listener;

        ///< 创建解码线程。
        mDecoderThread = new HandlerThread("KFVideoSurfaceDecoderThread");
        mDecoderThread.start();
        mDecoderHandler = new Handler((mDecoderThread.getLooper()));

        ///< 创建渲染线程。
        mRenderThread = new HandlerThread("KFVideoSurfaceRenderThread");
        mRenderThread.start();
        mRenderHandler = new Handler((mRenderThread.getLooper()));

        mDecoderHandler.post(()->{
            if (mInputMediaFormat == null) {
                _callBackError(KFMediaCodecInterfaceErrorParams,"mInputMediaFormat null");
                return;
            }

            ///< 创建 OpenGL 上下文、纹理缓存、纹理缓存 Surface、OES 转 2D 数据。
            mEGLContext = new KFGLContext(eglShareContext);
            mEGLContext.bind();
            mSurfaceTexture = new KFSurfaceTexture(mSurfaceTextureListener);
            mSurfaceTexture.getSurfaceTexture().setDefaultBufferSize(mInputMediaFormat.getInteger(MediaFormat.KEY_WIDTH),mInputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
            mSurface = new Surface(mSurfaceTexture.getSurfaceTexture());
            mOESConvert2DFilter = new KFGLFilter(false, KFGLBase.defaultVertexShader,KFGLBase.oesFragmentShader);
            mEGLContext.unbind();

            _setupDecoder();
        });
    }

    @Override
    public MediaFormat getOutputMediaFormat() {
        return mOutMediaFormat;
    }

    @Override
    public MediaFormat getInputMediaFormat() {
        return mInputMediaFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void release() {
        mDecoderHandler.post(()-> {
            ///< 释放解码器、GL 上下文、数据缓存、SurfaceTexture。
            if (mDecoder != null) {
                try {
                    mDecoder.stop();
                    mDecoder.release();
                } catch (Exception e) {
                    Log.e(TAG, "release: " + e.toString());
                }
                mDecoder = null;
            }

            mEGLContext.bind();
            if (mSurfaceTexture != null) {
                mSurfaceTexture.release();
                mSurfaceTexture = null;
            }
            if (mSurface != null) {
                mSurface.release();
                mSurface = null;
            }
            if (mOESConvert2DFilter != null) {
                mOESConvert2DFilter.release();
                mOESConvert2DFilter = null;
            }
            mEGLContext.unbind();

            if (mEGLContext != null) {
                mEGLContext.release();
                mEGLContext = null;
            }

            mListLock.lock();
            mList.clear();
            mListLock.unlock();

            mDecoderThread.quit();
            mRenderThread.quit();
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void flush() {
        mDecoderHandler.post(()-> {
            ///< 刷新解码器缓冲区。
            if (mDecoder == null) {
                return;
            }

            try {
                mDecoder.flush();
            } catch (Exception e) {
                Log.e(TAG, "flush" + e);
            }

            mListLock.lock();
            mList.clear();
            mListLock.unlock();
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public int processFrame(KFFrame inputFrame) {
        if (inputFrame == null) {
            return KFMediaCodeProcessParams;
        }

        KFBufferFrame frame = (KFBufferFrame)inputFrame;
        if (frame.buffer ==null || frame.bufferInfo == null || frame.bufferInfo.size == 0) {
            return KFMediaCodeProcessParams;
        }

        ///< 外层数据进入缓存。
        _appendFrame(frame);

        mDecoderHandler.post(()-> {
            if (mDecoder == null) {
                return;
            }

            ///< 缓存获取数据,尽量多的输入给解码器。
            mListLock.lock();
            int mListSize = mList.size();
            mListLock.unlock();
            while (mListSize > 0) {
                mListLock.lock();
                KFBufferFrame packet = mList.get(0);
                mListLock.unlock();

                int bufferIndex;
                try {
                    ///< 获取解码器输入缓存下标。
                    bufferIndex = mDecoder.dequeueInputBuffer(10 * 1000);
                } catch (Exception e) {
                    Log.e(TAG, "dequeueInputBuffer" + e);
                    return;
                }

                if (bufferIndex >= 0) {
                    ///< 填充数据。
                    mInputBuffers[bufferIndex].clear();
                    mInputBuffers[bufferIndex].put(packet.buffer);
                    mInputBuffers[bufferIndex].flip();
                    try {
                        ///< 数据塞入解码器。
                        mDecoder.queueInputBuffer(bufferIndex, 0, packet.bufferInfo.size, packet.bufferInfo.presentationTimeUs, packet.bufferInfo.flags);
                    } catch (Exception e) {
                        Log.e(TAG, "queueInputBuffer" + e);
                        return;
                    }

                    mLastInputPts = packet.bufferInfo.presentationTimeUs;
                    mListLock.lock();
                    mList.remove(0);
                    mListSize = mList.size();
                    mListLock.unlock();
                } else {
                    break;
                }
            }

            ///< 从解码器拉取尽量多的数据出来。
            long outputDts = -1;
            MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo();
            while (outputDts < mLastInputPts) {
                int bufferIndex;
                try {
                    ///< 获取解码器输出缓存下标。
                    bufferIndex = mDecoder.dequeueOutputBuffer(outputBufferInfo, 10 * 1000);
                } catch (Exception e) {
                    Log.e(TAG, "dequeueOutputBuffer" + e);
                    return;
                }

                if (bufferIndex >= 0) {
                    ///< 释放缓存,第二个参数必须设置位 true,这样数据刷新到指定 surface。
                    mDecoder.releaseOutputBuffer(bufferIndex,true);
                } else {
                    if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        mOutMediaFormat = mDecoder.getOutputFormat();
                    }
                    break;
                }
            }
        });

        return KFMediaCodeProcessSuccess;
    }


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _appendFrame(KFBufferFrame frame) {
        ///< 添加数据到缓存 List。
        KFBufferFrame packet = new KFBufferFrame();

        ByteBuffer newBuffer = ByteBuffer.allocateDirect(frame.bufferInfo.size);
        newBuffer.put(frame.buffer).position(0);
        MediaCodec.BufferInfo newInfo = new MediaCodec.BufferInfo();
        newInfo.size = frame.bufferInfo.size;
        newInfo.flags = frame.bufferInfo.flags;
        newInfo.presentationTimeUs = frame.bufferInfo.presentationTimeUs;
        packet.buffer = newBuffer;
        packet.bufferInfo = newInfo;

        mListLock.lock();
        mList.add(packet);
        mListLock.unlock();
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private boolean _setupDecoder() {
        ///< 初始化解码器。
        try {
            ///< 根据输入格式描述创建解码器。
            String mimetype = mInputMediaFormat.getString(MediaFormat.KEY_MIME);
            mDecoder = MediaCodec.createDecoderByType(mimetype);
        } catch (Exception e) {
            Log.e(TAG, "createDecoderByType" + e);
            _callBackError(KFMediaCodecInterfaceErrorCreate,e.getMessage());
            return false;
        }

        try {
            ///< 配置位 Surface 解码模式。
            mDecoder.configure(mInputMediaFormat, mSurface, null, 0);
        } catch (Exception e) {
            Log.e(TAG, "configure" + e);
            _callBackError(KFMediaCodecInterfaceErrorConfigure,e.getMessage());
            return false;
        }

        try {
            ///< 启动解码器。
            mDecoder.start();
            ///< 获取解码器输入缓存。
            mInputBuffers = mDecoder.getInputBuffers();
        } catch (Exception e) {
            Log.e(TAG, "start" +  e );
            _callBackError(KFMediaCodecInterfaceErrorStart,e.getMessage());
            return false;
        }

        return true;
    }

    private void _callBackError(int error, String errorMsg) {
        ///< 错误回调。
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.onError(error,TAG + errorMsg);
            });
        }
    }

    private KFSurfaceTextureListener mSurfaceTextureListener = new KFSurfaceTextureListener() {
        ///< SurfaceTexture 数据回调。
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            mRenderHandler.post(() -> {
                mEGLContext.bind();
                mSurfaceTexture.getSurfaceTexture().updateTexImage();
                if (mListener != null) {
                    int width = mInputMediaFormat.getInteger(MediaFormat.KEY_WIDTH);
                    int height = mInputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
                    int rotation = (mInputMediaFormat.getInteger(MediaFormat.KEY_ROTATION) + 360) % 360;
                    int rotationWidth = (rotation % 360 == 90 || rotation % 360 == 270) ? height : width;
                    int rotationHeight = (rotation % 360 == 90 || rotation % 360 == 270) ? width : height;
                    KFTextureFrame frame = new KFTextureFrame(mSurfaceTexture.getSurfaceTextureId(),new Size(rotationWidth,rotationHeight),mSurfaceTexture.getSurfaceTexture().getTimestamp() * 1000,true);
                    mSurfaceTexture.getSurfaceTexture().getTransformMatrix(frame.textureMatrix);
                    ///< OES 数据转换 2D。
                    KFFrame convertFrame = mOESConvert2DFilter.render(frame);
                    mListener.dataOnAvailable(convertFrame);
                }
                mEGLContext.unbind();
            });
        }
    };
}

上面是 KFVideoSurfaceDecoder 的实现,与视频解码 KFByteBufferCodec 对比区别如下:

  • 1)数据输出不同。
    • KFByteBufferCodec 输出为 YUV 数据 KFBufferFrame
    • KFVideoSurfaceEncoder 输出为纹理数据 KFTextureFrame
  • 2)解码流水线不同。
    • KFVideoSurfaceEncoder 输出为纹理数据,将数据解码到纹理缓存 mSurface。释放缓存 releaseOutputBuffer 触发 KFSurfaceTextureListeneronFrameAvailable 回调,需要注意 releaseOutputBuffer 方法第 2 个参数 render 设置为 true。然后调用 mSurfaceTextureupdateTexImage 将数据刷新到自定义纹理。
  • 3)使用场景不同。
    • KFVideoSurfaceDecoder 适用于输出数据为纹理的情况,例如播放器。
    • KFByteBufferCodec 适用于输出数据非纹理数据,例如抽帧。

更具体细节见上述代码及其注释。

4、解封装和解码(ByteBuffer) MP4 文件中的视频部分存储为 YUV 文件

我们在一个 MainActivity 中来实现视频解封装及解码逻辑,并将解码后的数据存储为 YUV 文件。

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
public class MainActivity extends AppCompatActivity {
    private KFMP4Demuxer mDemuxer; ///< 解封装器。
    private KFDemuxerConfig mDemuxerConfig; ///< 解封装器配置。
    private KFMediaCodecInterface mDecoder = null; ///< 解码器。
    private FileOutputStream mStream = null;

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

        ///< 解封装配置。
        mDemuxerConfig = new KFDemuxerConfig();
        mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + "/2.mp4";
        mDemuxerConfig.demuxerType = KFMediaBase.KFMediaType.KFMediaVideo;
        if (mStream == null) {
            try {
                mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.yuv");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }

        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 (mDemuxer == null) {
                    mDemuxer = new KFMP4Demuxer(mDemuxerConfig,mDemuxerListener);
                    ///< 创建解码器。
                    mDecoder = new KFByteBufferCodec();
                    mDecoder.setup(false,mDemuxer.videoMediaFormat(),mDecoderListener,null);

                    ///< 循环读取数据输入给解码器。
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    ByteBuffer nextBuffer = mDemuxer.readVideoSampleData(bufferInfo);
                    while (nextBuffer != null) {
                        mDecoder.processFrame(new KFBufferFrame(nextBuffer,bufferInfo));
                        nextBuffer = mDemuxer.readVideoSampleData(bufferInfo);
                    }
                    mDecoder.flush();
                    Log.i("KFDemuxer","complete");
                }
            }
        });
        addContentView(startButton, startParams);
    }

    private KFDemuxerListener mDemuxerListener = new KFDemuxerListener() {
        @Override
        ///< 解封装回调出错。
        public void demuxerOnError(int error, String errorMsg) {
            Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
        }
    };

    private KFMediaCodecListener mDecoderListener = new KFMediaCodecListener() {
        @Override
        ///< 解码回调出粗。
        public void onError(int error, String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        ///< 解码后数据回调。
        public void dataOnAvailable(KFFrame frame) {
            if (frame == null) {
                return;
            }

            KFBufferFrame bufferFrame = (KFBufferFrame)frame;
            if (bufferFrame.buffer == null) {
                return;
            }

            MediaFormat mediaFormat = mDecoder.getOutputMediaFormat();
            int width = mediaFormat.getInteger("width");
            int height = mediaFormat.getInteger("height");
            int cropLeft = mediaFormat.getInteger("crop-left");
            int cropRight = mediaFormat.getInteger("crop-right");
            int cropTop = mediaFormat.getInteger("crop-top");
            int cropBottom = mediaFormat.getInteger("crop-bottom");
            int colorFormat = mediaFormat.getInteger("color-format"); //COLOR_FormatYUV420SemiPlanar

            ///< YUV 数据存储本地。
            try {
                byte[] dst = new byte[(int) (width*height*1.5)];
                bufferFrame.buffer.get(dst);
                mStream.write(dst);
            }  catch (IOException e) {
                e.printStackTrace();
            }
        }
    };
}

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

  • 1)通过启动视频解封装来驱动整个解封装和解码流程。
    • onClick 中实现开始动作并且循环读取数据塞入解码器。
  • 2)在解码模块 KFByteBufferCodec 的数据回调中获取解码后的 YUV 数据存储为文件。
    • KFMediaCodecListenerdataOnAvailable 中实现。
    • 这里按照 NV12 的 YUV 格式存储。

5、解封装和解码(Surface) MP4 文件中的视频纹理进行渲染

我们在一个 MainActivity 中来实现视频解封装及解码逻辑,并将解码后的数据进行渲染。

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
public class MainActivity extends AppCompatActivity {
    private KFMP4Demuxer mDemuxer; ///< 解封装器。
    private KFDemuxerConfig mDemuxerConfig; ///< 解封装器配置。
    private KFMediaCodecInterface mDecoder = null; ///< 解码。
    private KFRenderView mRenderView; ///< 渲染。
    private KFGLContext mGLContext; ///< GL 上下文。
    private Timer mTimer;

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

        ///< 创建解封装器配置。
        mDemuxerConfig = new KFDemuxerConfig();
        mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + "/2.mp4";
        mDemuxerConfig.demuxerType = KFMediaBase.KFMediaType.KFMediaVideo;

        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 (mDemuxer == null) {
                    mDemuxer = new KFMP4Demuxer(mDemuxerConfig,mDemuxerListener);
                    mDecoder = new KFVideoSurfaceDecoder();
                    mDecoder.setup(false, mDemuxer.videoMediaFormat(),mDecoderListener,mGLContext.getContext());
                    ((Button)view).setText("停止");
                } else {
                    mDemuxer.release();
                    mDemuxer = null;
                    mDecoder.release();
                    mDecoder = null;
                    ((Button)view).setText("开始");
                }
            }
        });
        addContentView(startButton, startParams);

        Timer timer = new Timer();
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                ///< 根据 Timer 回调读取解封装数据给解码器。
                if (mDemuxer != null) {
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    ByteBuffer byteBuffer = mDemuxer.readVideoSampleData(bufferInfo);
                    if (byteBuffer != null) {
                        KFBufferFrame frame = new KFBufferFrame();
                        frame.bufferInfo = bufferInfo;
                        frame.buffer = byteBuffer;
                        mDecoder.processFrame(frame);
                    }
                }
            }
        };
        timer.schedule(task,0,33);
    }

    private KFDemuxerListener mDemuxerListener = new KFDemuxerListener() {
        @Override
        ///< 解封装回调出错。
        public void demuxerOnError(int error, String errorMsg) {
            Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
        }
    };

    private KFMediaCodecListener mDecoderListener = new KFMediaCodecListener() {
        @Override
        ///< 解码回调出错。
        public void onError(int error, String errorMsg) {

        }

        ///< 解码回调进行渲染。
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void dataOnAvailable(KFFrame frame) {
            mRenderView.render((KFTextureFrame) frame);
        }
    };
}

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

  • 1)通过启动视频解封装来驱动整个解封装和解码流程。
    • onClick 中实现开始动作。
  • 2)启动 Timer 模块指定间隔进行解码渲染。
    • 启动 Timer 模块 mTimer
    • Timer 中 调用获取视频数据 readVideoSampleData,输入到解码器 processFrame
  • 3)在解码模块 KFVideoSurfaceDecoder 的数据回调中获取纹理数据进行渲染。
    • KFMediaCodecListenerdataOnAvailable 中进行渲染到 mRenderView

6、用工具播放 YUV 文件

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

1
$ ffplay -f rawvideo -pix_fmt nv12 -video_size 1280x720 -i test.yuv

注意这里的参数要对齐在工程中存储的 YUV 格式,我们 Demo 中的视频尺寸是 1280x720,我们是用 NV12 格式存储的 YUV。

关于播放 YUV 文件的工具,可以参考《FFmpeg 工具》第 2 节 ffplay 命令行工具《可视化音视频分析工具》第 1.2 节 YUVToolkit 或 1.3 节 YUVView

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