【聲 明】
首先,這一系列文章均基於自己的理解和實踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深入的知識網上也有許許多多的博文供大家學習了。
最後,寫文章過程中,會借鑑參考其他人分享的文章,會在文章最後列出,感謝這些作者的分享。
碼字不易,轉載請註明出處!
教程代碼:【Github傳送門】 |
---|
目錄
一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
- 1,初步瞭解OpenGL ES
- 2,使用OpenGL渲染視頻畫面
- 3,OpenGL渲染多視頻,實現畫中畫
- 4,深入瞭解OpenGL之EGL
- 5,OpenGL FBO數據緩衝區
- 6,Android音視頻硬編碼:生成一個MP4
三、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()
}
}
// ......
}
循環編碼放在 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
了,退出了編碼流程。
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音視頻開發的大門,可以去實現一些以前看起來很神祕的視頻效果,然後保存成一個真正的可播放的視頻。
這一系列文章每篇都很長,感謝每個能閱讀到這裏的讀者,我覺得我們都應該感謝一下自己,堅持真的很難。
最後無比感謝每一位給文章點贊、留言、提問、鼓勵的人兒,是你們讓冰冷的文字充滿溫情,是我堅持的動力。
咱們,下一篇章,不見不散!