【Android 音視頻開發打怪升級:OpenGL渲染視頻畫面篇】六、Android音視頻硬編碼:生成一個MP4

【聲 明】

首先,這一系列文章均基於自己的理解和實踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深入的知識網上也有許許多多的博文供大家學習了。
最後,寫文章過程中,會借鑑參考其他人分享的文章,會在文章最後列出,感謝這些作者的分享。

碼字不易,轉載請註明出處!

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
三、Android FFmpeg音視頻解碼篇
  • 1,FFmpeg so庫編譯
  • 2,Android 引入FFmpeg
  • 3,Android FFmpeg視頻解碼播放
  • 4,Android FFmpeg+OpenSL ES音頻解碼播放
  • 5,Android FFmpeg+OpenGL ES播放視頻
  • 6,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
  • 7,Android FFmpeg視頻編碼

本文你可以瞭解到

本文將結合前面系列文中介紹的MediaCodec、OpenGL、EGL、FBO、MediaMuxer等知識,實現對一個視頻的解碼,編輯,編碼,最後保存爲新視頻的流程。

終於到了本篇章的最後一篇文章,前面的一系列文章中,圍繞OpenGL,介紹瞭如何使用OpenGL來實現視頻畫面的渲染和顯示,以及如何對視頻畫面進行編輯,有了以上基礎以後,我們肯定想把編輯好的視頻保存下來,實現整個編輯流程的閉環,本文就把最後一環補上。

一、MediaCodec編碼器封裝

在【音視頻硬解碼流程:封裝基礎解碼框架】這篇文章中,介紹瞭如何使用Android原生提供的硬編解碼工具MediaCodec,對視頻進行解碼。同時,MediaCodec也可以實現對音視頻的硬編碼。

還是先來看看官方的編解碼數據流圖

  • 解碼流程

在解碼的時候,通過 dequeueInputBuffer 查詢到一個空閒的輸入緩衝區,在通過 queueInputBuffer未解碼 的數據壓入解碼器,最後,通過 dequeueOutputBuffer 得到 解碼好 的數據。

  • 編碼流程

其實,編碼流程和解碼流程基本是一樣的。不同在於壓入 dequeueInputBuffer 輸入緩衝區的數據是 未編碼 的數據, 通過 dequeueOutputBuffer 得到的是 編碼好 的數據。

依葫蘆畫瓢,仿照封裝解碼器的流程,來封裝一個基礎編碼器 BaseEncoder

1. 定義編碼器變量

完整代碼請查看 BaseEncoder

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {

    private val TAG = "BaseEncoder"

    // 目標視頻寬,只有視頻編碼的時候纔有效
    protected val mWidth: Int = width

    // 目標視頻高,只有視頻編碼的時候纔有效
    protected val mHeight: Int = height

    // Mp4合成器
    private var mMuxer: MMuxer = muxer

    // 線程運行
    private var mRunning = true

    // 編碼幀序列
    private var mFrames = mutableListOf<Frame>()

    // 編碼器
    private lateinit var mCodec: MediaCodec

    // 當前編碼幀信息
    private val mBufferInfo = MediaCodec.BufferInfo()

    // 編碼輸出緩衝區
    private var mOutputBuffers: Array<ByteBuffer>? = null

    // 編碼輸入緩衝區
    private var mInputBuffers: Array<ByteBuffer>? = null

    private var mLock = Object()

    // 是否編碼結束
    private var mIsEOS = false

    // 編碼狀態監聽器
    private var mStateListener: IEncodeStateListener? = null
    
    // ......
}

首先,這是一個 abstract 抽象類,並且繼承 Runnable ,上面先定義需要用到的內部變量。基本和解碼類似。

要注意的是這裏的寬高只對視頻有效,MMuxer 是之前在【Mp4重打包】的是時候定義的Mp4封裝工具。還有一個緩存隊列mFrames,用來緩存需要編碼的幀數據。

關於如何把數據寫入到mp4中,本文不再重述,請查看【Mp4重打包】。

其中一幀數據定義如下:

class Frame {
    //未編碼數據
    var buffer: ByteBuffer? = null

    //未編碼數據信息
    var bufferInfo = MediaCodec.BufferInfo()
    private set

    fun setBufferInfo(info: MediaCodec.BufferInfo) {
        bufferInfo.set(info.offset, info.size, info.presentationTimeUs, info.flags)
    }
}

