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 catchFileOutputStream 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 catchYuvImage 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

nv21nv21Codec 的 uv 局部存储程序是相同的。

【Android音视频开发系列教程】