Android 音视频开发【视频篇】【四】视频解码 | 8月更文挑战

前面几章介绍了关于RGBYUV格式,并介绍了如何采集和编码视频数据,那么本章就来介绍如何解码视频

一、MediaCodec

我们再来回顾下MediaCodec,首先上谷歌官网的MediaCodec流程图

封面.png

这张图为我们展示了MediaCodec的工作流程

之前在音频的部分,我们也具体介绍了关于MediaCodec的工作流程,这里我们再简单描述下:

  1. MediaCodec具有解码和编码的功能,是Android内置的编解码组件
  2. MediaCodec分为两个部分,一个是客户端,一个是服务端,客户端可以理解为我们的调用端,而服务端则是具体编解码的硬件部分
  3. 客户端从Codec中获取一个空的Buffer,然后将待编/解码的数据填入该Buffer中,接着插入Codec输入队列,再接着Codec处理完数据后,我们再从Codec中获取编/解码完成的数据,至此,即完成了一次输入输出的过程,重复该过程,我们则得到完整的一次编/解码数据
  4. MediaCodec有两种模式,异步同步,具体使用哪种模式视具体业务而定,比如,在视频编码那一章,我们使用的是异步的编码模式,这是因为从相机获取到的数据是通过回调,而在音频编码时,我们使用的是同步模式,这是因为AudioRecord是支持同步获取数据

以上就是关于MediaCodec的介绍,更加具体的介绍和使用可以查看前几章的内容

关于视频解码,还涉及到一个组件MediaExtractor,它能够解封装Mp4,将其分为音轨视轨,并能够一帧一帧地读取,在前面音频解码的章节也有具体描述,不再赘述

二、视频解码

关于视频解码,我们将使用MediaCodec的同步方式去实现

先来看整体流程

  1. 开启子线程
  2. 配置必要参数
  3. MediaExtractor中找到指定的Track
  4. 初始化MediaCodec
  5. 开始解码
  6. 循环从MediaExtractor中读取数据,并送入编码器
  7. 解码完成后,可以得到解码后的数据,也可以直接进行渲染
  8. 停止解码,释放资源

下面对上述步骤,一一讲解

2.1 开启子线程

和音频解码一样,都需要在子线程中执行

private static class DecodeThread extends Thread {
    public DecodeThread() {}
}
复制代码

2.2 配置必要参数

private static class DecodeThread extends Thread {
    private static final long TIMEOUT_MS = 2000L;
    private MediaExtractor mediaExtractor;
    private MediaCodec mediaCodec;
    private String path;
    private Surface surface;
    private String mime;
    private MediaFormat format;
    private boolean isStopDecode = false;
    public DecodeThread(String path, Surface surface) {
        this.path = path;
        this.surface = surface;
    }
}
复制代码

可以看到,在构造方法中有两个参数

  • pathMp4地址,这个是必须的
  • surface:这个是解码用的Surface,非必须的,我们可以直接得到解码后的数据,也可以将解码的数据渲染到指定的Surface中,不过一般我们都是直接解码到Surface上,因为解码后的数据就只是YUV数据,使用起来也麻烦

2.3 从MediaExtractor中找到指定的Track

首先重写Threadrun方法

@Override
public void run() {
    super.run();
    initMediaExtractor();
    initMediaCodec();
    decode();
}
复制代码

接下来看initMediaExtractor()方法