編碼流程相對於解碼流程來說比較簡單,分爲3個步驟:

  • 初始化編碼器
  • 將數據壓入編碼器
  • 從編碼器取出數據,並壓入mp4

2. 初始化編碼器

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {

    //省略其他代碼......
    
    init {
        initCodec()
    }
    
    /**
     * 初始化編碼器
     */
    private fun initCodec() {
        mCodec = MediaCodec.createEncoderByType(encodeType())
        configEncoder(mCodec)
        mCodec.start()
        mOutputBuffers = mCodec.outputBuffers
        mInputBuffers = mCodec.inputBuffers
    }
    
    
    /**
     * 編碼類型
     */
    abstract fun encodeType(): String

    /**
     * 子類配置編碼器
     */
    abstract fun configEncoder(codec: MediaCodec)
    
    // .......
}

這裏定義了兩個虛函數,子類必須實現。一個用於配置音頻和視頻對應的編碼類型,如視頻編碼爲h264對應的編碼類型爲:"video/avc" ;音頻編碼爲AAC對應的編碼類型爲:"audio/mp4a-latm"

根據獲取到的編碼類型,就可以初始化得到一個編碼器。

接着,調用 configEncoder 在子類中配置具體的編碼參數,這裏暫不細說,定義音視頻編碼子類的時候再說。

2. 開啓編碼循環

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    // 省略其他代碼......
    
    override fun run() {
        loopEncode()
        done()
    }
    
    /**
     * 循環編碼
     */
    private fun loopEncode() {
        while (mRunning && !mIsEOS) {
            val empty = synchronized(mFrames) {
                mFrames.isEmpty()
            }
            if (empty) {
                justWait()
            }
            if (mFrames.isNotEmpty()) {
                val frame = synchronized(mFrames) {
                    mFrames.removeAt(0)
                }

                if (encodeManually()) {
                    //【1. 數據壓入編碼】
                    encode(frame)
                } else if (frame.buffer == null) { // 如果是自動編碼(比如視頻),遇到結束幀的時候,直接結束掉
                    // This may only be used with encoders receiving input from a Surface
                    mCodec.signalEndOfInputStream()
                    mIsEOS = true
                }
            }
            //【2. 拉取編碼好的數據】
            drain()
        }
    }
    
    // ......
}

循環編碼放在 Runnablerun 方法中。

loopEncode 中,將前面提到的 2(壓數據)3(取數據) 合併在一起。邏輯也比較簡單。

判斷未編碼的緩存隊列是否爲空,是則線程掛起,進入等待;否則編碼數據,和取出數據。

有2點需要注意:

  • 音頻和視頻的編碼流程稍微有點區別

音頻編碼 需要我們自己將數據壓入編碼器,實現數據的編碼。

視頻編碼 的時候,可以通過將 Surface 綁定給 OpenGL ,系統自動從 Surface 中去數據,實現自動編碼。也就是說,不需要用戶自己手動壓入數據,只需從輸出緩衝中取數據就可以了。

因此,這裏定義一個虛函數,由子類控制是否需要手動壓入數據,默認爲true:手動壓入。

下文中,將這兩種形式分別叫做:手動編碼自動編碼

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {

    // 省略其他代碼......
    
    /**
     * 是否手動編碼
     * 視頻:false 音頻:true
     *
     * 注:視頻編碼通過Surface,MediaCodec自動完成編碼;音頻數據需要用戶自己壓入編碼緩衝區,完成編碼
     */
    open fun encodeManually() = true
    
    
    // ......
}
  • 結束編碼

在編碼過程中,如果發現 Framebuffernull ,就認爲編碼已經完成了,沒有數據需要壓入了。這時,有兩種方法告訴編碼器結束編碼。

第一種,通過 queueInputBuffer 壓入一個空數據,並且將數據類型標記設置爲 MediaCodec.BUFFER_FLAG_END_OF_STREAM 。具體如下:

mCodec.queueInputBuffer(index, 0, 0,
    frame.bufferInfo.presentationTimeUs,
    MediaCodec.BUFFER_FLAG_END_OF_STREAM)

第二種,通過 signalEndOfInputStream 發送結束信號。

我們已經知道,視頻是自動編碼,所以無法通過第一種結束編碼,只能通過第二種方式結束編碼。

