Android Media API 的介绍(1)

Posted by alonealice on 2020-10-27

Android系统已经提供了对视音频的强大支持,这边先介绍几个和视音频相关的类,通过这几个类的组合使用,可以实现很多音视频处理的相关功能,下面就对这几个类进行简单介绍。

1
2
3
4
5
6
7
8
MediaMetadataRetriever::用来获取视频的相关信息,例如视频宽高、时长、旋转角度、码率等等。
MediaExtractor::视音频分离器,将一些格式的视频分离出视频轨道和音频轨道。
MediaCodec:视音频相应的编解码类。
MediaMuxer:视音频合成器,将视频和音频合成相应的格式。
MediaFormat:视音频相应的格式信息。
MediaCodec.BufferInfo:存放ByteBuffer相应信息的类。
MediaCrypto:视音频加密解密处理的类。
MediaCodecInfo:视音频编解码相关信息的类。

MediaMetadataRetriever

MediaMetadataRetriever用来获取视音频的相关信息。

使用示例:

1
2
3
4
5
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(file.getAbsolutePath());
String widthString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
String heightString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
String durationString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);

MediaExtractor

MediaExtractor的作用是把音频和视频的数据进行分离。

主要API介绍:

  • setDataSource(String path):设置数据源,即可以设置本地文件又可以设置网络文件
  • getTrackCount():得到源文件通道数
  • getTrackFormat(int index):获取指定(index)的通道格式
  • getSampleTime():返回当前的时间戳
  • readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
  • advance():读取下一帧数据
  • release(): 读取结束后释放资源

使用示例:

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
//设置数据源
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) {
extractor.selectTrack(i);
}
}
//构建ByteBuffer
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
//读取数据到ByteBuffer
while (extractor.readSampleData(inputBuffer, 0) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
//下一帧
extractor.advance();
}
//释放资源
extractor.release();
extractor = null;

MediaCodec

MediaCodec是Android视音频里面最为重要的类,它主要实现的功能是对视音频进行编解码处理。

检查

在Android系统中,MediaCodec支持的格式有限,在使用MediaCodec之前需要对硬编类型的支持进行检测,如果MediaCodec支持再进行使用。

检查使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static MediaCodecInfo selectCodec(String mimeType) {
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) {
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo;
}
}
}
return null;
}

一般来说Android MediaCodec支持如下几种颜色格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static boolean isRecognizedFormat(int colorFormat) {
switch (colorFormat) {
// these are the formats we know how to handle for this test
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
return true;
default:
return false;
}
}

另外MediaCodec支持Surface的方式输入和输出,当编码的时候只需要在Surface上进行绘制就可以输入到编码器,而解码的时候可以将解码图像直接输出到Surface上。

创建

