Android MediaCodec 编解码

本文最后更新于:2023年4月15日 下午

Android MediaCodec 使用方式

使用MediaCodec进行编解码。输入H.264格式的数据,输出帧数据并发送给监听器。

H.264的配置

创建并配置codec。配置codec时,若手动创建MediaFormat对象的话,一定要记得设置”csd-0”和”csd-1”这两个参数
“csd-0”和”csd-1”这两个参数一定要和接收到的帧对应上。

输入数据

给codec输入数据时,如果对输入数据进行排队,需要检查排队队列的情况。
例如一帧数据暂用1M内存,1秒30帧,排队队列有可能会暂用30M的内存。当内存暂用过高,我们需要采取一定的措施来减小内存占用。
codec硬解码时会受到手机硬件的影响。若手机性能不佳,编解码的速度有可能慢于原始数据输入。不得已的情况我们可以将排队中的旧数据抛弃,输入新数据。

解码器性能

对视频实时性要求高的场景,codec没有可用的输入缓冲区,mCodec.dequeueInputBuffer返回-1。
为了实时性,这里会强制释放掉输入输出缓冲区mCodec.flush()

问题1 - MediaCodec输入数据和输出数据数量之间有没有特定的关系

对于MediaCodec,输入数据和输出数据数量之间有没有特定的关系?假设输入10帧的数据,可以得到多少次输出?

实测发现,不能百分百保证输入输出次数是相等的。例如vivo x6 plus,输入30帧,能得到28帧结果。或者300次输入,得到298次输出。

异常1 - dequeueInputBuffer(0)一直返回-1

某些手机长时间编解码后,可能会出现尝试获取codec输入缓冲区时下标一直返回-1。
例如vivo x6 plus,运行约20分钟后,mCodec.dequeueInputBuffer(0)一直返回-1。

处理方法:如果一直返回-1,同步方式下尝试调用codec.flush()方法,异步方式下尝试codec.flush()后再调用codec.start()方法。

有一些手机解码速度太慢,有可能会经常返回-1。不要频繁调用codec.flush(),以免显示不正常。

代码示例 - 同步方式进行编解码

这一个例子使用同步方式进行编解码

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/**
* 解码器
*/
public class CodecDecoder {
private static final String TAG = "CodecDecoder";

private static final String MIME_TYPE = "video/avc";
private static final String CSD0 = "csd-0";
private static final String CSD1 = "csd-1";

private static final int TIME_INTERNAL = 1;
private static final int DECODER_TIME_INTERNAL = 1;

private MediaCodec mCodec;
private long mCount = 0; // 媒体解码器MediaCodec用的

// 送入编解码器前的缓冲队列
// 需要实时监控这个队列所暂用的内存情况 在这里堵塞的话很容易引起OOM
private Queue<byte[]> data = null;

private DecoderThread decoderThread;
private CodecListener listener; // 自定义的监听器 当解码得到帧数据时通过它发送出去

public CodecDecoder() {
data = new ConcurrentLinkedQueue<>();
}

public boolean isCodecCreated() {
return mCodec!=null;
}

public boolean createCodec(CodecListener listener, byte[] spsBuffer, byte[] ppsBuffer, int width, int height) {
this.listener = listener;
try {
mCodec = MediaCodec.createDecoderByType(Constants.MIME_TYPE);
MediaFormat mediaFormat = createVideoFormat(spsBuffer, ppsBuffer, width, height);
mCodec.configure(mediaFormat, null, null, 0);
mCodec.start();

Log.d(TAG, "decoderThread mediaFormat in:" + mediaFormat);

decoderThread = new DecoderThread();
decoderThread.start();

return true;
}
catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "MediaCodec create error:" + e.getMessage());

return false;
}
}

private MediaFormat createVideoFormat(byte[] spsBuffer, byte[] ppsBuffer, int width, int height) {
MediaFormat mediaFormat;
mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
mediaFormat.setByteBuffer(CSD0, ByteBuffer.wrap(spsBuffer));
mediaFormat.setByteBuffer(CSD1, ByteBuffer.wrap(ppsBuffer));
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);

return mediaFormat;
}

private long lastInQueueTime = 0;

// 输入H.264帧数据 这里会监控排队情况
public void addData(byte[] dataBuffer) {
final long timeDiff = System.currentTimeMillis() - lastInQueueTime;
if (timeDiff > 1) {
lastInQueueTime = System.currentTimeMillis();
int queueSize = data.size(); // ConcurrentLinkedQueue查询长度时会遍历一次 在数据量巨大的情况下尽量少用这个方法
if (queueSize > 30) {
data.clear();
LogInFile.getLogger().e("frame queue 帧数据队列超出上限,自动清除数据 " + queueSize);
}
data.add(dataBuffer.clone());
Log.e(TAG, "frame queue 添加一帧数据");
} else {
LogInFile.getLogger().e("frame queue 添加速度太快,跳过此帧. timeDiff=" + timeDiff);
}
}

