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 非常强大,支持的编解码数据类型有:压缩音频数据、压缩视频数据、原始音频数据和原始视频数据,并且支持不同封装格式的编解码,如前所述,如果是硬解码,当然,也需要手机厂商的支持。您可以通过设置 Surface 来获取/呈现原始视频数据。MediaCodec 关于 API 每个方法和参数都有自己的含义。你可以慢慢深入地使用它。
MediaCodec 的编解码过程下图是Android官方文档提供的,官方文档很详细。developer.android.google/reference/android/media/MediaCodec?hl=en
MediaCodec 处理输入数据产生输出数据,异步处理数据时,使用一组输入输出byteBuffer。该过程通常是
- 将数据填充到预设的输入缓冲区(ByteBuffer)中,
- 输入缓冲区填满数据后,传递给 MediaCodec 的编码和解码处理。经过编码和解码后,它会在 .
- 然后用户可以得到编码和解码后的数据,然后将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(摘自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