据视音频的类型创建相应的MediaCodec,比如视频使用了H264,而音频使用了AAC-LC,那么创建音频编码器需要传入相应的MIME,AAC-LC对应的是audio/mp4a-latm,而H264对应的是video/avc。

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
public static MediaCodec getAudioMediaCodec() throws IOException {
int size = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 1);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1000);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, size);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
//Video
public static MediaCodec getVideoMediaCodec() throws IOException {
int videoWidth = getVideoSize(1280);
int videoHeight = getVideoSize(720);
MediaFormat format = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 1300* 1000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
format.setInteger(MediaFormat.KEY_BITRATE_MODE,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
format.setInteger(MediaFormat.KEY_COMPLEXITY,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}

使用

MediaCodec创建之后,需要通过start()方法进行开启。MediaCodec有输入缓冲区队列和输出缓冲区队列,不断通过往输入缓冲区队列传递数据,经过MediaCodec处理后就可以得到响应的输出数据。

当在编码的时候,需要向输入缓冲区传入采集到的原始的视音频数据,然后获取输出缓冲区的数据,输出出来的数据也就是编码处理后的数据。

当在解码的时候,往输入缓冲区输入需要解码的数据,然后获取输出缓冲区的数据,输出出来的数据也就是解码后得到的原始的视音频数据。

当需要清空输入和输出缓冲区的时候,可以调用MediaCodec的flush()方法。当编码或者解码结束时,通过往输入缓冲区输入带结束标记的数据,然后从输出缓冲区可以得到这个结束标记,从而完成整个编解码过程。

在API>21之后,支持同步和异步两种方式

异步使用示例:

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
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
// 异步模式下需要在configure之前设置callback
codec.setCallback(new MediaCodec.Callback() {
/**
* 在onInputBufferAvailable回调方法中,MediaCodec会通知什么时候input
* buffer有效,根据buffer id,调用getInputBuffer(id)可以获得这个buffer,
* 此时就可以向这个buffer中写入数据,最后调用queueInputBuffer(id, …)提交
* 给MediaCodec处理。
*/
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data

codec.queueInputBuffer(inputBufferId, …);
}

/**
* 在onOutputBufferAvailable回调方法中,MediaCodec会通知什么时候output
* buffer有效,根据buffer id,调用getOutputBuffer(id)可以获得这个buffer,
* 此时就可以读取这个buffer中的数据,最后调用releaseOutputBuffer(id, …)释放
* 给MediaCodec再次使用。
*/

@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.

codec.releaseOutputBuffer(outputBufferId, …);
}
/**
* 当MediaCodec的output format发生变化是会回调该方法,一般在start之后都会首先回调该方法
*/
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
/**
* MediaCodec运行发生错误时会回调该方法
*/
@Override
void onError(…) {

}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start(); // start 之后MediaCodec立即进入Running子状态,并会回调callback中的方法
// wait for processing to complete
codec.stop(); // stop后MediaCodec进入Uninitialized子状态
codec.release(); //使用完毕要释放掉MediaCdoec占用的资源

MediaCodec.start()后Codec 立即进入Running子状态,通过设置的callback中的回调方法onInputBufferAvailable()会自动收到可用(empty)的input buffer,此时可以根据input buffer id调用getInputBuffer(id)得到这个buffer,并将需要的处理的数据写入该buffer中,最后调用queueInputBuffer(id, ...)将该buffer提交给Codec处理;Codec每处理完一帧数据就会将处理结果写入一个空的output buffer,并通过回调函数onOutputBufferAvailable`来通知Client来读取结果,Client可以根据output bufffer id调用getOutputBuffer(id)获取该buffer并读取结果,完毕后可以调用releaseOutputBuffer(id, …)释放该buffer给Codec再次使用,同时也将解码后的内容输出到Surface。

同步示例:

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
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, ...);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start(); // start()方法后会进入Flushed子状态
/**
* 在一个无限循环中不断地请求Codec是否有可用的input buffer 或 output buffer
*/
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs); // 请求是否有可用的input buffer
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(...); // 获取input buffer
// fill inputBuffer with valid data
...
codec.queueInputBuffer(inputBufferId, ...); // 提交数据给Codec
}
int outputBufferId = codec.dequeueOutputBuffer(...); // 请求是否有可用的output buffer
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); // 获取output buffer
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
...
codec.releaseOutputBuffer(outputBufferId, ...); // 释放output buffer供Codec再次使用
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release(); //释放资源

一个无限循环中通过调用dequeueInputBuffer(…)和dequeueOutputBuffer(…)来不断地请求Codec是否有可用的input buffer 或 output buffer。如果有可用的input buffer:根据得到的buffer id,调用getInputBuffer(id)获取该buffer,并向其中写入待处理的数据,然后调用queueInputBuffer(id,…)提交到Codec进行处理。如果有可用的output buffer: 根据得到的buffer id,调用getOutputBuffer(id)获取该buffer,读取其中的处理结果,然后调用releaseOutputBuffer(id,…)释放该buffer供Codec再次使用,并输出到surface。

MediaMuxer

MediaMuxer的作用是生成音频或视频文件;还可以把音频与视频混合成一个音视频文件。

相关API介绍:

  • MediaMuxer(String path, int format):path:输出文件的名称 format:输出文件的格式;当前只支持MP4格式;
  • addTrack(MediaFormat format):添加通道;我们更多的是使用MediaCodec.getOutpurForma()或Extractor.getTrackFormat(int index)来获取MediaFormat;也可以自己创建;
  • start():开始合成文件
  • writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把ByteBuffer中的数据写入到在构造器设置的文件中;
  • stop():停止合成文件
  • release():释放资源
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
//构建MediaMuxer
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
//创建音视频的MediaFormat
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
//添加通道
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
//构建输出的ByteBuffer
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
//开始导出
muxer.start();
while(!finished) {
int readSampleSize = mediaExtractor.readSampleData(byteBuffer, 0);
// 如果没有可获取的样本,退出循环
if (readSampleSize < 0) {
mediaExtractor.unselectTrack(videoIndex);
finished = true;
break;
}
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
//添加数据
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
//读取下一帧
mediaExtractor.advance();
}
};
muxer.stop();
muxer.release();