音頻是手動編碼,可以通過第一種方式結束編碼。

一個坑
測試發現,視頻結束編碼的時候 signalEndOfInputStream 之後,在獲取編碼數據輸出的時候,並沒有得到結束編碼標記的數據,所以,上面的代碼中,如果是自動編碼,在判斷到 Framebuffer 爲空時,直接將 mIsEOF 設置爲 true 了,退出了編碼流程。

3. 手動編碼

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其他代碼......
    
    /**
     * 編碼
     */
    private fun encode(frame: Frame) {

        val index = mCodec.dequeueInputBuffer(-1)

        /*向編碼器輸入數據*/
        if (index >= 0) {
            val inputBuffer = mInputBuffers!![index]
            inputBuffer.clear()
            if (frame.buffer != null) {
                inputBuffer.put(frame.buffer)
            }
            if (frame.buffer == null || frame.bufferInfo.size <= 0) { // 小於等於0時,爲音頻結束符標記
                mCodec.queueInputBuffer(index, 0, 0,
                    frame.bufferInfo.presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
            } else {
                mCodec.queueInputBuffer(index, 0, frame.bufferInfo.size,
                    frame.bufferInfo.presentationTimeUs, 0)
            }
            frame.buffer?.clear()
        }
    }
    
    // ......
}

和解碼一樣,先查詢到一個可用的輸入緩衝索引,接着把數據壓入輸入緩衝。

這裏,先判斷是否結束編碼,是則往輸入緩衝壓入編碼結束標誌

4. 拉取數據

把一幀數據壓入編碼器後,進入 drain 方法,顧名思義,我們要把編碼器輸出緩衝中的數據,全部抽乾。所以這裏是一個while循環,直到輸出緩衝沒有數據 MediaCodec.INFO_TRY_AGAIN_LATER ,或者編碼結束 MediaCodec.BUFFER_FLAG_END_OF_STREAM

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其他代碼......
    
    /**
     * 榨乾編碼輸出數據
     */
    private fun drain() {
        loop@ while (!mIsEOS) {
            val index = mCodec.dequeueOutputBuffer(mBufferInfo, 0)
            when (index) {
                MediaCodec.INFO_TRY_AGAIN_LATER -> break@loop
                MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    addTrack(mMuxer, mCodec.outputFormat)
                }
                MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
                    mOutputBuffers = mCodec.outputBuffers
                }
                else -> {
                    if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                        mIsEOS = true
                        mBufferInfo.set(0, 0, 0, mBufferInfo.flags)
                    }

                    if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                        // SPS or PPS, which should be passed by MediaFormat.
                        mCodec.releaseOutputBuffer(index, false)
                        continue@loop
                    }

                    if (!mIsEOS) {
                        writeData(mMuxer, mOutputBuffers!![index], mBufferInfo)
                    }
                    mCodec.releaseOutputBuffer(index, false)
                }
            }
        }
    }
    
    
    /**
     * 配置mp4音視頻軌道
     */
    abstract fun addTrack(muxer: MMuxer, mediaFormat: MediaFormat)

    /**
     * 往mp4寫入音視頻數據
     */
    abstract fun writeData(muxer: MMuxer, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo)
    
    // ......
}

很重要的一點
mCodec.dequeueOutputBuffer 返回的是 MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 時,說明編碼參數格式已經生成(比如視頻的碼率,幀率,SPS/PPS幀信息等),需要把這些信息寫入到mp4對應媒體軌道中(這裏通過 addTrack 在子類中配置音視頻對應的編碼格式),之後才能開始將編碼完成的數據,通過MediaMuxer寫入到相應媒體通道中。

5. 退出編碼,釋放資源

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其他代碼......

    /**
     * 編碼結束,是否資源
     */
    private fun done() {
        try {
            release(mMuxer)
            mCodec.stop()
            mCodec.release()
            mRunning = false
            mStateListener?.encoderFinish(this)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    
    /**
     * 釋放子類資源
     */
    abstract fun release(muxer: MMuxer)
    
    // ......
}

調用子類中的虛函數 release ,子類需要根據自己的媒體類型,釋放對應mp4中的媒體通道。

6. 一些外部調用的方法

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其他代碼......
    
    /**
     * 將一幀數據壓入隊列,等待編碼
     */
    fun encodeOneFrame(frame: Frame) {
        synchronized(mFrames) {
            mFrames.add(frame)
            notifyGo()
        }
        // 延時一點時間,避免掉幀
        Thread.sleep(frameWaitTimeMs())
    }

    /**
     * 通知結束編碼
     */
    fun endOfStream() {
        Log.e("ccccc","endOfStream")
        synchronized(mFrames) {
            val frame = Frame()
            frame.buffer = null
            mFrames.add(frame)
            notifyGo()
        }
    }
    
    /**
     * 設置狀態監聽器
     */
    fun setStateListener(l: IEncodeStateListener) {
        this.mStateListener = l
    }
    
    
    /**
     * 每一幀排隊等待時間
     */
    open fun frameWaitTimeMs() = 20L
    
    // ......
}

