1. 程式人生 > >使用MediaCodec實現H264編碼「第四章,Android音視訊編碼那點破事」

使用MediaCodec實現H264編碼「第四章,Android音視訊編碼那點破事」

  本章僅對部分程式碼進行講解,以幫助讀者更好的理解章節內容。
本系列文章涉及的專案HardwareVideoCodec已經開源到Github,支援軟編和硬編。使用它你可以很容易的實現任何解析度的視訊編碼,無需關心攝像頭預覽大小。一切都如此簡單。目前已迭代多個穩定版本,歡迎查閱學習和使用,如有BUG或建議,歡迎Issue。

  說到Android的視訊硬編碼,很多新人首先會想到MediaRecorder,這可以說是Android早期版本視訊硬編碼的唯一選擇。這個類的使用很簡單,只需要給定一個Surface(輸入)和一個File(輸出),它就給你生成一個標準的mp4檔案。
  但越是簡單的東西便意味著越難以控制,MediaRecorder的缺點很明顯。相信很多人在接觸到斷點視訊錄製

這個需求的時候,首先會想到使用MediaRecorder,很遺憾,這個東西並不能給你很多期待,就像一開始的我一樣。
  首先,MediaRecorder並沒有斷點錄製的API,當然你可以使用一些“小技巧”,每次錄製的時候,都把MediaRecorder stop掉,然後再次初始化,這樣就會生成一系列的視訊,最後把它們拼接起來。然而問題在於,每次初始化MediaRecorder都需要消耗很長時間,這意味著,當用戶快速點選錄製按鈕的時候可能會出現問題。對於這個問題,你可以等到MediaRecorder初始化完成才讓使用者點選開始錄製,但是這樣往往會因為等待時間過長,導致使用者體驗極差。
  這種情況下,一個可控的視訊編碼器是必須的。雖然在Android 4.4以前我們沒得選擇,但是在Android 4.4之後,我們有了MediaCodec
,一個完全可控的視訊編碼器,雖然無法直接輸出mp4(需要配合MediaMuxer來對音視訊進行混合,最終輸出mp4,或者其它封裝格式)。如今的Android生態,大部分手機都已經是Android 5.0系統,完全可以使用MediaCodec來進行音視訊編碼的開發,而MediaRecorder則降級作為一個提高相容性的備選方案。
  廢話不多說,我們直接步入正題。要想正確的使用MediaCodec,我們首先得先了解它的工作流程,關於這個,強烈大家去看一下Android文件。呃呃,相信在這個快速開發為王道的環境,沒幾個人會去看,所以還是在這裡簡單介紹一下。