public void destroyCodec() {
if (mCodec != null) {
try {
mCount = 0;

if(data!=null) {
data.clear();
data = null;
}

if(decoderThread!=null) {
decoderThread.stopThread();
decoderThread = null;
}

mCodec.release();
mCodec = null;
}
catch (Exception e) {
e.printStackTrace();
Log.d(TAG, "destroyCodec exception:" + e.toString());
}
}
}

private class DecoderThread extends Thread {
private final int INPUT_BUFFER_FULL_COUNT_MAX = 50;
private boolean isRunning;
private int inputBufferFullCount = 0; // 输入缓冲区满了多少次

public void stopThread() {
isRunning = false;
}

@Override
public void run() {
setName("CodecDecoder_DecoderThread-" + getId());
isRunning = true;
while (isRunning) {
try {
if (data != null && !data.isEmpty()) {
int inputBufferIndex = mCodec.dequeueInputBuffer(0);
if (inputBufferIndex >= 0) {
byte[] buf = data.poll();
ByteBuffer inputBuffer = mCodec.getInputBuffer(inputBufferIndex);
if (null != inputBuffer) {
inputBuffer.clear();
inputBuffer.put(buf, 0, buf.length);
mCodec.queueInputBuffer(inputBufferIndex, 0,
buf.length, mCount * TIME_INTERNAL, 0);
mCount++;
}
inputBufferFullCount = 0; // 还有缓冲区可以用的时候重置计数
} else {
inputBufferFullCount++;
LogInFile.getLogger().e(TAG, "decoderThread inputBuffer full. inputBufferFullCount=" + inputBufferFullCount);
if (inputBufferFullCount > INPUT_BUFFER_FULL_COUNT_MAX) {
mCount = 0;
mCodec.flush(); // 在这里清除所有缓冲区
LogInFile.getLogger().e(TAG, "mCodec.flush()...");
}
}
}

// Get output buffer index
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) {
final int index = outputBufferIndex;
Log.d(TAG, "releaseOutputBuffer " + Thread.currentThread().toString());
final ByteBuffer outputBuffer = byteBufferClone(mCodec.getOutputBuffer(index));
Image image = mCodec.getOutputImage(index);
if (null != image) {
// 获取NV21格式的数据
final byte[] nv21 = ImageUtil.getDataFromImage(image, FaceDetectUtil.COLOR_FormatNV21);
final int imageWid = image.getWidth();
final int imageHei = image.getHeight();
// 这里选择创建新的线程去发送数据 - 这是可优化的地方
new Thread(new Runnable() {
@Override
public void run() {
listener.onDataDecoded(outputBuffer,
mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT),
nv21, imageWid, imageHei);
}
}).start();
} else {
listener.onDataDecoded(outputBuffer,
mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT),
new byte[]{0}, 0, 0);
}

try {
mCodec.releaseOutputBuffer(index, false);
} catch (IllegalStateException ex) {
android.util.Log.e(TAG, "releaseOutputBuffer ERROR", ex);
}
outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "decoderThread exception:" + e.getMessage());
}

try {
Thread.sleep(DECODER_TIME_INTERNAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

// deep clone byteBuffer
private static ByteBuffer byteBufferClone(ByteBuffer buffer) {
if (buffer.remaining() == 0)
return ByteBuffer.wrap(new byte[]{0});

ByteBuffer clone = ByteBuffer.allocate(buffer.remaining());

if (buffer.hasArray()) {
System.arraycopy(buffer.array(), buffer.arrayOffset() + buffer.position(), clone.array(), 0, buffer.remaining());
} else {
clone.put(buffer.duplicate());
clone.flip();
}

return clone;
}
}

代码示例 - 工具函数

一些工具函数。比如从image中取出NV21格式的数据。

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
private byte[] getDataFromImage(Image image) {
return getDataFromImage(image, COLOR_FormatNV21);
}

/**
* 将Image根据colorFormat类型的byte数据
*/
private byte[] getDataFromImage(Image image, int colorFormat) {
if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) {
throw new IllegalArgumentException("only support COLOR_FormatI420 " + "and COLOR_FormatNV21");
}
if (!isImageFormatSupported(image)) {
throw new RuntimeException("can't convert Image to byte array, format " + image.getFormat());
}
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
Image.Plane[] planes = image.getPlanes();
byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
int channelOffset = 0;
int outputStride = 1;
for (int i = 0; i < planes.length; i++) {
switch (i) {
case 0:
channelOffset = 0;
outputStride = 1;
break;
case 1:
if (colorFormat == COLOR_FormatI420) {
channelOffset = width * height;
outputStride = 1;
} else if (colorFormat == COLOR_FormatNV21) {
channelOffset = width * height + 1;
outputStride = 2;
}
break;
case 2:
if (colorFormat == COLOR_FormatI420) {
channelOffset = (int) (width * height * 1.25);
outputStride = 1;
} else if (colorFormat == COLOR_FormatNV21) {
channelOffset = width * height;
outputStride = 2;
}
break;
}
ByteBuffer buffer = planes[i].getBuffer();
int rowStride = planes[i].getRowStride();
int pixelStride = planes[i].getPixelStride();

int shift = (i == 0) ? 0 : 1;
int w = width >> shift;
int h = height >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
for (int row = 0; row < h; row++) {
int length;
if (pixelStride == 1 && outputStride == 1) {
length = w;
buffer.get(data, channelOffset, length);
channelOffset += length;
} else {
length = (w - 1) * pixelStride + 1;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++) {
data[channelOffset] = rowData[col * pixelStride];
channelOffset += outputStride;
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length);
}
}
}
return data;
}

