人間觀察
窮人家的孩子真的是在社會上瞎混
遙遠的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 onError(…) {
…
}
});
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
如果描述不正確的,歡迎指正。