Android原生編解碼接口 MediaCodec 之——踩坑


版權聲明:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/gb702250823/article/details/81669684
希望我們尊重每個人的成果,轉載請標明出處:
https://blog.csdn.net/gb702250823/article/details/81669684
本文出自小口鍋的博客

關鍵幀
MediaCodec 有兩種方式觸發輸出關鍵幀,一是由配置時設置的 KEY_FRAME_RATE和KEY_I_FRAME_INTERVAL參數自動觸發,二是運行過程中通過 setParameters 手動觸發輸出關鍵幀。

自動觸發輸出關鍵幀
在MediaCodec硬編碼中設置I(關鍵幀)時間間隔,在 api 中是這么設置的

mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //關鍵幀間隔時間 單位s
1
自動觸發實際是按照幀數觸發的,例如設置幀率為 20 fps,關鍵幀間隔為 1s ,那就會每 20楨輸出一個關鍵幀,一旦實際幀率低於配置幀率,那就會導致關鍵幀間隔時間變長。由於 MediaCodec 啟動后就不能修改配置幀率/關鍵幀間隔了,所以如果希望改變關鍵幀間隔幀數,就必須重啟編碼器。

手動觸發輸出關鍵幀:

if (System.currentTimeMillis() - timeStamp >= 1000) {//1000毫秒后,設置參數
timeStamp = System.currentTimeMillis();
if (Build.VERSION.SDK_INT >= 23) {
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
mMediaCodec.setParameters(params);
}


關鍵幀踩坑
有時候你會發現自動觸發關鍵幀方式失效了

經排查發現真正的原因是在於視頻的輸入源,如果是通過Camera的PreviewCallback的方式來獲取視頻數據再喂給MediaCodec的方式是無法控制輸出關鍵幀的數量的。
發現當選擇支持顏色格式為yuv420p的編碼器時,KEY_I_FRAME_INTERVAL 設置無效;
選擇支持yuv420sp的編碼器時,KEY_I_FRAME_INTERVAL 設置有效;

想要控制輸出輸出關鍵幀數量就必須通過調用MediaCodec.createInputSurface()方法獲取輸入Surface,再通過Opengl渲染后喂給MediaCodec才能真正控制關鍵幀的數量。

//判斷輸出數據是否為關鍵幀的方法:
boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;

部分機型MediaCodec.configure直接crash
未設置編碼強制要求的一些配置 會拋出 IllegalStateException
看這個例子stackoverflow

如果初始化MediaFormat視頻流的預設寬高高於當前手機支持的解碼最大分辨率那么在調用MediaCodec.configure的時候就會crash。把MediaFormat.createVideoFormat時候的寬高設置小一點就ok了,那么就會有另外一個問題,就是如果我設置1080*720的后,視頻流來了一個1920*1080的會不會有影響?如果當前設備的最大分辨率高於這個值,就算預設值不一樣,也還是可以正常解碼並顯示1920*1080的畫面。那么如果低於這個值呢?兩種情況 綠屏/MediaCodec.dequeueInputBuffer的值一直拋IllegalStateException

如何獲取當前手機支持的解碼最大分辨率
每個手機下都有這樣一個文件,/system/etc/media_codecs.xml (your path)。這是一個xml文件,可以直接看到MediaCodecs–>Decoders節點下的各個視頻格式的支持情況,以華為榮耀7x Android 8.0 為例


獲取解碼視頻的寬和高

//獲得音視頻的配置器MediaFormat
private static MediaFormat getFormat(String path,boolean isVideo) {
try {
MediaExtractor mediaExtractor = new MediaExtractor();
mediaExtractor.setDataSource(path);
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
if (trackFormat.getString(MediaFormat.KEY_MIME).startsWith(isVideo ? "video/" :"audio/")) {
return mediaExtractor.getTrackFormat(i);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
//單獨獲取寬高
MediaFormat newFormat = getFormat(path,true);
int videoWidth = newFormat.getInteger(MediaFormat.KEY_WIDTH);
int videoHeight = newFormat.getInteger(MediaFormat.KEY_HEIGHT);
//結合編碼時獲取寬高
MediaFormat newFormat = mMediaCodec.getOutputFormat();
int videoWidth = newFormat.getInteger(MediaFormat.KEY_WIDTH);
int videoHeight = newFormat.getInteger(MediaFormat.KEY_HEIGHT);

部分機型MediaCodec.dequeueOutputBuffer一直報IllegalStateException
部分機型會一直卡在MediaCodec.INFO_TRY_AGAIN_LATER中,有的原因也是因為這個
該機型硬解碼最大配置分辨率低於當前視頻流的分辨率

部分機型輸出的數據太短,或者為0
取出 output buffer 后,要手動設置 position 和 limit(api19以下必須設置),有些設備的編碼器不會設置這兩個值,導致無法正確取出數據;取出 input buffer 后,要手動調用 clear。參見 bigflake FAQ #11

mMediaCodec.createInputSurface()創建失敗或者取出的數據不理想
雖然 mMediaCodec.createInputSurface() 從 API 18 就已經引入,但用在某些 API 18 的機型上會導致編碼器輸出數據量特別小,畫面是黑屏,所以 Surface 輸入模式從 API 19 啟用。
mMediaCodec.createInputSurface() 必須在mediaformat.configure(..)之后,在mediacodec.start() 之前調用。

關於BufferInfo中的presentationTimeUs設置
如果不正確設置presentationTimeUs,有的設備的編碼器會丟掉輸入楨,或者輸出圖像質量很差,參見bigflake FAQ #8;
MediaCodec 使用的是微秒,大多數java 使用毫秒和納秒,單位要處理好

如果采用surface輸入,想要丟幀要如何操作
??(何時開始編碼條件) 滿足該條件后開始編碼。這就做到可控了。

if (mBufferInfo.size != 0 && ??(何時開始編碼條件)) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
// mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
} else {
outputBuffer.clear();
}
// 處理結束,釋放輸出緩存區資源
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);

輸出數據和寫入數據為MP4要在同一線程中,否則會出現偶發性的花屏,馬賽克等
以下做法會出現花屏,馬賽克

if (mBufferInfo.size != 0) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
//這邊線程就懶得寫成阻塞等待數據作用了,懶。知道這個情況就OK了
new Thread(new Runnable() {
@Override
public void run() {
// mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
}
}).start();
}

關於 bitrate_mode 配置問題
MediaCodec中的bitrate mode有個坑,比如我在設置之前想確認下CBR是否支持,那么會調用isBitrateModeSupported()判斷,這樣會有問題。

// MediaCodecInfo.java
public boolean isBitrateModeSupported(int mode) {
for (Feature feat: bitrates) {
if (mode == feat.mValue) {
return (mBitControl & (1 << mode)) != 0;
}
}
return false;
}

mode是否支持從bitrates判斷

private static final Feature[] bitrates = new Feature[] {
new Feature("VBR", BITRATE_MODE_VBR, true),
new Feature("CBR", BITRATE_MODE_CBR, false),
new Feature("CQ", BITRATE_MODE_CQ, false)
};

framework居然把它寫死了!而不是從hardware或者xml中獲取,而xml是寫着支持的。
vendor/rockchip/common/vpu/etc/media_codecs.xml

如果你寫代碼比較嚴謹,先用isBitrateModeSupported()判斷CBR是否支持,那么就悲劇了。
另外,在默認情況下,如果上層沒有主動設置bitrate_mode的話,返回的是VBR。 也就是默認采用VBR 關於VBR CQ CBR區別,可查看Android原生編解碼接口 MediaCodec 之——完全解析中的流控。

關於 level、profile設置
由於視頻編碼后顯示的數據質量偏低,所以需要調整質量。這個時候需要在這個設置level、profile

Profile是對視頻壓縮特性的描述(CABAC呀、顏色采樣數等等)。
Level是對視頻本身特性的描述(碼率、分辨率、fps)。
簡單來說,Profile越高,就說明采用了越高級的壓縮特性。
Level越高,視頻的碼率、分辨率、fps越高

// 不支持設置Profile和Level,而應該采用默認設置
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
mediaFormat.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel41); // Level 4.1

關於設置這兩個參數,我發現某些設備上,設置了無效,還是默認值,經排查
是因為在android7.0以下,android 內部寫死了參數,編碼出來的只能是Baseline,除非系統改過這個BUG,否者設置無效,甚至會導致configure參數失敗。
————————————————
版權聲明:本文為CSDN博主「小口鍋」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/gb702250823/article/details/81669684


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM