共计 10522 个字符,预计需要花费 27 分钟才能阅读完成。
Android MediaCodec 编解码应用形式
应用 MediaCodec 进行编解码。输出 H.264 格局的数据,输入帧数据并发送给监听器。
接下来咱们简称 MediaCodec 为 codec
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()
,免得显示不失常。
代码示例 – 同步形式进行编解码
这一个例子应用同步形式进行编解码
应用同步形式
/** | |
* 解码器 | |
*/ | |
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 格局的数据。
工具函数
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
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 及当前性能非常弱小。
应用 Image 类存储图片
private static void dumpFile(String fileName, byte[] data) { | |
FileOutputStream outStream; | |
try {outStream = new FileOutputStream(fileName); | |
} catch (IOException ioe) {throw new RuntimeException("rustfisher: Unable to create output file" + fileName, ioe); | |
} | |
try {outStream.write(data); | |
outStream.close();} catch (IOException ioe) {throw new RuntimeException("rustfisher: 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("rustfisher: 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 的办法
间接存入文件
nv21 存为 jpg 文件
// in try catch | |
FileOutputStream fos = new FileOutputStream(Environment.getExternalStorageDirectory() + "/rustfisher.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
// 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(); |
codec 抉择 YUV420 格局输入 OutputBuffer 的问题
假如 codec 抉择的格局是COLOR_FormatYUV420Flexible
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
解码后,失去的格局是COLOR_QCOM_FormatYUV420SemiPlanar32m // 0x7FA30C04
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
nv21
和 nv21Codec
的 uv 局部存储程序是相同的。
【Android 音视频开发系列教程】