這裏有點需要注意,在把數據壓入排隊隊列之後,做了一個默認 20ms 的延時,同時子類可以通過重寫 frameWaitTimeMs 方法修改時間。

一個是爲了避免音頻解碼過快,導致數據堆積太多,音頻在子類中重新設置等待爲5ms,具體見子類 AudioEncoder 代碼。

另一個是因爲由於視頻是系統自動獲取Surface數據,如果解碼數據刷新太快,可能會導致漏幀,這裏使用默認的20ms。

因此這裏做了一個簡單粗暴的延時,但並非最好的解決方式

二、視頻編碼器

有了基礎封裝,寫一個視頻編碼器還不是so easy的事嗎?

反手就貼出一個視頻編碼器:

const val DEFAULT_ENCODE_FRAME_RATE = 30

class VideoEncoder(muxer: MMuxer, width: Int, height: Int): BaseEncoder(muxer, width, height) {

    private val TAG = "VideoEncoder"
    
    private var mSurface: Surface? = null

    override fun encodeType(): String {
        return "video/avc"
    }

    override fun configEncoder(codec: MediaCodec) {
        if (mWidth <= 0 || mHeight <= 0) {
            throw IllegalArgumentException("Encode width or height is invalid, width: $mWidth, height: $mHeight")
        }
        val bitrate = 3 * mWidth * mHeight
        val outputFormat = MediaFormat.createVideoFormat(encodeType(), mWidth, mHeight)
        outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
        outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, DEFAULT_ENCODE_FRAME_RATE)
        outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)

        try {
            configEncoderWithCQ(codec, outputFormat)
        } catch (e: Exception) {
            e.printStackTrace()
            // 捕獲異常,設置爲系統默認配置 BITRATE_MODE_VBR
            try {
                configEncoderWithVBR(codec, outputFormat)
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, "配置視頻編碼器失敗")
            }
        }

        mSurface = codec.createInputSurface()
    }

    private fun configEncoderWithCQ(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 本部分手機不支持 BITRATE_MODE_CQ 模式,有可能會異常
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    private fun configEncoderWithVBR(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    override fun addTrack(muxer: MMuxer, mediaFormat: MediaFormat) {
        muxer.addVideoTrack(mediaFormat)
    }

    override fun writeData(
        muxer: MMuxer,
        byteBuffer: ByteBuffer,
        bufferInfo: MediaCodec.BufferInfo
    ) {
        muxer.writeVideoData(byteBuffer, bufferInfo)
    }

    override fun encodeManually(): Boolean {
        return false
    }

    override fun release(muxer: MMuxer) {
        muxer.releaseVideoTrack()
    }

    fun getEncodeSurface(): Surface? {
        return mSurface
    }
}

繼承了 BaseEncoder 實現所有的虛函數就可以了。

重點來看 configEncoder 這個方法。

i. 配置了碼率 KEY_BIT_RATE

計算公式源自【MediaCodec編碼OpenGL速度和清晰度均衡

Biterate = Width * Height * FrameRate * Factor 

Factor: 0.1~0.2

ii. 配置幀率 KEY_FRAME_RATE ,這裏爲30幀/秒
iii. 配置關鍵幀出現頻率 KEY_I_FRAME_INTERVAL ,這裏爲1幀/秒
iv. 配置數據來源 KEY_COLOR_FORMAT ,爲 COLOR_FormatSurface,既來自 Surface
v. 配置碼率模式 KEY_BITRATE_MODE

- BITRATE_MODE_CQ 忽略用戶設置的碼率,由編碼器自己控制碼率,並儘可能保證畫面清晰度和碼率的均衡  
- BITRATE_MODE_CBR 無論視頻的畫面內容如果,儘可能遵守用戶設置的碼率  
- BITRATE_MODE_VBR 儘可能遵守用戶設置的碼率,但是會根據幀畫面之間運動矢量  
(通俗理解就是幀與幀之間的畫面變化程度)來動態調整碼率,如果運動矢量較大,則在該時間段將碼率調高,如果畫面變換很小,則碼率降低。 

優先選擇 BITRATE_MODE_CQ ,如果編碼器不支持,切換回系統默認的 BITRATE_MODE_VBR

vi. 最後,通過編碼器 codec.createInputSurface() 新建一個 Surface ,用於 EGL 的窗口綁定。視頻解碼得到的畫面都將渲染到這個 Surface 中,MediaCodec自動從裏面取出數據,並編碼。

三、音頻編碼器

音頻編碼器則更加簡單。

// 編碼採樣率率
val DEST_SAMPLE_RATE = 44100
// 編碼碼率
private val DEST_BIT_RATE = 128000

class AudioEncoder(muxer: MMuxer): BaseEncoder(muxer) {

    private val TAG = "AudioEncoder"

    override fun encodeType(): String {
        return "audio/mp4a-latm"
    }

    override fun configEncoder(codec: MediaCodec) {
        val audioFormat = MediaFormat.createAudioFormat(encodeType(), DEST_SAMPLE_RATE, 2)
        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, DEST_BIT_RATE)
        audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100*1024)
        try {
            configEncoderWithCQ(codec, audioFormat)
        } catch (e: Exception) {
            e.printStackTrace()
            try {
                configEncoderWithVBR(codec, audioFormat)
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, "配置音頻編碼器失敗")
            }
        }
    }

    private fun configEncoderWithCQ(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 本部分手機不支持 BITRATE_MODE_CQ 模式,有可能會異常
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    private fun configEncoderWithVBR(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    override fun addTrack(muxer: MMuxer, mediaFormat: MediaFormat) {
        muxer.addAudioTrack(mediaFormat)
    }

    override fun writeData(
        muxer: MMuxer,
        byteBuffer: ByteBuffer,
        bufferInfo: MediaCodec.BufferInfo
    ) {
        muxer.writeAudioData(byteBuffer, bufferInfo)
    }

    override fun release(muxer: MMuxer) {
        muxer.releaseAudioTrack()
    }
}

可以看到,configEncoder 實現也比較簡單:

i. 設置音頻比特率 MediaFormat.KEY_BIT_RATE,這裏設置爲 128000
ii. 設置輸入緩衝區大小 KEY_MAX_INPUT_SIZE ,這裏設置爲 100*1024

四、整合

音頻和視頻的編碼工具已經完成,接下來就來看看,如何把解碼器、OpenGL、EGL、編碼器串聯起來,實現視頻編輯功能。

  • 改造EGL渲染器

開始之前,需要改造一下【深入瞭解OpenGL之EGL】 這篇文章中定義的EGL渲染器。

i. 在之前定義的渲染器中,只支持設置一個SurfaceView,並綁定到 EGL 顯示窗口中。這裏需要讓它支持設置一個Surface,接收來自 VideoEncoder 中創建的Surface作爲渲染窗口。

ii. 由於是要對窗口的畫面進行編碼,所以無需在渲染器中不斷的刷新畫面,只要在視頻解碼器解碼出一幀的時候,刷新一下畫面即可。同時把當前幀的時間戳傳遞給OpenGL。

完整代碼如下,已經將新增的部分標記出來:

class CustomerGLRenderer : SurfaceHolder.Callback {

    private val mThread = RenderThread()

    private var mSurfaceView: WeakReference<SurfaceView>? = null

    private var mSurface: Surface? = null

    private val mDrawers = mutableListOf<IDrawer>()

    init {
        mThread.start()
    }

    fun setSurface(surface: SurfaceView) {
        mSurfaceView = WeakReference(surface)
        surface.holder.addCallback(this)

        surface.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener{
            override fun onViewDetachedFromWindow(v: View?) {
                stop()
            }

            override fun onViewAttachedToWindow(v: View?) {
            }
        })
    }

//-------------------新增部分-----------------

    // 新增設置Surface接口
    fun setSurface(surface: Surface, width: Int, height: Int) {
        mSurface = surface
        mThread.onSurfaceCreate()
        mThread.onSurfaceChange(width, height)
    }

    // 新增設置渲染模式 RenderMode見下面
    fun setRenderMode(mode: RenderMode) {
        mThread.setRenderMode(mode)
    }

    // 新增通知更新畫面方法
    fun notifySwap(timeUs: Long) {
        mThread.notifySwap(timeUs)
    }
/----------------------------------------------

    fun addDrawer(drawer: IDrawer) {
        mDrawers.add(drawer)
    }

    fun stop() {
        mThread.onSurfaceStop()
        mSurface = null
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        mSurface = holder.surface
        mThread.onSurfaceCreate()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        mThread.onSurfaceChange(width, height)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        mThread.onSurfaceDestroy()
    }

    inner class RenderThread: Thread() {

        // 渲染狀態
        private var mState = RenderState.NO_SURFACE

        private var mEGLSurface: EGLSurfaceHolder? = null

        // 是否綁定了EGLSurface
        private var mHaveBindEGLContext = false

        //是否已經新建過EGL上下文,用於判斷是否需要生產新的紋理ID
        private var mNeverCreateEglContext = true

        private var mWidth = 0
        private var mHeight = 0

        private val mWaitLock = Object()

        private var mCurTimestamp = 0L

        private var mLastTimestamp = 0L

        private var mRenderMode = RenderMode.RENDER_WHEN_DIRTY

        private fun holdOn() {
            synchronized(mWaitLock) {
                mWaitLock.wait()
            }
        }

        private fun notifyGo() {
            synchronized(mWaitLock) {
                mWaitLock.notify()
            }
        }

        fun setRenderMode(mode: RenderMode) {
            mRenderMode = mode
        }

        fun onSurfaceCreate() {
            mState = RenderState.FRESH_SURFACE
            notifyGo()
        }

        fun onSurfaceChange(width: Int, height: Int) {
            mWidth = width
            mHeight = height
            mState = RenderState.SURFACE_CHANGE
            notifyGo()
        }

        fun onSurfaceDestroy() {
            mState = RenderState.SURFACE_DESTROY
            notifyGo()
        }

        fun onSurfaceStop() {
            mState = RenderState.STOP
            notifyGo()
        }

        fun notifySwap(timeUs: Long) {
            synchronized(mCurTimestamp) {
                mCurTimestamp = timeUs
            }
            notifyGo()
        }

        override fun run() {
            initEGL()
            while (true) {
                when (mState) {
                    RenderState.FRESH_SURFACE -> {
                        createEGLSurfaceFirst()
                        holdOn()
                    }
                    RenderState.SURFACE_CHANGE -> {
                        createEGLSurfaceFirst()
                        GLES20.glViewport(0, 0, mWidth, mHeight)
                        configWordSize()
                        mState = RenderState.RENDERING
                    }
                    RenderState.RENDERING -> {
                        render()
                        
                        //新增判斷:如果是 `RENDER_WHEN_DIRTY` 模式,渲染後,把線程掛起,等待下一幀
                        if (mRenderMode == RenderMode.RENDER_WHEN_DIRTY) {
                            holdOn()
                        }
                    }
                    RenderState.SURFACE_DESTROY -> {
                        destroyEGLSurface()
                        mState = RenderState.NO_SURFACE
                    }
                    RenderState.STOP -> {
                        releaseEGL()
                        return
                    }
                    else -> {
                        holdOn()
                    }
                }
                if (mRenderMode == RenderMode.RENDER_CONTINUOUSLY) {
                    sleep(16)
                }
            }
        }

        private fun initEGL() {
            mEGLSurface = EGLSurfaceHolder()
            mEGLSurface?.init(null, EGL_RECORDABLE_ANDROID)
        }

        private fun createEGLSurfaceFirst() {
            if (!mHaveBindEGLContext) {
                mHaveBindEGLContext = true
                createEGLSurface()
                if (mNeverCreateEglContext) {
                    mNeverCreateEglContext = false
                    GLES20.glClearColor(0f, 0f, 0f, 0f)
                    //開啓混合,即半透明
                    GLES20.glEnable(GLES20.GL_BLEND)
                    GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
                    generateTextureID()
                }
            }
        }

        private fun createEGLSurface() {
            mEGLSurface?.createEGLSurface(mSurface)
            mEGLSurface?.makeCurrent()
        }

        private fun generateTextureID() {
            val textureIds = OpenGLTools.createTextureIds(mDrawers.size)
            for ((idx, drawer) in mDrawers.withIndex()) {
                drawer.setTextureID(textureIds[idx])
            }
        }

        private fun configWordSize() {
            mDrawers.forEach { it.setWorldSize(mWidth, mHeight) }
        }

// ---------------------修改部分代碼------------------------
        // 根據渲染模式和當前幀的時間戳判斷是否需要重新刷新畫面
        private fun render() {
            val render = if (mRenderMode == RenderMode.RENDER_CONTINUOUSLY) {
                true
            } else {
                synchronized(mCurTimestamp) {
                    if (mCurTimestamp > mLastTimestamp) {
                        mLastTimestamp = mCurTimestamp
                        true
                    } else {
                        false
                    }
                }
            }

            if (render) {
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
                mDrawers.forEach { it.draw() }
                mEGLSurface?.setTimestamp(mCurTimestamp)
                mEGLSurface?.swapBuffers()
            }
        }
        
//------------------------------------------------------

        private fun destroyEGLSurface() {
            mEGLSurface?.destroyEGLSurface()
            mHaveBindEGLContext = false
        }

        private fun releaseEGL() {
            mEGLSurface?.release()
        }
    }

    /**
     * 渲染狀態
     */
    enum class RenderState {
        NO_SURFACE, //沒有有效的surface
        FRESH_SURFACE, //持有一個未初始化的新的surface
        SURFACE_CHANGE, //surface尺寸變化
        RENDERING, //初始化完畢,可以開始渲染
        SURFACE_DESTROY, //surface銷燬
        STOP //停止繪製
    }

//---------新增渲染模式定義------------
    enum class RenderMode {
        // 自動循環渲染
        RENDER_CONTINUOUSLY,
        // 由外部通過notifySwap通知渲染
        RENDER_WHEN_DIRTY
    }
//-------------------------------------
}

