Android音硬解码播放H264(Android音硬解码播放H264)

最近在学习ing,加油

硬件和软件编解码器

在介绍之前,我们需要知道什么是硬编解码器和软编解码器?

1.软编解码器:使用软件本身或使用CPU的方式对原始视频进行编解码。

优点:兼容性好。.

缺点:CPU占用率高,app内存占用较高,可能是因为CPU发热导致降频、卡顿、无法流畅录制、播放视频等。

2.硬编解码器:使用非CPU编码,如显卡GPU、专用DSP芯片、厂商芯片等。一般编解码算法是固定的,所以采用芯片处理。

优点:非常快速高效,CPU占用率低,即使长时间录制高清视频,手机也不会发热。

缺点:但是兼容性不好,经常出现画面不够精细的问题(但是看不出来)。

MediaCodec 硬编解码器

一般安卓直播采集终端/短视频编辑软件默认使用硬编解码,如果手机不支持软编解码。硬编解码器为王。

在 AndroidMediaCodec类中使用的是编码和解码。MediaCodec它是什么??MediaCodec是 Android 音视频编解码类,它通过访问底层codec来实现编解码的功能,比如需要把摄像头的视频 yuv 数据编码为h264/h265,pcm编码为aac,h264/h265解码等待yuv,aac解码pcm等待。MediaCodec是 Android 4.1 API16 引入了,在 Android 5.0 API21 中加入了异步模式。

MediaCodec Call是系统注册的编解码器,硬件厂商在系统中注册自己的硬编解码器,称为硬编解码器,如果硬件厂商注册软件编解码器,就是软件编解码器。通常不同的硬件制造商是不同的。然后 MediaCodec 负责调用。

获取手机支持的编解码器

不同的手机支持的编解码器不同,如何获取手机支持的编解码器?如下:

@RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun getSupportCodec() { val list = MediaCodecList(MediaCodecList.REGULAR_CODECS) val codecs = list.codecInfos Log.d(TAG, "Decoders:") for (codec in codecs) { if (!codec.isEncoder) Log.d(TAG, codec.name) } Log.d(TAG, "Encoders:") for (codec in codecs) { if (codec.isEncoder) Log.d(TAG, codec.name) } }

输出

2020-12-25 19:16:00.914 13115-13115/com.bj.gxz.h264decoderdemo D/H264: Decoders: 2020-12-25 19:16:00.914 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.aac.decoder 2020-12-25 19:16:00.914 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrnb.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrwb.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.flac.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.g711.alaw.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.g711.mlaw.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.gsm.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.mp3.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.opus.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.raw.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vorbis.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.avc 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h264.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h263.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.hevc 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.hevc.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.mpeg2 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.mpeg4 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.mpeg4.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.vp8 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp8.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp9.decoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: Encoders: 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.aac.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrnb.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrwb.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.flac.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.encoder.avc 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h264.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h263.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.encoder.hevc 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.mpeg4.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp8.encoder 2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp9.encoder

看一下命名,软解码器通常都是OMX.google以开头的,就像上面那个一样OMX.google.h264.decoder。硬解码器是基于OMX.[hardware_vendor]一开始的,像上面那个应该是海思芯片OMX.hisi.video.decoder.avc 。hisi当然也有一些不遵守这个规则的,系统会认为它是软解码器。编码器的名称是相同的。来自Android系统的源码可以判断规则,源码地址:http ://androidos/android/6.0.1_r16/xref/frameworks/av/media/libstagefright/OMXCodec.cpp

static bool IsSoftwareCodec(const char *componentName) { if (!strncmp("OMX.google.", componentName, 11)) { return true; } if (!strncmp("OMX.", componentName, 4)) { return false; } return true; }

MediaCodec 处理数据的类型

MediaCodec 非常强大,支持的编解码数据类型有:压缩音频数据、压缩视频数据、原始音频数据和原始视频数据,并且支持不同封装格式的编解码,如前所述,如果是硬解码,当然,也需要手机厂商的支持。您可以通过设置 Surface 来获取/呈现原始视频数据。MediaCodec 关于 API 每个方法和参数都有自己的含义。你可以慢慢深入地使用它。

MediaCodec 的编解码过程

下图是Android官方文档提供的,官方文档很详细。developer.android.google/reference/android/media/MediaCodec?hl=en

Android音硬解码播放H264(Android音硬解码播放H264)(1)

MediaCodec 处理输入数据产生输出数据,异步处理数据时,使用一组输入输出byteBuffer。该过程通常是

  1. 将数据填充到预设的输入缓冲区(ByteBuffer)中,
  2. 输入缓冲区填满数据后,传递给 MediaCodec 的编码和解码处理。经过编码和解码后,它会在 .
  3. 然后用户可以得到编码和解码后的数据,然后将ByteBuffer释放回MediaCodec,往复循环。

