人間觀察
窮人家的孩子真的是在社會上瞎混
遙遠的2020年馬上就過去了,天吶!!!
前兩篇介紹了下H264的知識和碼流結構,本篇就拿上篇從抖音/快手抽離的h264檔案實作在Android中進行解碼播放&以及介紹所涉及的知識,
本文代碼用kotlin來寫,最近在學習ing,加油吧,打工人,你要悄悄打工,
視頻效果
文章搞不了視頻,貼個圖吧,

軟硬編解碼
在介紹前我們需要知道什么是軟硬編解碼?
1.軟編解碼:是利用軟體本身或者說是使用CPU對原視頻進行編解碼的方式,
優點:兼容性好,
缺點:CPU占用率高,app記憶體占用率變高,可能會因CPU發熱而降頻、卡頓,無法流暢錄制、播放視頻等問題,
2.硬編解碼:使用非CPU進行編碼,如顯卡GPU、專用的DSP芯片、廠商芯片等,一般編解碼演算法固定所以采用芯片處理,
優點:編碼速度非常快且效率極高,CPU的占用率低,就算長時間高清錄制視頻手機也不會發燙,
缺點:但是兼容性不好,往往畫面不夠精細也很難解決(但是還可以沒到不能看的程度),
MediaCodec硬編解碼
一般Android中直播采集端/短視頻的編輯軟體都是默認采用硬編解碼,如果手機不支持再采用軟編解碼,硬編解碼是王道,
在Android中是使用MediaCodec類進行編解碼,MediaCodec是什么呢? MediaCodec是Android提供的用于對音視頻進行編解碼的類,它通過訪問底層的codec來實作編解碼的功能,比如你要把攝像頭的視頻yuv資料編碼為h264/h265,pcm編碼為aac,h264/h265解碼為yuv,aac解碼為pcm等等,MediaCodec是Android 4.1 API16引入的,在Android 5.0 API21加入了異步模式,
MediaCodec呼叫的是系統注冊過的編解碼器,硬體廠商把自己的硬編解碼器注冊到系統中就是硬編解碼,如果硬體廠商注冊的是軟編解碼就是軟解碼,往往不同的硬體廠商是不一樣的,然后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.net.cn/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官方檔案提供的,官方檔案很詳細了,
https://developer.android.google.cn/reference/android/media/MediaCodec?hl=en

