利用 MediaCodec 进行转码

前面的文章简单介绍了 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 压缩数据在另外一篇文章中单独介绍。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • 一、文章说明 最近工作实在太忙,很久没有更新文章了,收到很多小伙伴催更的消息,心中实在惭愧,趁着今天有空赶紧更新。...
    风从影阅读 18,817评论 33 118
  • 原文:https://developer.android.com/reference/android/media/...
    thebestofrocky阅读 6,066评论 0 6
  • 3、使用 MediaCodec创建之后,需要通过start()方法进行开启。MediaCodec有输入缓冲区队列和...
    韩瞅瞅阅读 1,303评论 0 1
  • 打包 视音频在传输过程中需要定义相应的格式,这样传输到对端的时候才能正确地被解析出来。 1、HTTP-FLV We...
    韩瞅瞅阅读 1,635评论 2 5
  • flink的核心操作有3部分: source transformation sink window 有2类窗口...
    3bd3c1497272阅读 483评论 0 0