需要注意的是, Buffer 并不是我们自己对 MediaCodec 的新对象,它是 MediaCodec 为了更好地控制 Buffer 来处理,我们需要使用 MediaCodec 提供的方法来获取,然后将数据插入其中并获取数据。

媒体编解码器 API
  • MediaCodec 的创建createDecoderByType/createEncoderByType:根据具体的MIME类型(如“video/avc”)建立codec。Decoder就是decoder,Encoder就是encoder。createByCodecName: 知道组件的确切名称(如 OMX.google.h264.decoder)时,从组件名称编解码器创建。使用 MediaCodecList 可以获取组件的名称,如上所述。
  • configure:配置解码器或编码器。比如可以配置解码后的数据通过surface来显示,本文下面部分是解码h264的demo配置surface来把yuv的数据渲染到surface上。
  • start:开始编解码,等待数据。
  • 数据处理,开始编解码dequeueInputBuffer:返回一个有效的输入缓冲区的索引queueInputBuffer: 输入流入队列。通常是用数据填充它dequeueOutputBuffer:从输出队列中取出编码/解码数据,如果输入的数据较多,可能需要循环读取,一般在写代码的时候需要调用循环releaseOutputBuffer: 释放ByteBuffer数据返回给MediaCodecgetInputBuffers: 获取需要对数据进行编码和解码的输入流队列,返回一个 ByteBuffer ArraygetOutputBuffers: 获取编解码后的数据输出流队列,返回一个ByteBuffer数组
  • flush:清空输入输出队列缓冲区
  • stop:停止编解码
  • release: 发布编解码器

从上面的api我可以看到电影MediaCodec编解码器API的生命周期,可以再看官网。

MediaCodec 的同步和异步编解码器同步方式

官方样本

MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B codec.start(); for (;;) { int inputBufferId = codec.dequeueInputBuffer(timeoutUs); if (inputBufferId >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(…); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is identical to outputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec_OUTPUT_FORMAT_CHANGED) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) outputFormat = codec.getOutputFormat(); // option B } } codec.stop(); codec.release();

流程如下:

- Create and configure MediaCodec object - Loop until it's done : - If input buffer Be on it - Read a piece of input , Fill it in with input buffer Encoding and decoding are performed in the system - If the output buffer Be on it : - From output buffer Get the data after encoding and decoding for processing . - After processing , The destruction MediaCodec object .

异步方式

在Android 5.0,API21,引入异步模式。官方样品:

MediaCodec codec = MediaCodec.createByCodecName(name); MediaFormat mOutputFormat; // member variable codec.setcallback(new MediaCodec.Callback() { @Override void onInputBufferAvailable(MediaCodec mc, int inputBufferId) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } @Override void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is equivalent to mOutputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } @Override void onOutputFormatChanged(MediaCodec mc, MediaFormat format) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) mOutputFormat = format; // option B } @Override void onError(…) { … } }); codec.configure(format, …); mOutputFormat = codec.getOutputFormat(); // option B codec.start(); // wait for processing to complete codec.stop(); codec.release();

- Create and configure MediaCodec object . - to MediaCodec Object settings callback MediaCodec.Callback - stay onInputBufferAvailable Calling back : - Read a piece of input , Fill it in with input buffer Encoding and decoding are performed in the system - stay onOutputBufferAvailable Calling back : - From output buffer After encoding and decoding, the data is processed . - After processing , The destruction MediaCodec object .

解码h264视频

让我们解码视频中的一个h264(摘自tiktok的一段话。/葵h264文件).h265是一回事,只要了解h264.h265的编码方式和原理以及码流结构,都是小菜一碟。为了更好地理解 h264 的比特流数据,我们演示了一次只读取内存中的文件 byte 数据。

我们从两个方面来处理,都可以正常播放,只是我们对h264 Stream数据有了更深入的了解。

  • 就是我们做的h264比特流结构,一次一个的NAL单元(NALU)偷偷给MediaCodec,包括第一个SPS,PPS。
  • 是的,我们只是截取几个 k,然后用 MediaCodec 填充它

首先初始化MediaCodec