新增部分已經標出來,也不復雜,主要是新增了設置Surface,區分了兩種渲染模式,請大家看代碼即可。

  • 改造解碼器

還記得之前的文章中提到,音視頻要正常播放,需要對音頻和視頻進行音視頻同步嗎?

而由於編碼的時候,並不需要把視頻畫面和音頻播放出來,所以可以把音視頻同步去掉,加快編碼速度。

修改也很簡單,在 BaseDecoder 中新增一個變量 mSyncRender ,如果 mSyncRender == false ,就把音視頻同步去掉。

這裏,只列出修改的部分,完整代碼請看 BaseDecoder

abstract class BaseDecoder(private val mFilePath: String): IDecoder {
    
    // 省略無關代碼......
    
    // 是否需要音視頻渲染同步
    private var mSyncRender = true
    
    
    final override fun run() {
        //省略無關代碼...
        
        while (mIsRunning) {
            // ......
            
            // ---------【音視頻同步】-------------
            if (mSyncRender && mState == DecodeState.DECODING) {
                sleepRender()
            }
            
            if (mSyncRender) {// 如果只是用於編碼合成新視頻,無需渲染
                render(mOutputBuffers!![index], mBufferInfo)
            }
            
            // ......
        }
        //
    }
    
    override fun withoutSync(): IDecoder {
        mSyncRender = false
        return this
    }
    