MediaCodec工作流程

  1. 首先,通過MediaCodec的工廠方法createEncoderByType
    createByCodecName建立例項,這時候MediaCodec處於Uninitialized狀態。
  2. 接下來,呼叫configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)設定編碼器引數,這時候MediaCodec處於Configured狀態。
  3. 正確設定各種引數之後,呼叫start方法,讓MediaCodec開始編碼,這時候MediaCodec處於Running狀態。
  4. 最後順序呼叫signalEndOfInputStreamstoprelease來結束編碼。

  流程很簡單,相信大家都能看懂。難點在於running狀態,也就是上圖右側綠色部分的流程。
  當MediaCodec處於Running狀態時,內部會持有兩個緩衝區佇列,一個輸入緩衝區,一個輸出緩衝區。當我們向輸入緩衝區輸入資料後,MediaCodec會從中取出資料,送到硬體進行編碼,編碼結束後送到緩衝區,這是一個非同步過程,這時候我們可以從輸出緩衝區取出編碼後的資料。這個過程在更高版本有更好的API,新版MediaCodec可以通過回撥返回編碼後的資料。由於我們可以控制什麼時候給編碼器輸入資料,所以可以隨時暫停或者開始編碼。
  理論講的差不多了,接下來我們看一下具體實現。

    //初始化一個編碼器配置MediaFormat
    fun createVideoFormat(parameter: Parameter, ignoreDevice: Boolean = false): MediaFormat? {
        val codecInfo = getCodecInfo(parameter.video.mime, true)
        if (!ignoreDevice && null == codecInfo) {//Unsupport codec type
            return null
        }
        val mediaFormat = MediaFormat()
        //使用H264編碼
        mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC)
        //設定視訊寬度
        mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width)
        //設定視訊高度
        mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height)
        //設定視訊輸入顏色格式,這裡選擇使用Surface作為輸入,可以忽略顏色格式的問題,並且不需要直接操作輸入緩衝區。
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
        //設定視訊位元速率,這裡計算公式選擇一箇中等位元速率,把3改為更大的值可以開啟更高位元速率,通常不建議超過5
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * fps * 3)
        //設定視訊fps
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps)
        //設定視訊關鍵幀間隔,這裡設定兩秒一個關鍵幀
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            /**
             * 可選配置,設定位元速率模式
             * BITRATE_MODE_VBR:恆定質量
             * BITRATE_MODE_VBR:可變位元速率
             * BITRATE_MODE_CBR:恆定位元速率
             */
            mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)
            /**
             * 可選配置,設定H264 Profile
             * 需要做相容性檢查
             */
            mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh)
            /**
             * 可選配置,設定H264 Level
             * 需要做相容性檢查
             */
            mediaFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel31)
        }
        return mediaFormat
    }
    //初始化並配置編碼器
    private fun initCodec() {
        val format = CodecHelper.createVideoFormat(parameter)
        debug_v("create codec: ${format.getString(MediaFormat.KEY_MIME)}")
        try {
            codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME))
            /**
             * 配置編碼器
             */
            codec!!.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
            /**
             * 由於我們使用Surface作為輸入,所以不需要直接操作輸入緩衝區,只需要把MediaCodec生成的Surface繫結到OpenGL即可,所以這裡使用了一個紋理封裝CodecTextureWrapper,請參考前幾章的CameraTextureWrapper和ScreenTextureWrapper,或者直接檢視文章末尾給出的原始碼。
             */
            codecWrapper = CodecTextureWrapper(codec!!.createInputSurface(), textureId, eglContext)
            codecWrapper?.egl?.makeCurrent()
            codec!!.start()
        } catch (e: Exception) {
            debug_e("Can not create codec")
        } finally {
            if (null == codec)
                debug_e("Can not create codec")
        }
    }
    /**
     * 從編碼器迴圈取出編碼資料,通過OpenGL來控制資料輸入,省去了直接控制輸入緩衝區的步驟,所以這裡直接操控輸出緩衝區即可
     */
    private fun dequeue(): Boolean {
        try {
            /**
             * 從輸出緩衝區取出一個Buffer,返回一個狀態
             * 這是一個同步操作,所以我們需要給定最大等待時間WAIT_TIME,一般設定為10000ms
             */
            val flag = codec!!.dequeueOutputBuffer(mBufferInfo, WAIT_TIME)
            when (flag) {
                MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {//輸出緩衝區改變,通常忽略
                    debug_v("INFO_OUTPUT_BUFFERS_CHANGED")
                }
                MediaCodec.INFO_TRY_AGAIN_LATER -> {//等待超時,需要再次等待,通常忽略
//                    debug_v("INFO_TRY_AGAIN_LATER")
                    return false
                }
            /**
             * 輸出格式改變,很重要
             * 這裡必須把outputFormat設定給MediaMuxer,而不能不能用inputFormat代替,它們時不一樣的,不然無法正確生成mp4檔案
             */
                MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    debug_v("INFO_OUTPUT_FORMAT_CHANGED")
                    //這裡通過回撥把outputFormat送出去
                    onSampleListener?.onFormatChanged(codec!!.outputFormat)
                }
                else -> {
                    if (flag < 0) [email protected] false//如果小於零,則跳過
                    val data = codec!!.outputBuffers[flag]//否則代表便阿門成功,可以從輸出緩衝區佇列取出資料
                    if (null != data) {
                        val endOfStream = mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        if (endOfStream == 0) {//如果沒有收到BUFFER_FLAG_END_OF_STREAM訊號,則代表輸出資料時有效的
                            mBufferInfo.presentationTimeUs = pTimer.presentationTimeUs
                            //這裡把編碼後的資料通過回撥送出去
                            onSampleListener?.onSample(mBufferInfo, data)
                        }
                        //緩衝區使用完後必須把它還給MediaCodec,以便再次使用,至此一個流程結束,再次迴圈
                        codec!!.releaseOutputBuffer(flag, false)
//                        if (endOfStream == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
//                            return true
//                        }
                        return true
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return false
    }
    //編碼結束後,停止編碼器
    private fun stop(){
        while (dequeue()) {//取出編碼器輸出緩衝區中剩餘的幀資料
        }
        debug_e("Video encoder stop")
        //編碼結束,傳送結束訊號,讓surface不在提供資料
        codec!!.signalEndOfInputStream()
        codec!!.stop()
        codec!!.release()
    }

  以上就是本章關於MediaCodec的全部學習內容,如果有疑問或者錯誤,歡迎在評論區留言。

本章知識點:

  1. MediaCodec的工作流程。
  2. MediaCodec的使用。

相關推薦

使用MediaCodec實現H264編碼Android視訊編碼點破

  本章僅對部分程式碼進行講解,以幫助讀者更好的理解章節內容。 本系列文章涉及的專案HardwareVideoCodec已經開源到Github,支援軟編和硬編。使用它你可以很容易的實現任何解析度的視訊編碼,無需關心攝像頭預覽大小。一切都如此簡單。目前已迭代多個穩定版本,歡迎

使用java實現面向對象

變量 面向接口 public 其他 多態 void () 具體實現 實例 第四章 接口 一、接口 1.接口可以看作是一種特殊的“抽象類”。 2.接口有比抽象類更好的特性 3.可以被多繼承 4.設計和實現完全分離 5

Linux內核設計與實現 總結筆記()進程調度

什麽 原則 好的 nic 調度系統 相交 中間 使用 就是 進程調度 調度程序負責決定將哪個進程投入運行,何時運行以及運行多長時間。 調度程序沒有太復雜的原理,最大限度地利用處理器時間的原則是,只要有可以執行的進程,那麽就總會有進程正在執行。 多任務 多任務系統可以劃分

【MySQL】《高性能MySQL》學習筆記Schema與數據類型優化

MySQL優化 MySQL表設計 MySQL開發規範 MySQL數據類型 【MySQL】《高性能MySQL》學習筆記,第四章,Schema與數據類型優化 良好的邏輯設計和物理設計是高性能的基石,應該根據系統將要執行的查詢語句來設計schema。 反範式的設計可以加快某些類型的查詢,單同時可能使

《數學之美》讀書記錄【思維導圖記錄】:談談中文分詞

post IT .cn splay top style title mage blog 《數學之美》讀書記錄【思維導圖記錄】:第四章,談談中文分詞

淺談RPA--RPA是否需要BA

首先,為大家普及一下,何為BA,為啥企業內部要存在BA. Business Analytics vs Business analysis,首先博主在這裡介紹的是Business analysis而非Business Analytics。好奇的盆友會問,二者究竟有啥子區別呀?

大牛請進Android視訊聊天各種實現方式問題

      最近專案需要,要完成一款區域網通訊軟體,主要實現Android端的文字、語音、視訊聊天功能,並且實現伺服器,伺服器端能實現客戶端上下線監控以及資源推送等簡單功能,但是伺服器端現在還沒人做,想找找有沒有現成的,減少工作量。        這兩天查了一些資料看主流

字符編碼

2個 明顯 更多 post pychar 16進制 滿足 就是 直接 1、存取文件不亂碼的法則:用什麽編碼存的,就要用什麽編碼讀 2、 decode encode bytes------------->unicode----

Taglib 原理和實現: 迴圈的Tag

1。問題:在request裡的 People 物件,有個屬性叫 men ,men 是一個Collection ,有許多個man 。現在,把 collection裡的man的名字都顯示出來  顯然,這是一個巢狀T

《機器學習》 周志華學習筆記 決策樹(課後習題)python 實現

一、基本內容 1.基本流程 決策樹的生成過程是一個遞迴過程,有三種情形會導致遞迴返回 (1)當前節點包含的yangben全屬於同一類別,無需劃分; (2)當前屬性集為空,或是所有yangben在所有屬性上的取值相同,無法劃分; (3)當前結點包含的yangben集合為空,不能

深入淺出學Vue開發:、Vue的生命週期及原始碼實現

歡迎大家訪問我的個人網站 - Sunday俱樂部 通過上面兩章的學習,我們已經學會了Vue的所有基礎語法,包括: 1、{{Mustache}} 語法 2、v-if、v-else、v-else-if、v-show 3、v-for 4、v-bind 5、v-mo

2018/12/01 一個64位作業系統的實現 匯入kernel.bin(2)

  在做程式4-1的實驗的時候, 我刪除了之前的虛擬軟盤和boot.bin、loader.bin、kernel.bin等二進位制檔案, 從頭開始新建虛擬軟盤等等, 試驗成功後. 我嘗試的將原來的kernel.bin 檔案刪除後, 將程式4-2中的kernel.bin檔案複製到bochs-2.6.9資料夾中,

2018/12/01 一個64位作業系統的實現 匯入kernel.bin(5)

參照之前的部落格, 我直接將程式4-5中生成的kernel.bin程式碼複製到bochs-2.6.9資料夾中, 使用部落格中的描述將kernel.bin載入虛擬軟盤的命令, 執行後, 得到成功的結果: 之後又按照書本上的要求將程式4-目錄下的main.c檔案中的 i = 1/0; 修

2018/12/01 一個64位操作系統的實現 內存管理(1)

文件的 ade png oot mage make 源文件 href http 本來打算刪掉源文件的kernel.bin文件, 然後直接用程序4-6中make生成的kernel.bin替代, 然而不行, 可能是我操作錯誤, 我直接將boot.img boot.bin loa

2018/12/01 一個64位操作系統的實現 導入kernel.bin(2)

64位操作系統 bin文件 color 文件刪除 mage inf 操作系統 技術分享 http   在做程序4-1的實驗的時候, 我刪除了之前的虛擬軟盤和boot.bin、loader.bin、kernel.bin等二進制文件, 從頭開始新建虛擬軟盤等等, 試驗成功後.

Runtime應用:實現NSCoding的自動歸檔和自動解檔

用runtime提供的函式遍歷Model自身所有屬性,並對屬性進行encode和decode操作。 通常系統自帶的資料型別,如:字典NSDictionary,陣列NSArray,字串NSString,

人工智慧:python 實現 NLP 天 A Bag Of Words

使用用詞袋(a bag of words)模型提取頻繁項文字分析的主要目標之一是將文字轉化為數值形式。以便使用機器進行學習。我們考慮下,數以百萬計的單詞文件,為了去分析這些文件,我們需要提取文字 並且將其轉化為數值符號。機器學習演算法需要處理數值的資料,以便他們能夠分析資料並

資料庫設計與實現

資料庫結構模型 概念資料模型(Concept Data Model,CDM)是一種面向使用者的系統資料模型,它用來描述現實世界的系統概念化資料結構。使資料庫設計人員在系統設計的初始階段,擺脫計算機系統及DBMS的具體技術問題,集中精力分析業務資料以及資料之間的聯絡等,描述系統的資料物件及其組成關

】 資源 之 4.2 內建Resource實現 ——跟我學spring3

4.2  內建Resource實現 4.2.1  ByteArrayResource        ByteArrayResource代表byte[]陣列資源,對於“getInputStream”操作將返回一個ByteArrayInputStream。 首先讓我們看下使用ByteArrayResource如

微信開發(從申請微信到註冊上線的一整套流程) 實現天氣預報功能

這一章裡,我們來實現微信上的天氣預報功能,我們使用方倍工作室的天氣預報介面,其介面為 http://apix.sinaapp.com/weather/ 這個介面的引數appkey為公眾號原始id,引數city為城市名 例如,查詢深圳的天氣預報時,將city值做urlenc