var bytes: ByteArray? = null var mediaCodec: MediaCodec init { // demo test , To facilitate one-time access to memory bytes = FileUtil.getBytes(path) // video/avc Namely H264, Create decoder mediaCodec = MediaCodec.createDecoderByType("video/avc") val mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height) mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15) mediaCodec.configure(mediaFormat, surface, null, 0) }

方式一:划分NAL单元(NALU)方式

private fun decodeSplitNalu() { if (bytes == null) { return } // Data start subscribing var startFrameIndex = 0 val totalSizeIndex = bytes!!.size - 1 Log.i(TAG, "totalSize=$totalSizeIndex") val inputBuffers = mediaCodec.inputBuffers val info = MediaCodec.BufferInfo() while (true) { // 1ms=1000us subtle val inIndex = mediaCodec.dequeueInputBuffer(10_000) if (inIndex >= 0) { // Split a frame of data if (totalSizeIndex == 0 || startFrameIndex >= totalSizeIndex) { Log.e(TAG, "startIndex >= totalSize-1 ,break") break } val nextFrameStartIndex: Int = findNextFrame(bytes!!, startFrameIndex 1, totalSizeIndex) if (nextFrameStartIndex == -1) { Log.e(TAG, "nextFrameStartIndex==-1 break") break } // Fill in the data val byteBuffer = inputBuffers[inIndex] byteBuffer.clear() byteBuffer.put(bytes!!, startFrameIndex, nextFrameStartIndex - startFrameIndex) mediaCodec.queueInputBuffer(inIndex, 0, nextFrameStartIndex - startFrameIndex, 0, 0) startFrameIndex = nextFrameStartIndex } var outIndex = mediaCodec.dequeueOutputBuffer(info, 10_000) while (outIndex >= 0) { // Here's a simple temporal way to keep the video alive fps, Otherwise the video will play very fast // demo Of H264 File is 30fps try { sleep(33) } catch (e: InterruptedException) { e.printStackTrace() } // Parameters 2 Render to surface On ,surface Namely mediaCodec.configure Parameters of 2 mediaCodec.releaseOutputBuffer(outIndex, true) outIndex = mediaCodec.dequeueOutputBuffer(info, 0) } } }

NALU分割方法

private fun findNextFrame(bytes: ByteArray, startIndex: Int, totalSizeIndex: Int): Int { for (i in startIndex..totalSizeIndex) { // 00 00 00 01 H264 Start code for if (bytes[i].toInt() == 0x00 && bytes[i 1].toInt() == 0x00 && bytes[i 2].toInt() == 0x00 && bytes[i 3].toInt() == 0x01) { // Log.e(TAG, "bytes[i 4]=0X${Integer.toHexString(bytes[i 4].toInt())}") // Log.e(TAG, "bytes[i 4]=${(bytes[i 4].toInt().and(0X1F))}") return i // 00 00 01 H264 Start code for } else if (bytes[i].toInt() == 0x00 && bytes[i 1].toInt() == 0x00 && bytes[i 2].toInt() == 0x01) { // Log.e(TAG, "bytes[i 3]=0X${Integer.toHexString(bytes[i 3].toInt())}") // Log.e(TAG, "bytes[i 3]=${(bytes[i 3].toInt().and(0X1F))}") return i } } return -1 }

方式一:固定字节数据填充

private fun findNextFrameFix(bytes: ByteArray, startIndex: Int, totalSizeIndex: Int): Int { // Every time, it's better to make the data bigger , Otherwise, it's like a weak network , Slow data flow leads to video card val len = startIndex 40000 return if (len > totalSizeIndex) totalSizeIndex else len }

说明:在实际项目中,通常是web/数据流塞进去的方式,只是给大家demo演示MediaCodec解码h264文件播放。

保存 decode h264 视频 yuv 数据为图片

我们保存在哪里呢,前面说了,解码后一定是h264保存,解码后的数据就是yuv数据。也就是说dequeueOutputBuffer然后取出解码后的数据,然后使用YuvImageClass compressToJpegSave as Jpeg Just图片。我们 3s 保持一个。本地代码:

// 3s Save a picture if (System.currentTimeMillis() - saveImage > 3000) { saveImage = System.currentTimeMillis() val byteBuffer: ByteBuffer = mediaCodec.outputBuffers[outIndex] byteBuffer.position(info.offset) byteBuffermit(info.offset info.size) val ba = ByteArray(byteBuffer.remaining()) byteBuffer.get(ba) try { val parent = File(Environment.getExternalStorageDirectory().absolutePath "/h264pic/") if (!parent.exists()) { parent.mkdirs() Log.d(TAG, "parent=${parent.absolutePath}") } // take NV21 Format picture , With quality 70 Compressed into Jpeg val path = "${parent.absolutePath}/${System.currentTimeMillis()}-frame.jpg" Log.e(TAG, "path:$path") val fos = FileOutputStream(File(path)) val yuvImage = YuvImage(ba, ImageFormat.NV21, width, height, null) yuvImagepressToJpeg( Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 80, fos) fos.flush() fos.close() } catch (e: IOException) { e.printStackTrace() } }

最后,硬解码速度很快,效率很高,播放视频需要PTS时间戳处理.demo最好的办法就是让它渲染慢一点(demo视频文件是30fps,也就是说1000ms/30= 33ms一帧yuv数据),所以在mediaCodec.releaseOutputBuffer(outIndex, true)休眠前(33ms)达到正常播放速度。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页