MediaCodec處理輸入資料產生輸出資料,當異步處理資料時,使用一組輸入輸出ByteBuffer.流程通常是
- 將資料填入到預先設定的輸入緩沖區(ByteBuffer),
- 輸入緩沖區填滿資料后將其傳給MediaCodec進行編解碼處理,編解碼處理完后它又填充到一個輸出ByteBuffer中,
- 然后使用方就可以獲取編解碼后的資料,再把ByteBuffer釋放回MediaCodec,往復回圈,
需要注意的是Bufffer佇列不是我們自己new物件后塞給MediaCodec,而是MediaCodec為了更好的控制Bufffer的處理,我們需要使用MediaCodec提供的方法獲取然后塞給它資料并取出資料,
MediaCodec API
- MediaCodec的創建
- createDecoderByType/createEncoderByType:根據特定MIME型別(比如"video/avc")創建codec,Decoder就是解碼器,Encoder就是編碼器,
- createByCodecName:知道組件的確切名稱(如OMX.google.h264.decoder)的時候,根據組件名創建codec,使用MediaCodecList可以獲取組件的名稱,如上文所介紹,
- configure:配置解碼器或者編碼器,比如你可以配置把解碼的資料通過surface進行展示,本文的后續就是解碼h264的demo就是配置surface來把yuv資料渲染到此surface上,
- start:開始編解碼,處于等待資料的到來,
- 資料的處理,開始編解碼
- dequeueInputBuffer:回傳有效的輸入buffer的索引
- queueInputBuffer:輸入流入佇列,一般是把資料塞給它
- dequeueOutputBuffer:從輸出佇列中取出編/解碼后的資料,如果輸入的資料多,你可能要回圈讀取,一般在寫代碼的時候是需要回圈呼叫的
- releaseOutputBuffer:釋放ByteBuffer資料回傳給MediaCodec
- getInputBuffers:獲取需要編解碼資料的輸入流佇列,回傳的是一個ByteBuffer陣列
- getOutputBuffers:獲取編解碼之后的資料輸出流佇列,回傳的是一個ByteBuffer陣列
- flush:清空的輸入和輸出佇列buffer
- 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.INFO_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();
流程大概如下:
- 創建并配置MediaCodec物件
- 回圈直到完成:
- 如果輸入buffer準備好了
- 讀取一段輸入,將其填充到輸入buffer中進行編解碼
- 如果輸出buffer準備好了:
- 從輸出buffer中獲取編解碼后資料進行處理,
- 處理完畢后,銷毀 MediaCodec 物件,
異步方式
在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 one rror(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
- 創建并配置MediaCodec物件,
- 給MediaCodec物件設定回呼MediaCodec.Callback
- 在onInputBufferAvailable回呼中:
- 讀取一段輸入,將其填充到輸入buffer中進行編解碼
- 在onOutputBufferAvailable回呼中:
- 從輸出buffer中獲取進行編解碼后資料進行處理,
- 處理完畢后,銷毀 MediaCodec 物件,
解碼h264視頻
我們就解碼一個h264的視頻(拿上篇從抖音/快手抽離的h264檔案),h265也一樣,只要你明白了h264,h265的編碼方式和原理和碼流結構,都是小菜一碟,為了更明白h264的碼流資料,我們demo就一次性把檔案讀如刀記憶體的byte資料中,
處理我們分兩種方式,都能正常播放,只是我們更清楚的了解h264碼流資料,
- 是我們按照h264的碼流結構,每次截取一個NAL單元(NALU)塞給MediaCodec,包含最開始的SPS,PPS,
- 是我們就固定截取幾k,然后塞給MediaCodec
首先 初始化MediaCodec
var bytes: ByteArray? = null
var mediaCodec: MediaCodec
init {
// demo測驗,為方便一次性讀取到記憶體
bytes = FileUtil.getBytes(path)
// video/avc就是H264,創建解碼器
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
}
// 資料開始下標
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 微妙
val inIndex = mediaCodec.dequeueInputBuffer(10_000)
if (inIndex >= 0) {
// 分割出一幀資料
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
}
// 填充資料
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) {
// 這里用簡單的時間方式保持視頻的fps,不然視頻會播放很快
// demo 的H264檔案是30fps
try {
sleep(33)
} catch (e: InterruptedException) {
e.printStackTrace()
}
// 引數2 渲染到surface上,surface就是mediaCodec.configure的引數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的啟始碼
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的啟始碼
} 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 {
// 每次最好資料里大點,不然就像弱網的情況,資料流慢導致視頻卡
val len = startIndex + 40000
return if (len > totalSizeIndex) totalSizeIndex else len
}
說明:在真實的專案中一般是網路/資料流的方式塞入,這里只是為了demo演示MediaCodec解碼h264檔案進行播放,
保存解碼h264視頻的yuv資料為圖片
我們在哪里進行保存里,就如前問所說,肯定是在h264解碼后進行保存,解碼后的資料為yuv資料,也就是在dequeueOutputBuffer后取出解碼后的資料,然后用YuvImage類的compressToJpeg保存為Jpeg圖片即可,我們3s保存一張吧,
區域代碼:
// 3s 保存一張圖片
if (System.currentTimeMillis() - saveImage > 3000) {
saveImage = System.currentTimeMillis()
val byteBuffer: ByteBuffer = mediaCodec.outputBuffers[outIndex]
byteBuffer.position(info.offset)
byteBuffer.limit(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}")
}
// 將NV21格式圖片,以質量70壓縮成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)
yuvImage.compressToJpeg(
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)前在sleep(33ms)來達到正常的播放速度,
文章源代碼
https://github.com/ta893115871/H264DecoderDemo
如果描述不正確的,歡迎指正,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/240910.html
標籤:其他
下一篇:直播推流SDK綜述(一)
