使用 MediaExtractor 及 MediaCodec 解码音视频(Android)

展展与罗罗: 《沙漠骆驼》

01 前言

大家好,本文是  iOS/Android 音视频专题  的第四篇,从本篇文章开始我们将动手编写代码。代码工程将在 Github 进行托管。由于微信公众号不允许设置外部链接,你可以在微信公众( GeekDev )后台回复 资料   获取项目地址。

02 MediaExtractor 的基本使用

对音视频媒体文件解码时,我们首先需要分离出媒体文件的音视频轨道, MediaExtractor 就是干这个的,它可以告诉你媒体中轨道( Track )数量,并根据索引读取指定轨道数据。

API 概述

1. setDataSource(String path) 

为分离器指定数据源,支持网络地址和本地地址

2. getTrackCount()

获取轨道数据数量

3. getTrackFormat(int index)

获取指定索引位置的轨道格式信息

4. selectTrack(int index) 

根据轨道索引选中指定轨道,选中后将分离器将读取选中轨道的数据,读取数据之前须选中一个轨道,    而且同时只能选中一个轨道。

5. seekTo(long timeUs,int mode)

根据帧时间(timeUs)及搜寻模式(mode),搜寻最匹配的关键帧。

注意:分离视频轨道时,seekTo 不能精确到视频时间,seekTo 只能根据 mode 找到最匹配的关键帧。timeUs :搜寻的帧时间

mode:  搜寻模式

SEEK_TO_CLOSEST_SYNC 最接近 timeUS 的关键帧

SEEK_TO_NEXT_SYNC timeUS 的下一个关键帧

SEEK_TO_PREVIOUS_SYNC timeUS 的上一个关键帧

6. advance()

将分离器游标移动到下一帧

7. readSampleData(ByteBuffer buffer, int offset)

读取当前位置样本数据

8. getSampleTrackIndex()

获取当前选中的轨道索引

9. getSampleTime()

当前分离器样本时间

10. getSampleFlags() 

获取当前样本类型,为 SAMPLE_FLAG_SYNC 时表示为关键帧

示例

下面是分离视频轨道的关键步骤,音频轨道步骤一致,只需要选择对应的 mime type 索引即可。

创建一个媒体分离器

MediaExtractor extractor = new MediaExtractor();

为媒体分离器装载媒体文件路径


// 指定文件路径
String videoPath = “xxx.mp4”;
extractor.setDataSource(videoPath);


// 指定 Uri
Uri videoUri = xx
extractor.setDataSource(context,fileUri,null);

获取并选中指定类型的轨道


// 媒体文件中的轨道数量 (一般有视频,音频,字幕等)
int trackCount = extractor.getTrackCount();


// mime type 指示需要分离的轨道类型
String extractMimeType = “video/”;


// 记录轨道索引id,MediaExtractor 读取数据之前需要指定分离的轨道索引
int trackID = -1;
// 视频轨道格式信息
MediaFormat trackFormat;


for (int i = 0;i < trackCount; i++) {
trackFormat = extractor.getTrackFormat(i);
if (trackFormat.getString(MediaFormat.KEY_MIME).startsWith(extractMimeType)) {
trackID = i;
break;
}
}

// 媒体文件中存在视频轨道
if (trackID != -1){
extractor.selectTrack(trackID);
}

分离指定轨道的数据


// 获取最大缓冲区大小,
int maxInputSize = trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
// 开辟一个字节缓冲区,用于存放分离的媒体数据
ByteBuffer byteBuffer = ByteBuffer.allocate(maxInputSize);


// 记录当前帧数据大小
int sampleDataSize = 0;
while((sampleDataSize = extractor.readSampleData(byteBuffer,0)) > 0) {


MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
bufferInfo.offset = 0;
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.size = sampleDataSize;
bufferInfo.flags = extractor.getSampleFlags();

Log.i(“presentationTimeUs : %s”,extractor.getSampleTime());
}
// 释放分离器,释放后 extractor 将不可用
extractor.release();
extractor = null;

03 MediaFormat 媒体格式

MediaFormat 封装了媒体数据 音频,视频,字幕 格式的信息,所有信息都以键值对形式表示。 MediaFormat 中定义的 key 对于不同媒体数据并不是全部通用的,某些 key 只适用于特定媒体数据。

通用 Keys

KEY_MIME 格式类型
KEY_MAX_INPUT_SIZE 输出缓冲区的最大字节数
KEY_BIT_RATE 比特率

Video Keys

KEY_WIDTH    视频宽度

KEY_HEIGHT   视频高度

KEY_DURATION  内容持续时间(以微妙为单位)

KEY_CORLOR_FORMAT   视频内容颜色空间

KEY_FRAME_RATE   视频帧率

KEY_I_FRAME_INTERVAL   关键之间的时间间隔

KEY_ROTATION  视频旋转顺时针角度 

KEY_BITRATE     码率/比特率(画质和文件体积)

KEY_BITRATE_MODE    比特率模式

BITRATE_MODE_CBR : 编码器尽可能将输出码率控制为设置值 

BITRATE_MODE_CQ  : 编码器完全不控制码率,尽可能保证图图像质量