    //......
}
  • 整合
class SynthesizerActivity: AppCompatActivity(), MMuxer.IMuxerStateListener {

    private val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_2.mp4"
    private val path2 = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"

    private val threadPool = Executors.newFixedThreadPool(10)

    private var renderer = CustomerGLRenderer()

    private var audioDecoder: IDecoder? = null
    private var videoDecoder: IDecoder? = null

    private lateinit var videoEncoder: VideoEncoder
    private lateinit var audioEncoder: AudioEncoder

    private var muxer = MMuxer()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_synthesizer)
        muxer.setStateListener(this)
    }

    fun onStartClick(view: View) {
        btn.text = "正在編碼"
        btn.isEnabled = false
        initVideo()
        initAudio()
        initAudioEncoder()
        initVideoEncoder()
    }

    private fun initVideoEncoder() {
        // 視頻編碼器
        videoEncoder = VideoEncoder(muxer, 1920, 1080)

        renderer.setRenderMode(CustomerGLRenderer.RenderMode.RENDER_WHEN_DIRTY)
        renderer.setSurface(videoEncoder.getEncodeSurface()!!, 1920, 1080)

        videoEncoder.setStateListener(object : DefEncodeStateListener {
            override fun encoderFinish(encoder: BaseEncoder) {
                renderer.stop()
            }
        })
        threadPool.execute(videoEncoder)
    }

    private fun initAudioEncoder() {
        // 音頻編碼器
        audioEncoder = AudioEncoder(muxer)
        // 啓動編碼線程
        threadPool.execute(audioEncoder)
    }

    private fun initVideo() {
        val drawer = VideoDrawer()
        drawer.setVideoSize(1920, 1080)
        drawer.getSurfaceTexture {
            initVideoDecoder(path, Surface(it))
        }
        renderer.addDrawer(drawer)
    }

    private fun initVideoDecoder(path: String, sf: Surface) {
        videoDecoder?.stop()
        videoDecoder = VideoDecoder(path, null, sf).withoutSync()
        videoDecoder!!.setStateListener(object : DefDecodeStateListener {
            override fun decodeOneFrame(decodeJob: BaseDecoder?, frame: Frame) {
                renderer.notifySwap(frame.bufferInfo.presentationTimeUs)
                videoEncoder.encodeOneFrame(frame)
            }

            override fun decoderFinish(decodeJob: BaseDecoder?) {
                videoEncoder.endOfStream()
            }
        })
        videoDecoder!!.goOn()

        //啓動解碼線程
        threadPool.execute(videoDecoder!!)
    }

    private fun initAudio() {
        audioDecoder?.stop()
        audioDecoder = AudioDecoder(path).withoutSync()
        audioDecoder!!.setStateListener(object : DefDecodeStateListener {

            override fun decodeOneFrame(decodeJob: BaseDecoder?, frame: Frame) {
                audioEncoder.encodeOneFrame(frame)
            }

            override fun decoderFinish(decodeJob: BaseDecoder?) {
                audioEncoder.endOfStream()
            }
        })
        audioDecoder!!.goOn()

        //啓動解碼線程
        threadPool.execute(audioDecoder!!)
    }

    override fun onMuxerFinish() {
    
        runOnUiThread {
            btn.isEnabled = true
            btn.text = "編碼完成"
        }

        audioDecoder?.stop()
        audioDecoder = null

        videoDecoder?.stop()
        videoDecoder = null
    }
}