private void initMediaExtractor() {
    if (TextUtils.isEmpty(path)) {
        return;
    }
    try {
        mediaExtractor = new MediaExtractor();
        mediaExtractor.setDataSource(path);
        int trackCount = mediaExtractor.getTrackCount();
        for (int i = 0; i < trackCount; i++) {
            MediaFormat format = mediaExtractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (!TextUtils.isEmpty(mime) && mime.startsWith("video/")) {
                mediaExtractor.selectTrack(i);
                this.mime = mime;
                this.format = format;
                break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
        mediaExtractor = null;
        mime = null;
        format = null;
    }
}
复制代码

该方法用于初始化MediaExtractor,且获取对应视频的mimeformat

2.4 初始化MediaCodec

获取的mimeformat主要是为了初始化MediaCodec

private void initMediaCodec() {
    if (TextUtils.isEmpty(mime) || format == null || surface == null) {
        return;
    }
    try {
        mediaCodec = MediaCodec.createDecoderByType(mime);
        mediaCodec.configure(format, surface, null, 0);
    } catch (IOException e) {
        e.printStackTrace();
        mediaCodec = null;
    }
}
复制代码

MediaCodecconfig方法中,传入了formatsurface

  • format是在MediaExtractor中获取的
  • surface是外部传入

2.5 开始解码

mediaCodec.start();
复制代码

调用MediaCodecstart()方法即开始解码

2.6 循环从MediaExtractor中读取数据,并送入编码器

int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
    ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
    int readSize = -1;
    if (inputBuffer != null) {
        readSize = mediaExtractor.readSampleData(inputBuffer, 0);
    }
    if (readSize <= 0) {
        mediaCodec.queueInputBuffer(
                inputBufferId,
                0,
                0,
                0,
                MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        isStopDecode = true;
    } else {
        mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
        mediaExtractor.advance();
    }
}
复制代码

在该代码块中可以看到

  • MediaExtractor中读取对应的数据,将其填充到从MediaCodec获取的ByteBuffer

  • 在填充完毕后,会有两种情况

    • 一是数据大于0的情况,此时正常插入MediaCodec即可,并在插入后调用MediaExtractoradvance()方法,该方法表示进入下一帧
    • 还有一种情况是当数据小于等于0时,此时插入MediaCodec就得传入一个BUFFER_FLAG_END_OF_STREAMflag,表示整体输入流结束

2.7 解码完成后,可以得到解码后的数据,也可以直接进行渲染

int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
    ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
    if (outputBuffer != null && info.size > 0) {
        while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        byte[] data = new byte[info.size];
        outputBuffer.get(data);
        outputBuffer.clear();
        // 得到的data数据就是YUV数据,可以拿去做对应的业务
    }
    mediaCodec.releaseOutputBuffer(outputBufferId, true);
}
复制代码

在获取解码数据时,需要注意的是需要延时解码,因为解码时间和播放时间不一致,会导致播放变快,这也就解释了为什么直接解码不加延时,播放的视频会变得很快

至此,重复2.6 - 2.7过程,即可进行完整的解码,整体解码代码如下

private void decode() {
    if (mediaExtractor == null || mediaCodec == null) {
        return;
    }
    long startMs = System.currentTimeMillis();
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    mediaCodec.start();
    for (; ; ) {
        if (isStopDecode) {
            release();
            break;
        }
        int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
        if (inputBufferId >= 0) {
            ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
            int readSize = -1;
            if (inputBuffer != null) {
                readSize = mediaExtractor.readSampleData(inputBuffer, 0);
            }
            if (readSize <= 0) {
                mediaCodec.queueInputBuffer(
                        inputBufferId,
                        0,
                        0,
                        0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                isStopDecode = true;
            } else {
                mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
                mediaExtractor.advance();
            }
        }
        int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
        if (outputBufferId >= 0) {
            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
            if (outputBuffer != null && info.size > 0) {
                while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
                byte[] data = new byte[info.size];
                outputBuffer.get(data);
                outputBuffer.clear();
                // 得到的data数据就是YUV数据,可以拿去做对应的业务
            }
            mediaCodec.releaseOutputBuffer(outputBufferId, true);
        }
    }
}
复制代码

2.8 停止解码,释放资源

在完成一切之后,记得停止解码并释放资源

void stopDecode() {
    isStopDecode = true;
    try {
        join(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
private void release() {
    if (mediaExtractor != null) {
        mediaExtractor.release();
        mediaExtractor = null;
    }
    if (mediaCodec != null) {
        mediaCodec.stop();
        mediaCodec.release();
        mediaCodec = null;
    }
}
复制代码

三、GitHub

YuvDecoder.java

YuvActivity.java

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享