【聲 明】
首先,這一系列文章均基于自己的理解和實踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了。
最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。
碼字不易,轉(zhuǎn)載請注明出處!
| 教程代碼:【Github傳送門】 |
|---|
目錄
一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染視頻畫面
- 3,OpenGL渲染多視頻,實現(xiàn)畫中畫
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO數(shù)據(jù)緩沖區(qū)
- 6,Android音視頻硬編碼:生成一個MP4
三、Android FFmpeg音視頻解碼篇
- 1,F(xiàn)Fmpeg so庫編譯
- 2,Android 引入FFmpeg
- 3,Android FFmpeg視頻解碼播放
- 4,Android FFmpeg+OpenSL ES音頻解碼播放
- 5,Android FFmpeg+OpenGL ES播放視頻
- 6,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
- 7,Android FFmpeg視頻編碼
本文你可以了解到
本文將結(jié)合前面系列文中介紹的MediaCodec、OpenGL、EGL、FBO、MediaMuxer等知識,實現(xiàn)對一個視頻的解碼,編輯,編碼,最后保存為新視頻的流程。
終于到了本篇章的最后一篇文章,前面的一系列文章中,圍繞OpenGL,介紹了如何使用OpenGL來實現(xiàn)視頻畫面的渲染和顯示,以及如何對視頻畫面進行編輯,有了以上基礎(chǔ)以后,我們肯定想把編輯好的視頻保存下來,實現(xiàn)整個編輯流程的閉環(huán),本文就把最后一環(huán)補上。
一、MediaCodec編碼器封裝
在【音視頻硬解碼流程:封裝基礎(chǔ)解碼框架】這篇文章中,介紹了如何使用Android原生提供的硬編解碼工具MediaCodec,對視頻進行解碼。同時,MediaCodec也可以實現(xiàn)對音視頻的硬編碼。
還是先來看看官方的編解碼數(shù)據(jù)流圖
- 解碼流程
在解碼的時候,通過 dequeueInputBuffer 查詢到一個空閑的輸入緩沖區(qū),在通過 queueInputBuffer 將 未解碼 的數(shù)據(jù)壓入解碼器,最后,通過 dequeueOutputBuffer 得到 解碼好 的數(shù)據(jù)。
- 編碼流程
其實,編碼流程和解碼流程基本是一樣的。不同在于壓入 dequeueInputBuffer 輸入緩沖區(qū)的數(shù)據(jù)是 未編碼 的數(shù)據(jù), 通過 dequeueOutputBuffer 得到的是 編碼好 的數(shù)據(jù)。
依葫蘆畫瓢,仿照封裝解碼器的流程,來封裝一個基礎(chǔ)編碼器 BaseEncoder 。
1. 定義編碼器變量
完整代碼請查看 BaseEncoder
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
private val TAG = "BaseEncoder"
// 目標(biāo)視頻寬,只有視頻編碼的時候才有效
protected val mWidth: Int = width
// 目標(biāo)視頻高,只有視頻編碼的時候才有效
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
// 當(dāng)前編碼幀信息
private val mBufferInfo = MediaCodec.BufferInfo()
// 編碼輸出緩沖區(qū)
private var mOutputBuffers: Array<ByteBuffer>? = null
// 編碼輸入緩沖區(qū)
private var mInputBuffers: Array<ByteBuffer>? = null
private var mLock = Object()
// 是否編碼結(jié)束
private var mIsEOS = false
// 編碼狀態(tài)監(jiān)聽器
private var mStateListener: IEncodeStateListener? = null
// ......
}
首先,這是一個 abstract 抽象類,并且繼承 Runnable ,上面先定義需要用到的內(nèi)部變量?;竞徒獯a類似。
要注意的是這里的寬高只對視頻有效,
MMuxer是之前在【Mp4重打包】的是時候定義的Mp4封裝工具。還有一個緩存隊列mFrames,用來緩存需要編碼的幀數(shù)據(jù)。
關(guān)于如何把數(shù)據(jù)寫入到mp4中,本文不再重述,請查看【Mp4重打包】。
其中一幀數(shù)據(jù)定義如下:
class Frame {
//未編碼數(shù)據(jù)
var buffer: ByteBuffer? = null
//未編碼數(shù)據(jù)信息
var bufferInfo = MediaCodec.BufferInfo()
private set
fun setBufferInfo(info: MediaCodec.BufferInfo) {
bufferInfo.set(info.offset, info.size, info.presentationTimeUs, info.flags)
}
}
編碼流程相對于解碼流程來說比較簡單,分為3個步驟:
- 初始化編碼器
- 將數(shù)據(jù)壓入編碼器
- 從編碼器取出數(shù)據(jù),并壓入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)
// .......
}
這里定義了兩個虛函數(shù),子類必須實現(xiàn)。一個用于配置音頻和視頻對應(yīng)的編碼類型,如視頻編碼為h264對應(yīng)的編碼類型為:"video/avc" ;音頻編碼為AAC對應(yīng)的編碼類型為:"audio/mp4a-latm" 。
根據(jù)獲取到的編碼類型,就可以初始化得到一個編碼器。
接著,調(diào)用 configEncoder 在子類中配置具體的編碼參數(shù),這里暫不細說,定義音視頻編碼子類的時候再說。
2. 開啟編碼循環(huán)
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
// 省略其他代碼......
override fun run() {
loopEncode()
done()
}
/**
* 循環(huán)編碼
*/
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. 數(shù)據(jù)壓入編碼】
encode(frame)
} else if (frame.buffer == null) { // 如果是自動編碼(比如視頻),遇到結(jié)束幀的時候,直接結(jié)束掉
// This may only be used with encoders receiving input from a Surface
mCodec.signalEndOfInputStream()
mIsEOS = true
}
}
//【2. 拉取編碼好的數(shù)據(jù)】
drain()
}
}
// ......
}
循環(huán)編碼放在 Runnable 的 run 方法中。
在 loopEncode 中,將前面提到的 2(壓數(shù)據(jù)) 和 3(取數(shù)據(jù)) 合并在一起。邏輯也比較簡單。
判斷未編碼的緩存隊列是否為空,是則線程掛起,進入等待;否則編碼數(shù)據(jù),和取出數(shù)據(jù)。
有2點需要注意:
- 音頻和視頻的編碼流程稍微有點區(qū)別
音頻編碼 需要我們自己將數(shù)據(jù)壓入編碼器,實現(xiàn)數(shù)據(jù)的編碼。
視頻編碼 的時候,可以通過將 Surface 綁定給 OpenGL ,系統(tǒng)自動從 Surface 中去數(shù)據(jù),實現(xiàn)自動編碼。也就是說,不需要用戶自己手動壓入數(shù)據(jù),只需從輸出緩沖中取數(shù)據(jù)就可以了。
因此,這里定義一個虛函數(shù),由子類控制是否需要手動壓入數(shù)據(jù),默認為true:手動壓入。
下文中,將這兩種形式分別叫做:手動編碼 和 自動編碼
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
// 省略其他代碼......
/**
* 是否手動編碼
* 視頻:false 音頻:true
*
* 注:視頻編碼通過Surface,MediaCodec自動完成編碼;音頻數(shù)據(jù)需要用戶自己壓入編碼緩沖區(qū),完成編碼
*/
open fun encodeManually() = true
// ......
}
- 結(jié)束編碼
在編碼過程中,如果發(fā)現(xiàn) Frame 中 buffer 為 null ,就認為編碼已經(jīng)完成了,沒有數(shù)據(jù)需要壓入了。這時,有兩種方法告訴編碼器結(jié)束編碼。
第一種,通過 queueInputBuffer 壓入一個空數(shù)據(jù),并且將數(shù)據(jù)類型標(biāo)記設(shè)置為 MediaCodec.BUFFER_FLAG_END_OF_STREAM 。具體如下:
mCodec.queueInputBuffer(index, 0, 0,
frame.bufferInfo.presentationTimeUs,
MediaCodec.BUFFER_FLAG_END_OF_STREAM)
第二種,通過 signalEndOfInputStream 發(fā)送結(jié)束信號。
我們已經(jīng)知道,視頻是自動編碼,所以無法通過第一種結(jié)束編碼,只能通過第二種方式結(jié)束編碼。
音頻是手動編碼,可以通過第一種方式結(jié)束編碼。
一個坑
測試發(fā)現(xiàn),視頻結(jié)束編碼的時候signalEndOfInputStream之后,在獲取編碼數(shù)據(jù)輸出的時候,并沒有得到結(jié)束編碼標(biāo)記的數(shù)據(jù),所以,上面的代碼中,如果是自動編碼,在判斷到Frame的buffer為空時,直接將mIsEOF設(shè)置為true了,退出了編碼流程。
3. 手動編碼
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
// 省略其他代碼......
/**
* 編碼
*/
private fun encode(frame: Frame) {
val index = mCodec.dequeueInputBuffer(-1)
/*向編碼器輸入數(shù)據(jù)*/
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時,為音頻結(jié)束符標(biāo)記
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()
}
}
// ......
}
和解碼一樣,先查詢到一個可用的輸入緩沖索引,接著把數(shù)據(jù)壓入輸入緩沖。
這里,先判斷是否結(jié)束編碼,是則往輸入緩沖壓入編碼結(jié)束標(biāo)志
4. 拉取數(shù)據(jù)
把一幀數(shù)據(jù)壓入編碼器后,進入 drain 方法,顧名思義,我們要把編碼器輸出緩沖中的數(shù)據(jù),全部抽干。所以這里是一個while循環(huán),直到輸出緩沖沒有數(shù)據(jù) MediaCodec.INFO_TRY_AGAIN_LATER ,或者編碼結(jié)束 MediaCodec.BUFFER_FLAG_END_OF_STREAM 。
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
// 省略其他代碼......
/**
* 榨干編碼輸出數(shù)據(jù)
*/
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寫入音視頻數(shù)據(jù)
*/
abstract fun writeData(muxer: MMuxer, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo)
// ......
}
很重要的一點
當(dāng)mCodec.dequeueOutputBuffer返回的是MediaCodec.INFO_OUTPUT_FORMAT_CHANGED時,說明編碼參數(shù)格式已經(jīng)生成(比如視頻的碼率,幀率,SPS/PPS幀信息等),需要把這些信息寫入到mp4對應(yīng)媒體軌道中(這里通過addTrack在子類中配置音視頻對應(yīng)的編碼格式),之后才能開始將編碼完成的數(shù)據(jù),通過MediaMuxer寫入到相應(yīng)媒體通道中。
5. 退出編碼,釋放資源
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
// 省略其他代碼......
/**
* 編碼結(jié)束,是否資源
*/
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)
// ......
}
調(diào)用子類中的虛函數(shù) release ,子類需要根據(jù)自己的媒體類型,釋放對應(yīng)mp4中的媒體通道。
6. 一些外部調(diào)用的方法
abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
// 省略其他代碼......
/**
* 將一幀數(shù)據(jù)壓入隊列,等待編碼
*/
fun encodeOneFrame(frame: Frame) {
synchronized(mFrames) {
mFrames.add(frame)
notifyGo()
}
// 延時一點時間,避免掉幀
Thread.sleep(frameWaitTimeMs())
}
/**
* 通知結(jié)束編碼
*/
fun endOfStream() {
Log.e("ccccc","endOfStream")
synchronized(mFrames) {
val frame = Frame()
frame.buffer = null
mFrames.add(frame)
notifyGo()
}
}
/**
* 設(shè)置狀態(tài)監(jiān)聽器
*/
fun setStateListener(l: IEncodeStateListener) {
this.mStateListener = l
}
/**
* 每一幀排隊等待時間
*/
open fun frameWaitTimeMs() = 20L
// ......
}
這里有點需要注意,在把數(shù)據(jù)壓入排隊隊列之后,做了一個默認 20ms 的延時,同時子類可以通過重寫 frameWaitTimeMs 方法修改時間。
一個是為了避免音頻解碼過快,導(dǎo)致數(shù)據(jù)堆積太多,音頻在子類中重新設(shè)置等待為5ms,具體見子類 AudioEncoder 代碼。
另一個是因為由于視頻是系統(tǒng)自動獲取Surface數(shù)據(jù),如果解碼數(shù)據(jù)刷新太快,可能會導(dǎo)致漏幀,這里使用默認的20ms。
因此這里做了一個簡單粗暴的延時,但并非最好的解決方式。
二、視頻編碼器
有了基礎(chǔ)封裝,寫一個視頻編碼器還不是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()
// 捕獲異常,設(shè)置為系統(tǒng)默認配置 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 實現(xiàn)所有的虛函數(shù)就可以了。
重點來看 configEncoder 這個方法。
i. 配置了碼率 KEY_BIT_RATE。
計算公式源自【MediaCodec編碼OpenGL速度和清晰度均衡】
Biterate = Width * Height * FrameRate * Factor
Factor: 0.1~0.2
ii. 配置幀率 KEY_FRAME_RATE ,這里為30幀/秒
iii. 配置關(guān)鍵幀出現(xiàn)頻率 KEY_I_FRAME_INTERVAL ,這里為1幀/秒
iv. 配置數(shù)據(jù)來源 KEY_COLOR_FORMAT ,為 COLOR_FormatSurface,既來自 Surface 。
v. 配置碼率模式 KEY_BITRATE_MODE
- BITRATE_MODE_CQ 忽略用戶設(shè)置的碼率,由編碼器自己控制碼率,并盡可能保證畫面清晰度和碼率的均衡
- BITRATE_MODE_CBR 無論視頻的畫面內(nèi)容如果,盡可能遵守用戶設(shè)置的碼率
- BITRATE_MODE_VBR 盡可能遵守用戶設(shè)置的碼率,但是會根據(jù)幀畫面之間運動矢量
(通俗理解就是幀與幀之間的畫面變化程度)來動態(tài)調(diào)整碼率,如果運動矢量較大,則在該時間段將碼率調(diào)高,如果畫面變換很小,則碼率降低。
優(yōu)先選擇 BITRATE_MODE_CQ ,如果編碼器不支持,切換回系統(tǒng)默認的 BITRATE_MODE_VBR
vi. 最后,通過編碼器 codec.createInputSurface() 新建一個 Surface ,用于 EGL 的窗口綁定。視頻解碼得到的畫面都將渲染到這個 Surface 中,MediaCodec自動從里面取出數(shù)據(jù),并編碼。
三、音頻編碼器
音頻編碼器則更加簡單。
// 編碼采樣率率
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 實現(xiàn)也比較簡單:
i. 設(shè)置音頻比特率 MediaFormat.KEY_BIT_RATE,這里設(shè)置為 128000
ii. 設(shè)置輸入緩沖區(qū)大小 KEY_MAX_INPUT_SIZE ,這里設(shè)置為 100*1024
四、整合
音頻和視頻的編碼工具已經(jīng)完成,接下來就來看看,如何把解碼器、OpenGL、EGL、編碼器串聯(lián)起來,實現(xiàn)視頻編輯功能。
- 改造EGL渲染器
開始之前,需要改造一下【深入了解OpenGL之EGL】 這篇文章中定義的EGL渲染器。
i. 在之前定義的渲染器中,只支持設(shè)置一個SurfaceView,并綁定到 EGL 顯示窗口中。這里需要讓它支持設(shè)置一個Surface,接收來自 VideoEncoder 中創(chuàng)建的Surface作為渲染窗口。
ii. 由于是要對窗口的畫面進行編碼,所以無需在渲染器中不斷的刷新畫面,只要在視頻解碼器解碼出一幀的時候,刷新一下畫面即可。同時把當(dāng)前幀的時間戳傳遞給OpenGL。
完整代碼如下,已經(jīng)將新增的部分標(biāo)記出來:
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?) {
}
})
}
//-------------------新增部分-----------------
// 新增設(shè)置Surface接口
fun setSurface(surface: Surface, width: Int, height: Int) {
mSurface = surface
mThread.onSurfaceCreate()
mThread.onSurfaceChange(width, height)
}
// 新增設(shè)置渲染模式 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() {
// 渲染狀態(tài)
private var mState = RenderState.NO_SURFACE
private var mEGLSurface: EGLSurfaceHolder? = null
// 是否綁定了EGLSurface
private var mHaveBindEGLContext = false
//是否已經(jīng)新建過EGL上下文,用于判斷是否需要生產(chǎn)新的紋理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) }
}
// ---------------------修改部分代碼------------------------
// 根據(jù)渲染模式和當(dāng)前幀的時間戳判斷是否需要重新刷新畫面
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()
}
}
/**
* 渲染狀態(tài)
*/
enum class RenderState {
NO_SURFACE, //沒有有效的surface
FRESH_SURFACE, //持有一個未初始化的新的surface
SURFACE_CHANGE, //surface尺寸變化
RENDERING, //初始化完畢,可以開始渲染
SURFACE_DESTROY, //surface銷毀
STOP //停止繪制
}
//---------新增渲染模式定義------------
enum class RenderMode {
// 自動循環(huán)渲染
RENDER_CONTINUOUSLY,
// 由外部通過notifySwap通知渲染
RENDER_WHEN_DIRTY
}
//-------------------------------------
}
新增部分已經(jīng)標(biāo)出來,也不復(fù)雜,主要是新增了設(shè)置Surface,區(qū)分了兩種渲染模式,請大家看代碼即可。
- 改造解碼器
還記得之前的文章中提到,音視頻要正常播放,需要對音頻和視頻進行音視頻同步嗎?
而由于編碼的時候,并不需要把視頻畫面和音頻播放出來,所以可以把音視頻同步去掉,加快編碼速度。
修改也很簡單,在 BaseDecoder 中新增一個變量 mSyncRender ,如果 mSyncRender == false ,就把音視頻同步去掉。
這里,只列出修改的部分,完整代碼請看 BaseDecoder
abstract class BaseDecoder(private val mFilePath: String): IDecoder {
// 省略無關(guān)代碼......
// 是否需要音視頻渲染同步
private var mSyncRender = true
final override fun run() {
//省略無關(guān)代碼...
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,初始化編碼器,然后將解碼得到的數(shù)據(jù)扔到編碼器隊列中,監(jiān)聽解碼狀態(tài)和編碼狀態(tài),做相應(yīng)的操作。
解碼過程和使用EGL播放視頻基本是一樣的,只是渲染模式不同而已。
在這個代碼中,只是簡單的將原視頻解碼,渲染到OpenGL,重新編碼成新的mp4,也就是說輸出的視頻和原視頻是一模一樣的。
- 可以實現(xiàn)什么?
雖然上面只是一個普通的解碼和編碼的過程,但是卻可以衍生出無限的想象。
比如:
實現(xiàn)視頻裁剪:給解碼器設(shè)置一個開始和結(jié)束的時間即可。
實現(xiàn)炫酷的視頻畫面編輯:比如將視頻渲染器
VideoDrawer換成之前寫好的SoulVideoDrawer的話,將得到一個有靈魂出竅效果的視頻;結(jié)合之前的畫中畫,可以實現(xiàn)視頻的疊加。視頻拼接:結(jié)合多個視頻解碼器,將多個視頻連接起來,編碼成新的視頻。
加水印:結(jié)合OpenGL渲染圖片,加個水印超簡單的。
......
只要有想象力,那都不是事!
五、結(jié)束語
啊~~~,嗨森,終于寫完本系列的【OpenGL渲染視頻畫面篇】,到目前為止,如果你看過每一篇文章,并且動手碼過代碼,我相信你一定已經(jīng)踏入了Android音視頻開發(fā)的大門,可以去實現(xiàn)一些以前看起來很神秘的視頻效果,然后保存成一個真正的可播放的視頻。
這一系列文章每篇都很長,感謝每個能閱讀到這里的讀者,我覺得我們都應(yīng)該感謝一下自己,堅持真的很難。
最后無比感謝每一位給文章點贊、留言、提問、鼓勵的人兒,是你們讓冰冷的文字充滿溫情,是我堅持的動力。
咱們,下一篇章,不見不散!