首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。java
碼字不易,轉載請註明出處!git
教程代碼:【Github傳送門】 |
---|
本文將結合前面系列文中介紹的MediaCodec、OpenGL、EGL、FBO、MediaMuxer等知識,實現對一個視頻的解碼,編輯,編碼,最後保存爲新視頻的流程。github
終於到了本篇章的最後一篇文章,前面的一系列文章中,圍繞OpenGL,介紹瞭如何使用OpenGL來實現視頻畫面的渲染和顯示,以及如何對視頻畫面進行編輯,有了以上基礎之後,咱們確定想把編輯好的視頻保存下來,實現整個編輯流程的閉環,本文就把最後一環補上。web
在【音視頻硬解碼流程:封裝基礎解碼框架】這篇文章中,介紹瞭如何使用Android原生提供的硬編解碼工具MediaCodec,對視頻進行解碼。同時,MediaCodec也能夠實現對音視頻的硬編碼。segmentfault
仍是先來看看官方的編解碼數據流圖緩存
在解碼的時候,經過 dequeueInputBuffer
查詢到一個空閒的輸入緩衝區,在經過 queueInputBuffer
將 未解碼
的數據壓入解碼器,最後,經過 dequeueOutputBuffer
獲得 解碼好
的數據。app
其實,編碼流程和解碼流程基本是同樣的。不一樣在於壓入 dequeueInputBuffer
輸入緩衝區的數據是 未編碼
的數據, 經過 dequeueOutputBuffer
獲得的是 編碼好
的數據。框架
依葫蘆畫瓢,仿照封裝解碼器的流程,來封裝一個基礎編碼器 BaseEncoder
。ide
完整代碼請查看 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個步驟:
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
在子類中配置具體的編碼參數,這裏暫不細說,定義音視頻編碼子類的時候再說。
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()
}
}
// ......
}
複製代碼
循環編碼放在 Runnable
的 run
方法中。
在 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
// ......
}
複製代碼
在編碼過程當中,若是發現 Frame
中 buffer
爲 null
,就認爲編碼已經完成了,沒有數據須要壓入了。這時,有兩種方法告訴編碼器結束編碼。
第一種,經過 queueInputBuffer
壓入一個空數據,而且將數據類型標記設置爲 MediaCodec.BUFFER_FLAG_END_OF_STREAM
。具體以下:
mCodec.queueInputBuffer(index, 0, 0,
frame.bufferInfo.presentationTimeUs,
MediaCodec.BUFFER_FLAG_END_OF_STREAM)
複製代碼
第二種,經過 signalEndOfInputStream
發送結束信號。
咱們已經知道,視頻是自動編碼,因此沒法經過第一種結束編碼,只能經過第二種方式結束編碼。
音頻是手動編碼,能夠經過第一種方式結束編碼。
一個坑
測試發現,視頻結束編碼的時候signalEndOfInputStream
以後,在獲取編碼數據輸出的時候,並無獲得結束編碼標記的數據,因此,上面的代碼中,若是是自動編碼,在判斷到Frame
的buffer
爲空時,直接將mIsEOF
設置爲true
了,退出了編碼流程。
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()
}
}
// ......
}
複製代碼
和解碼同樣,先查詢到一個可用的輸入緩衝索引,接着把數據壓入輸入緩衝。
這裏,先判斷是否結束編碼,是則往輸入緩衝壓入編碼結束標誌
把一幀數據壓入編碼器後,進入 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寫入到相應媒體通道中。
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中的媒體通道。
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、編碼器串聯起來,實現視頻編輯功能。
開始以前,須要改造一下【深刻了解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音視頻開發的大門,能夠去實現一些之前看起來很神祕的視頻效果,而後保存成一個真正的可播放的視頻。
這一系列文章每篇都很長,感謝每一個能閱讀到這裏的讀者,我以爲咱們都應該感謝一下本身,堅持真的很難。
最後無比感謝每一位給文章點贊、留言、提問、鼓勵的人兒,是大家讓冰冷的文字充滿溫情,是我堅持的動力。
我們,下一篇章,不見不散!