使用MediaCodec进行视频的编码和解码

在Android中播放视频很简单,只要创建一个MediaPlayer实例,然后设置上DataSource和SurfaceView就可以了。但是播放视频还有一种方式就是使用Android提供的MediaCodec,它可以用于编码和解码。另外如果要播放使用Android Widevine加密的视频则必须使用MediaCodec来完成解密和解码的过程。MediaCodec的工作原理很好理解,如下图所示,有一个输入的ByteBuffers向其输入数据,MediaCodec进行处理后会将其输出到一个输出的ByteBuffers里,典型的生产者消费者模型。下面我们来实现一下使用MediaCodec进行解码和编码。

解码

首先我们先创建一个包装类,对MediaCodec的一些配置和控制操作给包装起来便于调用。当MediaCodec创建并配置好了之后,就需要周期性地进行releaseOutputBuffer操作输出解码后的内容到Surface。在这里我们使用Rxjava的interval操作符来进行这个周期性的操作。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class VideoDecoder {
private final Surface mSurface;
private MediaCodec mDecoder;
private Subscriber mSubscriber;

public VideoDecoder(Surface surface) {
mSurface = surface;
}

public void config(MediaFormat mediaFormat) {
try {
mDecoder = MediaCodec.createDecoderByType(Config.VIDEO_MIME);
mDecoder.configure(mediaFormat, mSurface, null, 0);
mDecoder.start();
} catch (IOException e) {
e.printStackTrace();
}
}

public void config(int width, int height, ByteBuffer csd0) {
Logger.i("config:" + csd0.limit());
MediaFormat format = MediaFormat.createVideoFormat(Config.VIDEO_MIME, width, height);
format.setByteBuffer("csd-0", csd0);
config(format);
}

public int dequeueInputBuffer(long timeout) {
return mDecoder.dequeueInputBuffer(timeout);
}

public ByteBuffer getInputBuffer(int index) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return mDecoder.getInputBuffers()[index];
} else {
return mDecoder.getInputBuffer(index);
}
}

/**
* queue data to the input buffer of codec
*/

public void queueInputBuffer(int inIndex, int offset, int size, long presentationTimeUs, int flags) {
mDecoder.queueInputBuffer(inIndex, offset, size, presentationTimeUs, flags);
}


/**
* index to render the content to the surfaceview
*/

public void start() {
Logger.i("index");
if (mSubscriber != null && !mSubscriber.isUnsubscribed()) {
mSubscriber.unsubscribe();
}
mSubscriber = new Subscriber<Boolean>() {
@Override
public void onCompleted() {
stop();
}

@Override
public void onError(Throwable e) {
stop();
}

@Override
public void onNext(Boolean aBoolean) {
if (aBoolean) {
stop();
unsubscribe();
}

}
};

Observable.interval(Config.INTERVAL, TimeUnit.MILLISECONDS)
.map(new Func1<Long, Boolean>() {
@Override
public Boolean call(Long aLong) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outIndex = mDecoder.dequeueOutputBuffer(info, 10000);
if (outIndex > 0) {
mDecoder.releaseOutputBuffer(outIndex, true);
}

if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0) {
Logger.d("OutputBuffer BUFFER_FLAG_END_OF_STREAM");
return true;
}
return false;
}
})
.subscribeOn(Schedulers.newThread())
.subscribe(mSubscriber);

}

/**
* stop mFileDecoder
*/

public void stop() {
Logger.e("stop");
if (mSubscriber != null && !mSubscriber.isUnsubscribed()) {
mSubscriber.unsubscribe();
}
if (mDecoder != null) {
mDecoder.stop();
mDecoder.release();
}
}
}

解码的过程还需要同MediaExtractor结合起来,根据mime type 从MediaExtractor中取出一条track,可以是video也可以是audio, 然后根据这条track的MediaFormat来对MediaCodec进行配置就完成了准备阶段。然后不断地从MediaExtractor中取出Sample数据,将其填充到输入buffer中。这个过程我们使用了Rxjava来完成,一方面逻辑清晰,另一方面可以让我们很容易地控制填充buffer的速度。代码如下:

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
Observable.range(0, mMediaExtractor.getTrackCount())
.filter(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer integer) {

//find the video track
MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(integer);
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
Logger.d(mime);
return mime.startsWith("video");
}
})
.flatMap(new Func1<Integer, Observable<Long>>() {
@Override
public Observable<Long> call(Integer integer) {
//create mFileDecoder according the video track

MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(integer);
mMediaExtractor.selectTrack(integer);
mDecoder.config(mediaFormat);
return Observable.interval(Config.INTERVAL, TimeUnit.MILLISECONDS);
}
})
.map(new Func1<Long, Boolean>() {
@Override
public Boolean call(Long aLong) {

int inIndex = mDecoder.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buffer = mDecoder.getInputBuffer(inIndex);
int sampleSize = mMediaExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
Logger.d("Input buffer eos");
mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
return true;
} else {
mDecoder.queueInputBuffer(inIndex, 0, sampleSize, mMediaExtractor.getSampleTime(), 0);
mMediaExtractor.advance();
}
}
return false;
}
})
.subscribe(mSubscriber);

编码

编码同解码一样,还是一个输入-处理-输出的过程。通过下面的方法,我们可以得到一个用来作为输入的Surface:

1
mSurface = mCodec.createInputSurface();

得到这个Surface之后,我们首先需要通过lockCanvas获得一个Canvas。 有了Canvas,我们就可以在上面画任何我们想画的东西了, 如画一些圆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Canvas canvas = mSurface.lockCanvas(null);
try {
onDraw(canvas);
} finally {
mSurface.unlockCanvasAndPost(canvas);
}

void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLUE);

if (mPaint == null) {
mPaint = new TextPaint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.YELLOW);
}
canvas.drawCircle(OUTPUT_WIDTH / 2, OUTPUT_HEIGHT / 2, currentRadius, mPaint);
currentRadius += 10;
currentRadius = currentRadius > 100 ? 10 : currentRadius;
}

在这个Canvas上画的内容会传输MediaCodec进行处理,然后会将编码后的内容输出到MediaCodec的显示Surface上。这个过程需要我们对输入和输出的buffer做一些处理,如输出了一定长度的buffer并release之后,我们就可以通知输入的buffer来输入同样长度的内容。也就是说输入和输出的速度是由我们来控制的。

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
int status = mCodec.dequeueOutputBuffer(mBufferInfo, 10000);
if (status >= 0) {
// encoded sample
ByteBuffer data = mCodec.getOutputBuffer(status);
if (data != null) {
final int endOfStream = mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM;
// pass to whoever listens to

if (endOfStream == 0 && mLister != null) {
mLister.onSampleEncoded(mBufferInfo, data);
}
// releasing buffer is important
mCodec.releaseOutputBuffer(status, false);
if (endOfStream == MediaCodec.BUFFER_FLAG_END_OF_STREAM)
return true;

}
}

public void onSampleEncoded(MediaCodec.BufferInfo info, ByteBuffer data) {
Logger.v("onSample encoded");
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
mDecoder.config(OUTPUT_WIDTH, OUTPUT_HEIGHT, data);
mDecoder.start();

} else {
int inIndex = mDecoder.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buffer = mDecoder.getInputBuffer(inIndex);
buffer.put(data);
if (info.size < 0) {

Logger.d("Input buffer eos");
mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {

mDecoder.queueInputBuffer(inIndex, 0, info.size, info.presentationTimeUs, info.flags);
}
}
}
}

最终我们就可以在输出的SurfaceView上看到不断重复画的圆了。

本文中的源代码在github