利用 MediaCodec 进行转码

作者: GeorgeMR | 来源:发表于2019-02-15 12:01 被阅读3次

    前面的文章简单介绍了 MediaCodec 的使用说明,这篇文章会说明如何使用 MediaCodec 进行视频转码。

    首先关于转码的流程:

    视频文件 ——> 解封装 ——> 解码 ——> 编码 ——> 封装 ——> 转码后的视频文件

    那么转换到 MediaCodec 中对应的流程即:

    视频

    1. MediaExtractor 解封装 video 数据,

    2. MediaCodec 解码器解码压缩视频数据,并输入到 Surface

    3. Surface 中的原始视频数据输入到 MediaCodec 编码器进行编码

    4. 对编码器输出数据进行封装(不分块的情况下:使用 MediaMuxer 进行封装。 分块的情况下:使用 FFmpeg muxer 进行封装)

    音频

    1. MediaExtractor 解封装 audio 数据,

    2. MediaCodec 解码器解码压缩视频数据

    3. 解码后的 ByteBuffer 数据输入 MediaCodec 编码器进行编码

    4. 对编码器输出数据进行封装(不分块的情况下:使用 MediaMuxer 进行封装。 分块的情况下:使用 FFmpeg muxer 进行封装)

    先简单介绍下前面流程中提到的 MediaExtractor & MediaMuxer

    MediaExtractor

    主要用于提取音视频相关信息,分离音视频。读取音视频文件,然后按照一定的格式输出出来。

    使用步骤(参考官方示例):

    MediaExtractor extractor = new MediaExtractor();
    // 设置数据源
    extractor.setDataSource(...);
    // 文件轨道总数
    int numTracks = extractor.getTrackCount();
    for (int i = 0; i < numTracks; ++i) {
      MediaFormat format = extractor.getTrackFormat(i);
      String mime = format.getString(MediaFormat.KEY_MIME);
      if (weAreInterestedInThisTrack) {
        // 因为 MediaExtractor 需要选定轨道之后,才能读取数据。所以针对 video & audio 如果想要同步处理的话,则需要创建两个MediaExtractor分别读取
        extractor.selectTrack(i);
      }
    }
    
    // 读取数据到 inputBuffer 
    ByteBuffer inputBuffer = ByteBuffer.allocate(...)
    while (extractor.readSampleData(inputBuffer, ...) != 0) {
      // 数据对应索引
      int trackIndex = extractor.getSampleTrackIndex();
      // 数据时间戳
      long presentationTimeUs = extractor.getSampleTime();
      ...
      // 前进到下一帧(不存在下一帧,则返回 false)
      extractor.advance();
    }
    // 释放
    extractor.release();
    extractor = null;
    

    MediaMuxer

    主要用于封装编码后的视频流和音频流到文件容器中(目前支持 MP4、Webm、3GP文件封装格式)

    使用步骤:

    // 创建 MP4 封装格式的封装器
    MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
    // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
    // or MediaExtractor.getTrackFormat().
    MediaFormat audioFormat = new MediaFormat(...);
    MediaFormat videoFormat = new MediaFormat(...);
    int audioTrackIndex = muxer.addTrack(audioFormat);
    int videoTrackIndex = muxer.addTrack(videoFormat);
    ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
    boolean finished = false;
    BufferInfo bufferInfo = new BufferInfo();
    muxer.start();
    while(!finished) {
      // getInputBuffer() will fill the inputBuffer with one frame of encoded
      // sample from either MediaCodec or MediaExtractor, set isAudioSample to
      // true when the sample is audio data, set up all the fields of bufferInfo,
      // and return true if there are no more samples.
      finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
      if (!finished) {
        int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
        // 写入文件
        muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
      }
    };
    muxer.stop();
    muxer.release();
    

    使用 Surface 作为解码的输出以及编码的输入

    MediaCodec 通过 Surface 可以实现编解码的硬件加速。

    编码器通过调用 createInputSurface() 方法获取一个 Surface 作为 encoder的输入。

    解码器在 调用 configure() 方法时传入 Surface 参数,解码后的数据直接输出到 Surface。

    前面简单介绍了 MediaCodec 的大致流程,下面展开具体介绍:

    MediaCodec 转码流程.png

    MediaCodec 选择异步方式,前面的文章已经介绍过异步方式下如何调用,主要是四个方法:

    public void onInputBufferAvailable(); // codec 存在可用输入缓冲区,将需要处理的数据输入缓冲区
    public void onOutputBufferAvailable();// codec 存在可用输出缓冲,取出完成编解码的数据进行下一步处理
    public void onError(); // 编解码出错
    public void onOutputFormatChanged(); // 输出的 MediaFormat 发生了改变
    

    参考着上面的流程图,介绍下每个主要的步骤

    视频:

    1. 创建 MediaExtractor, 用于获取输入视频的 MediaFormat 以及 读取视频压缩数据

    2. 配置视频输出相关参数(码率、宽&高、帧率等)MediaFormat, 创建 video 编码器,并获取 encoder 的输入 Surface

    3. 通过 MediaExtractor 获取输入视频的 MediaFormat, 创建 video 解码器,并在 configure 时传入 Surface 作为输出目标

    4. 当 decoder 存在可用输入缓冲时,通过 MediaExtractor 读取 video 压缩数据,传入 decoder 进行处理(queueInputBuffer)

    5. 当 decoder 存在可用输出缓冲时,调用 releaseOutputBuffer(index, true) 将数据输出到 Surface,

      encoder 存在可用输入缓冲时,会直接从 Surface 获取数据(这部分会自动处理,不用做额外工作)

    6. encoder 存在可用输出缓冲时,getOutputBuffer(index) 获取 video 压缩数据,进行封装

    音频:

    1. 创建 MediaExtractor, 用于获取输入音频的 MediaFormat 以及 读取音频压缩数据

    2. 配置音频输出相关参数(采样率、比特率、信道数量等)MediaFormat, 创建 audio 编码器

    3. 通过 MediaExtractor 获取输入音频的 MediaFormat, 创建 audio 解码器

    4. 当 decoder 存在可用输入缓冲时,通过 MediaExtractor 读取 audio 压缩数据,传入 decoder 进行处理(queueInputBuffer)

    5. 当 decoder 存在可用输出缓冲时,getOutputBuffer(index) 获取音频原始数据,并存入本地缓存

      encoder 存在可用输入缓冲时,将本地缓存中的音频原始数据 queInputBuffer 输入编码器

    6. encoder 存在可用输出缓冲时,getOutputBuffer(index) 获取 audio 压缩数据,进行封装

    Tips:

    转码中存在视频截取的场景,MediaCodec 中没有类似 FFmpeg 中 "-ss、-t" 可以控制截取起点和时长的参数,所以需要在向解码器输入参数时人为进行截取:

    // seek 到指定时间(mode - 指定时间的前一帧、后一帧、最靠近的一帧)
    public native void seekTo(long timeUs, @SeekMode int mode);
    

    首先: 调用 MediaExtractor.seekTo 方法 seek 到视频截取开始时间

    然后: 在向解码器中传输压缩数据时,判断是否处理了足够时长的数据,下面直接通过代码来看:

    while (!mVideoReadDone) {
        // 读取视频数据到解码器输入缓冲
        int size = mVideoExtractor.readSampleData(decoderInputBuffer, 0);
        long pst = mVideoExtractor.getSampleTime();
        // 判断当前帧的时间戳是否已经超过要截取的时长
        if (length != 0 && pst > start + length) {
            // 到达剪辑时间
            mVideoReadDone = true;
            } else {
                if (start > 0) {
                    // 如果需要截取视频,需要重新计算时间戳(因为当前帧记录的还是截取之前的时间戳)
                    videoPst += videoSampleTime;
                    pst = videoPst;
                }
                if (size >= 0) {
                    // 将解码器缓冲送入解码器
                    codec.queueInputBuffer(index, 0, size, pst,
                                    mVideoExtractor.getSampleFlags());
                }
    
                // 视频数据是否已读取完
                mVideoReadDone = !mVideoExtractor.advance();
            }
            if (mVideoReadDone) {
                // 视频数据读完 或 到达剪辑时间
                logdw(LOG_LEVEL_DEBUG, "Video extractor: EOS");
    
                // send EOS to decoder
                codec.queueInputBuffer(index, 0, 0, 0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            }
            if (size >= 0) {
                break;
            }
    }
    

    视频封装:

    MediaMuxer:

    在使用 MediaMuxer 进行音视频封装时需要注意:需要先添加 video & audio track,然后才能向 muxer 写入压缩数据。

    public abstract void onOutputFormatChanged(
                    @NonNull MediaCodec codec, @NonNull MediaFormat format);
    

    在编码器输出数据之前,会先输出压缩数据的 MediaFormat,因此要在 video & audio 编码器都输出 OutputFormat 之后,并添加到 MeidaMuxer 之后,再调用 start 方法启动 Muxer:

    // 记录下 video & audio 的track,后面写入数据时需要用到
    mOutputVideoTrack = mMuxer.addTrack(mEncoderVideoFormat);
    mOutputAudioTrack = mMuxer.addTrack(mEncoderAudioFormat);
        
    mMuxer.start();
    

    当编码器输出压缩数据后:

    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)
    

    就可以将 video & audio 压缩数据写入 MediaMuxer 进行封装:

    // video 
    ByteBuffer videoOutputBuffer = mVideoEncoder.getOutputBuffer(index);
    mMuxer.writeSampleData(mOutputVideoTrack, videoOutputBuffer, info);
    
    // audio
    ByteBuffer audioOutputBuffer = mAudioEncoder.getOutputBuffer(index);
    mMuxer.writeSampleData(mOutputAudioTrack, audioOutputBuffer, info);
    

    FFmpeg: 关于使用 FFmpeg muxer 封装 MediaCodec 压缩数据在另外一篇文章中单独介绍。

    相关文章

      网友评论

        本文标题:利用 MediaCodec 进行转码

        本文链接:https://www.haomeiwen.com/subject/iqypeqtx.html