/**
* 是否是支持的数据类型
*/
private static boolean isImageFormatSupported(Image image) {
int format = image.getFormat();
switch (format) {
case ImageFormat.YUV_420_888:
case ImageFormat.NV21:
case ImageFormat.YV12:
return true;
}
return false;
}

“csd-0”和”csd-1”是什么,对于H264视频的话,它对应的是sps和pps,对于AAC音频的话,对应的是ADTS,做音视频开发的人应该都知道,它一般存在于编码器生成的IDR帧之中。

得到的mediaFormat

1
mediaFormat in:{height=720, width=1280, csd-1=java.nio.ByteArrayBuffer[position=0,limit=7,capacity=7], mime=video/avc, csd-0=java.nio.ByteArrayBuffer[position=0,limit=13,capacity=13], color-format=2135033992}

存储图片的方法

Image类在Android API21及以后功能十分强大。

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
private static void dumpFile(String fileName, byte[] data) {
FileOutputStream outStream;
try {
outStream = new FileOutputStream(fileName);
} catch (IOException ioe) {
throw new RuntimeException("Unable to create output file " + fileName, ioe);
}
try {
outStream.write(data);
outStream.close();
} catch (IOException ioe) {
throw new RuntimeException("failed writing data to file " + fileName, ioe);
}
}

private void compressToJpeg(String fileName, Image image) {
FileOutputStream outStream;
try {
outStream = new FileOutputStream(fileName);
} catch (IOException ioe) {
throw new RuntimeException("Unable to create output file " + fileName, ioe);
}
Rect rect = image.getCropRect();
YuvImage yuvImage = new YuvImage(getDataFromImage(image, COLOR_FormatNV21), ImageFormat.NV21, rect.width(), rect.height(), null);
yuvImage.compressToJpeg(rect, 100, outStream);
}

NV21转bitmap的方法

直接存入文件

1
2
3
4
5
// in try catch
FileOutputStream fos = new FileOutputStream(Environment.getExternalStorageDirectory() + "/imagename.jpg");
YuvImage yuvImage = new YuvImage(nv21bytearray, ImageFormat.NV21, width, height, null);
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, fos);
fos.close();

获得Bitmap对象的方法,这个方法耗时耗内存
NV21 -> yuvImage -> jpeg -> bitmap

1
2
3
4
5
6
7
// in try catch
YuvImage yuvImage = new YuvImage(nv21bytearray, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream os = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, os);
byte[] jpegByteArray = os.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length);
os.close();

参考 https://stackoverflow.com/questions/32276522/convert-nv21-byte-array-into-bitmap-readable-format

codec选择YUV420格式输出OutputBuffer的问题

假设codec选择的格式是COLOR_FormatYUV420Flexible

1
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);

解码后,得到的格式是COLOR_QCOM_FormatYUV420SemiPlanar32m // 0x7FA30C04
1
mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT)

解码出来的ByteBuffer oBuffer = mCodec.getOutputBuffer(index);包含元素个数为1413120;记为nv21Codec
而通过mCodec.getOutputImage(index)得到的image对象获取到的nv21数组元素个数为1382400;记为nv21,这些是我们想要的数据

对比这2个数组我们发现,前面的y部分是相同的。nv21前921600个元素是y数据,后460800个元素是uv数据。
nv21Codec前921600个元素是y数据,之后的20480个字节都是0,再接下来的460800个元素是uv数据。最后的10240个字节是0

nv21nv21Codec的uv部分存储顺序是相反的。

参考资料

Android音视频相关文章请参考 https://rustfisher.com/tags/Android-Media/


Android MediaCodec 编解码
https://blog.rustfisher.com/2017/12/20/Android/Android-MediaCodec_use_demo/
作者
Rust Fisher
发布于
2017年12月20日
许可协议