BITRATE_MODE_VBR :编码器根据图像内容及复杂度动态调整输出码率

Audio Keys

KEY_CHANNEL_COUNT   通道数

KEY_SAMPLE_RATE   采样率  

KEY_DURATION   内容持续时间(以微妙为单位)

04 MediaCodec 的基本使用

在之前的文章中我们已经知道 MediaCodec 是被用来对媒体文件进行编解码,今天我们重点介绍下 MediaCodec API 及基本用法。

API 概述

1. createDecoderByType(String mimeType)

根据 mime type 创建一个解码器( mimeType 可通过 MediaExtractor 的 MediaFormat 中获取)

2. createEncoderByType(String mimeType)

根据 mime type 创建一个编码器

3. configure(MediaFormat format,Surface surface,MediaCrypto crypto,int flags)

配置编解码器 

format: 当为解码器时表示为输入的媒体格式,编码器时表示为输出的媒体格式

surfaceSurface 可与 SurfaceTexture 配合使用可将解码后的数据渲染到指定纹理中

crypto :    如果视频被加密的话,需要配置该参数解密 (DRM 相关)

flags :       解码器为:0   编码器配置为:1/CONFIGURE_FLAG_ENCODE

4. getInputBuffers()

获取需要编解码的输入流队列。 为了提高性能  MediaCodec 内部维护了输入和输出缓冲区队列, 只有当输入队列空闲时方可写入数据。

5.dequeueInputBuffer(long timeoutUs)

从输入缓冲区请求空闲的输入队列索引。

timeoutUs:指定  MediaCodec 当前没有空闲输入队列时最大等待时间。

当请求到空闲队列后返回 ByteBuffer,将  ByteBuffer 填充数据后可调用 queueInputBuffer 加入编解码队列。

6. queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags )

将指定  index 输入缓冲区加入编解码器队列,等待编解码操作。

index : 输入的缓冲区索引 一般通过 dequeueInputBuffer 方法获取。

7. getOutputBuffers()

获取编解码数据输出队列

8. dequeueOutputBuffer( BufferInfo info, long timeoutUs)

请求完成编解码后的输出队列索引

info: 接收当前编解码后的信息

timeoutUs:指定  MediaCodec 当前没有数据输出事最大超时时间,

9. release OutputBuffer( int index,boolean render)

当从 MediaCodec 请求的输出队列索引处理完成后归还至缓冲区。

index : 通过 dequeueOutputBuffer 请求的队列索引

render: 指定是否渲染到 Surface 如果为 false Surface 将无法接受到该帧的数据输出。

10. start()/stop()/release()

启动、停止、释放。

示例 


// step 1:创建一个媒体分离器
MediaExtractor extractor = new MediaExtractor();


// step 2:为媒体分离器装载媒体文件路径
// 指定文件路径
Uri videoPathUri = Uri.parse(“android.resource://” + getPackageName() + “/” + R.raw.img_video);
try {
extractor.setDataSource(this, videoPathUri, null);
} catch (IOException e) {
e.printStackTrace();
}

// step 3:获取并选中指定类型的轨道
// 媒体文件中的轨道数量 (一般有视频,音频,字幕等)
int trackCount = extractor.getTrackCount();
// mime type 指示需要分离的轨道类型
String extractMimeType = “video/”;
MediaFormat trackFormat = null;
// 记录轨道索引id,MediaExtractor 读取数据之前需要指定分离的轨道索引
int trackID = –1;
for (int i = 0; i < trackCount; i++) {
trackFormat = extractor.getTrackFormat(i);
if (trackFormat.getString(MediaFormat.KEY_MIME).startsWith(extractMimeType)) {
trackID = i;
break;
}
}

// 媒体文件中存在视频轨道
// step 4:选中指定类型的轨道
if (trackID != –1)
extractor.selectTrack(trackID);


// step 5:根据 MediaFormat 创建解码器
MediaCodec mediaCodec = null;
try {
mediaCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
mediaCodec.configure(trackFormat,null,null,0);
mediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}




while (true) {
// step 6: 向解码器喂入数据
boolean ret = feedInputBuffer(extractor,mediaCodec);
// step 7: 从解码器吐出数据
boolean decRet = drainOutputBuffer(mediaCodec);
if (!ret && !decRet)break;;
}


// step 8: 释放资源
// 释放分离器,释放后 extractor 将不可用
extractor.release();
// 释放解码器

mediaCodec.release();

完整代码详见:DemoMediaCodecActivity

05 结束语

目前,我们解码后的视频尚未渲染在屏幕上,在渲染到屏幕之前我们需要对 OpenGLES 有所了解,并需要知道 Surface 及 SurfaceView 的基本使用。这部分内容我们将在下篇文章中进行讲解。

如果你想了解更多信息,可关注微信公众号 ( GeekDev ) 并回复 资料  获取。

往期内容:

iOS/Android 音视频开发专题介绍

iOS/Android 音视频概念介绍

MediaCodec/OpenMAX/StageFright 介绍

下期预告:

《使用OpenGLES 及 Surface 渲染视频