Android MediaCodec 视频编码

使用MediaCodecMediaMuxer,实现视频编码。

编码流程如下:

  • 1.new MediaFormat()
  • 2.MediaCodec.createEncoderByType()
  • 3.MediaCodec.configure(format)
  • 4.MediaCodec.start()
  • 5.new MediaMuxer()
  • 6.MediaMuxer.start()
  • 7.mEncoder.getOutputBuffers() -> ByteBuffer[]
  • 8.mEncoder.dequeueOutputBuffer() -> status/index
  • 9.MediaMuxer.writeSampleData()
  • 10.mEncoder.releaseOutputBuffer()
  • 11.loop[7-10]
  • 12.mEncoder.signalEndOfInputStream()

Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final String MIME_TYPE = "video/avc";
private static final int WIDTH = 640;
private static final int HEIGHT = 480;
private static final int BIT_RATE = 4000000;
private static final int FRAMES_PER_SECOND = 4;
private static final int IFRAME_INTERVAL = 4;
private static final int NUM_FRAMES = 8;
// "live" state during recording
private MediaCodec.BufferInfo mBufferInfo;
private MediaCodec mEncoder;
private MediaMuxer mMuxer;
private Surface mInputSurface;
private int mTrackIndex;
private boolean mMuxerStarted;
private long mFakePts;

配置编码器Encoder

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
private void prepareEncoder(File outputFile) throws IOException {
mBufferInfo = new MediaCodec.BufferInfo();
// 指定视频格式(video/avc)和宽高
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT);
// Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception.
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // color格式
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); // bitrate
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMES_PER_SECOND); // 帧率(frames/sec)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); // 关键帧
Log.d(TAG, "format: " + format);
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
// we can use for input and wrap it with a class that handles the EGL work.
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); // 创建Encoder编码器
// 根据format配置编码器
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 创建Surface,之后在这个Surface上面进行绘制操作
mInputSurface = mEncoder.createInputSurface();
mEncoder.start(); // 启动编码器
// Create a MediaMuxer. We can't add the video track and start() the muxer here,
// because our MediaFormat doesn't have the Magic Goodies. These can only be
// obtained from the encoder after it has started processing data.
//
// We're not actually interested in multiplexing audio. We just want to convert
// the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
Log.d(TAG, "output will go to " + outputFile);
// 创建MediaMuxer
mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mTrackIndex = -1;
mMuxerStarted = false;
}

drainEncoder

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
/**
* Extracts all pending data from the encoder.
*
* If endOfStream is not set, this returns when there is no more data to drain. If it
* is set, we send EOS to the encoder, and then iterate until we see EOS on the output.
* Calling this with endOfStream set should be done once, right before stopping the muxer.
*/
private void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");
if (endOfStream) {
if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
mEncoder.signalEndOfInputStream();
}
// Retrieve the set of OutputBuffers
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
// 获取编码数据数组
while (true) {
// Returns the index of an output buffer that has been successfully decoded.
// or one of the INFO_* constants.
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.d(TAG, "try again later");
// no output available yet
if (!endOfStream) {
break; // out of while
} else {
if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder
Log.d(TAG, "output buffers changed");
encoderOutputBuffers = mEncoder.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
MediaFormat newFormat = mEncoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + newFormat);
// now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
mMuxerStarted = true;
} else if (encoderStatus < 0) {
Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
encoderStatus);
// let's ignore it
} else {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
" was null");
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status, Ignore it.
Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG: " + mBufferInfo.flags);
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
// adjust the ByteBuffer values to match BufferInfo
encodedData.position(mBufferInfo.offset); // 设置偏移量
encodedData.limit(mBufferInfo.offset + mBufferInfo.size); // 设置limit
mBufferInfo.presentationTimeUs = mFakePts;
mFakePts += 1000000L / FRAMES_PER_SECOND;
// 使用Muxer写入视频数据
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
Log.d(TAG, "sent " + mBufferInfo.offset + "/" + mBufferInfo.size + " bytes to muxer");
}
// release释放索引位置对应的buffer
mEncoder.releaseOutputBuffer(encoderStatus, false);
// endOfStream -> break;
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.w(TAG, "reached end of stream unexpectedly");
} else {
if (VERBOSE) Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}