可以看到,過程很簡單:初始化解碼器,初始化EGL Render,初始化編碼器,然後將解碼得到的數據扔到編碼器隊列中,監聽解碼狀態和編碼狀態,做相應的操作。

解碼過程和使用EGL播放視頻基本是一樣的,只是渲染模式不同而已。

在這個代碼中,只是簡單的將原視頻解碼,渲染到OpenGL,重新編碼成新的mp4,也就是說輸出的視頻和原視頻是一模一樣的。

  • 可以實現什麼?

雖然上面只是一個普通的解碼和編碼的過程,但是卻可以衍生出無限的想象。

比如:

  • 實現視頻裁剪:給解碼器設置一個開始和結束的時間即可。

  • 實現炫酷的視頻畫面編輯:比如將視頻渲染器 VideoDrawer 換成之前寫好的 SoulVideoDrawer 的話,將得到一個有 靈魂出竅 效果的視頻;結合之前的畫中畫,可以實現視頻的疊加。

  • 視頻拼接:結合多個視頻解碼器,將多個視頻連接起來,編碼成新的視頻。

  • 加水印:結合OpenGL渲染圖片,加個水印超簡單的。

只要有想象力,那都不是事!

五、結束語

啊~~~,嗨森,終於寫完本系列的【OpenGL渲染視頻畫面篇】,到目前爲止,如果你看過每一篇文章,並且動手碼過代碼,我相信你一定已經踏入了Android音視頻開發的大門,可以去實現一些以前看起來很神祕的視頻效果,然後保存成一個真正的可播放的視頻。

這一系列文章每篇都很長,感謝每個能閱讀到這裏的讀者,我覺得我們都應該感謝一下自己,堅持真的很難。

最後無比感謝每一位給文章點贊、留言、提問、鼓勵的人兒,是你們讓冰冷的文字充滿溫情,是我堅持的動力。

咱們,下一篇章,不見不散!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章