generateMovie

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
private void generateMovie(File outputFile) {
try {
// 准备并配置编码器
prepareEncoder(outputFile);
for (int i = 0; i < NUM_FRAMES; i++) {
// Drain any data from the encoder into the muxer.
// 将数据从编码器中抽取到文件中
drainEncoder(false);
// Generate a frame and submit it.
generateFrame(i);
}
// Send end-of-stream and drain remaining output.
drainEncoder(true);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
} finally {
releaseEncoder(); // release 编解码器
}
}
private void releaseEncoder() {
if (VERBOSE) Log.d(TAG, "releasing encoder objects");
if (mEncoder != null) {
mEncoder.stop();
mEncoder.release();
mEncoder = null;
}
if (mInputSurface != null) {
mInputSurface.release();
mInputSurface = null;
}
if (mMuxer != null) {
mMuxer.stop();
mMuxer.release();
mMuxer = null;
}
}

generateFrame

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
private void generateFrame(int frameNum) {
Canvas canvas = mInputSurface.lockCanvas(null);
try {
int width = canvas.getWidth();
int height = canvas.getHeight();
Paint paint = new Paint();
for (int i = 0; i < 8; i++) {
int color = 0xff000000;
if ((i & 0x01) != 0) {
color |= 0x00ff0000;
}
if ((i & 0x02) != 0) {
color |= 0x0000ff00;
}
if ((i & 0x04) != 0) {
color |= 0x000000ff;
}
paint.setColor(color);
float sliceWidth = width / 8;
canvas.drawRect(sliceWidth * i, 0, sliceWidth * (i + 1), height, paint);
}
paint.setColor(0x80808080);
float sliceHeight = height / 8;
int frameMod = frameNum % 8;
canvas.drawRect(0, sliceHeight * frameMod, width, sliceHeight * (frameMod + 1), paint);
} finally {
mInputSurface.unlockCanvasAndPost(canvas);
}
}

Log输出

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
12-20 22:40:39.225 SoftInputActivity: Generating movie...
12-20 22:40:39.232 SoftInputActivity: format: {color-format=2130708361, i-frame-interval=4,
mime=video/avc, width=640, bitrate=4000000, frame-rate=4, height=480}
12-20 22:40:39.256 SoftInputActivity: output will go to /storage/emulated/0/soft-input-surface.mp4
12-20 22:40:39.261 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.275 SoftInputActivity: try again later
12-20 22:40:39.286 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.294 SoftInputActivity: encoder output format changed: {max-bitrate=4000000,
csd-1=java.nio.HeapByteBuffer[pos=0 lim=10 cap=10], color-transfer=3,
mime=video/avc, width=640, bitrate=4000000, color-range=2, color-standard=4,
height=480, csd-0=java.nio.HeapByteBuffer[pos=0 lim=18 cap=18]}
12-20 22:40:39.298 SoftInputActivity: ignoring BUFFER_FLAG_CODEC_CONFIG: 2
12-20 22:40:39.309 SoftInputActivity: sent 0/2456 bytes to muxer
12-20 22:40:39.321 SoftInputActivity: try again later
12-20 22:40:39.328 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.336 SoftInputActivity: sent 0/1151 bytes to muxer
12-20 22:40:39.348 SoftInputActivity: try again later
12-20 22:40:39.355 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.364 SoftInputActivity: sent 0/502 bytes to muxer
12-20 22:40:39.375 SoftInputActivity: try again later
12-20 22:40:39.380 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.385 SoftInputActivity: sent 0/884 bytes to muxer
12-20 22:40:39.396 SoftInputActivity: try again later
12-20 22:40:39.401 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.408 SoftInputActivity: sent 0/1014 bytes to muxer
12-20 22:40:39.420 SoftInputActivity: try again later
12-20 22:40:39.427 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.435 SoftInputActivity: sent 0/1018 bytes to muxer
12-20 22:40:39.446 SoftInputActivity: try again later
12-20 22:40:39.455 SoftInputActivity: drainEncoder(false)
12-20 22:40:39.466 SoftInputActivity: try again later
12-20 22:40:39.485 SoftInputActivity: drainEncoder(true)
12-20 22:40:39.485 SoftInputActivity: sending EOS to encoder
12-20 22:40:39.498 SoftInputActivity: sent 0/2013 bytes to muxer
12-20 22:40:39.512 SoftInputActivity: try again later
12-20 22:40:39.485 SoftInputActivity: no output available, spinning to await EOS
12-20 22:40:39.525 SoftInputActivity: sent 0/3506 bytes to muxer
12-20 22:40:39.527 SoftInputActivity: end of stream reached
12-20 22:40:39.485 SoftInputActivity: releasing encoder objects
12-20 22:40:39.536 SoftInputActivity: Movie generation complete