MediaMuxer 儲存 mp4
MediaCodec 編碼後,將視頻儲存成 .avc 檔,音頻儲存成 .aac 檔。若要將音視二頻存放在一起,就要使用 MediaMuxer 。MediaMuxer 的概念是產生一個容器,這個容器可以加入一個視軌,一個音軌,及一個字幕軌。
AvcEncoder
AvcEncoder 是用來編碼影片的編碼器,編碼完後將資料存在 mutableListOf<DataVideo> 的 List 中,其中的 DataVideo 是自訂的資料類別,最後再將這個 List 傳給 mediaMuxer 進行寫入硬碟中。
AvcEncoder 完整代碼
先把 AvcEncoder 的完整代碼列出,再來慢慢說明
package net.ddns.mahaljsp.travelx
import android.content.Context
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.media.MediaFormat
import android.util.Log
import android.widget.Toast
import java.nio.ByteBuffer
class AvcEncoder(
val context: Context,
val width:Int,
val height:Int,
) {
lateinit var videoCodec: MediaCodec
lateinit var videoFormat: MediaFormat
var presentation=0L
var startTime=0L
val header_sps = byteArrayOf(0, 0, 0, 1, 103, 100, 0, 10, -84, -76, 3, -64, 17, 63, 44, -92, 20, 8, 8, 27, 66, -124, -44, 0, 0, 0, 1, 104, -18, 6, -14, -64)
val header_pps= byteArrayOf(0, 0, 0, 1, 104, -50, 1, -88, 53, -56)
//第一幀為 spspps 的值,一定要用物件變數保留
var sps= byteArrayOf(0,0,0,1,103,66,-64,41,-115,104,7,-128,34,126,89,8,16,16,16,60,34,17,-88,0,0,0,1,104,-50,1,-88,53,-56)
val pps = byteArrayOf(0, 0, 0, 1, 101)
var videoFrameRate=30
var isPause=false
init {
try {
val list= MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecs = list.codecInfos
for (codec in codecs){
if (codec.isEncoder)
Log.d("Thomas", codec.name+":"+codec.isHardwareAccelerated)
}
val brand=android.os.Build.BRAND
val model=android.os.Build.MODEL
val os=android.os.Build.VERSION.RELEASE
val cell_model="${brand}:${model}:${os}"
//高通 : Hardware : qcom
//小米 13T : Hardware :mt6895 //SOC 晶片型號
//Pixel 6a: hardware: bluejay, google:Pixel 6a:14
Log.d("Thomas", cell_model)
videoCodec=getCodecec(cell_model)
videoFormat = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, width, height
)
//MediaMuxer 儲存 video時,一定要加 csd-0及 csd-1,此值是參考網路上的說明
videoFormat.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps))
videoFormat.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps))
videoFormat.setInteger(
//MediaFormat.KEY_BIT_RATE, width * height * videoFrameRate /10
MediaFormat.KEY_BIT_RATE, 10_000_000
)
videoFormat.setInteger(MediaFormat.KEY_CAPTURE_RATE, videoFrameRate)
videoFormat.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, videoFrameRate)
videoFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
videoCodec.configure(
videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE
)
videoCodec.start()
//一般 start()就會直接進入 flushed狀態,但小米 11T 以上有 bug, 要手動 flush
if(cell_model=="Xiaomi:22081212UG:13" || cell_model=="Xiaomi:2107113SG:13"/* || cell_model=="Xiaomi:22081212UG:14"*/)
videoCodec.flush()
}
catch (e: Exception) {
Log.d("Thomas", "Error:"+e.stackTraceToString())
}
}
fun encoder(yuv420: ByteArray):MutableList{
if (startTime==0L)startTime=System.nanoTime()/1000
var outData: ByteArray? = null
var datas= mutableListOf()
try {
val inputBufferIndex = videoCodec.dequeueInputBuffer(0)
presentation=System.nanoTime()/1000-startTime
if (inputBufferIndex >= 0) {
val inputBuffer = videoCodec.getInputBuffer(inputBufferIndex)!!
inputBuffer!!.clear()
inputBuffer!!.put(yuv420)
videoCodec.queueInputBuffer(
inputBufferIndex,
0,
yuv420!!.size,
presentation,
0
)
val videoInfo = MediaCodec.BufferInfo()
var outputBufferIndex = videoCodec.dequeueOutputBuffer(videoInfo, 0)
//一定要用 while 把所有的 index 都解讀
while (outputBufferIndex >= 0) {
val outputBuffer = videoCodec.getOutputBuffer(outputBufferIndex)!!
outData = ByteArray(videoInfo.size)
outputBuffer.get(outData)
val videoData=DataVideo()
videoData.offset=videoInfo.offset
videoData.flags=videoInfo.flags
videoData.size=videoInfo.size
videoData.presentationTimeUs=videoInfo.presentationTimeUs
//首幀, 其值為 sps, 一部影片此段只會執行一次
//每支手機的 sps 都不一樣
if (videoInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
sps = ByteArray(videoInfo.size)
System.arraycopy(outData, 0, sps, 0, outData.size)
}
//key frame, 需先把 sps 加在 pps 前頭
else if (videoInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
var keyframe = ByteArray(videoInfo.size + sps.size)
System.arraycopy(sps, 0, keyframe, 0, sps.size)
System.arraycopy(outData, 0, keyframe, sps.size, outData.size)
videoData.muxerData=keyframe
val pps = ByteArray(5)
System.arraycopy(outData, 0, pps, 0, pps.size)
videoData.sps=sps
videoData.pps=pps
videoData.rtmpData=outData
datas.add(videoData)
}
else {
videoData.muxerData=outData
videoData.rtmpData=outData
datas.add(videoData)
}
videoCodec.releaseOutputBuffer(outputBufferIndex, false)
outputBufferIndex = videoCodec.dequeueOutputBuffer(videoInfo, 0)
}
}
} catch (e: Exception) {
Log.d("Thomas", "error : "+e.stackTraceToString())
}
return datas
}
fun getCodecec(cell_model:String): MediaCodec {
val list= MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecs = list.codecInfos
for (codec in codecs){
if (codec.isEncoder)
Log.d("Thomas", codec.name+":"+codec.isHardwareAccelerated)
}
var codecName="OMX.google.h264.encoder"
var codecMsg="軟編\n"
for (codec in codecs){
if (codec.name=="OMX.qcom.video.encoder.avc"){
codecName="OMX.qcom.video.encoder.avc"
codecMsg="硬編\n"
break
}
else if (codec.name=="OMX.MTK.VIDEO.ENCODER.AVC"){
codecName="OMX.MTK.VIDEO.ENCODER.AVC"
codecMsg="硬編\n"
break
}else if (codec.name=="c2.exynos.h264.encoder"){//Pixel
codecName="c2.exynos.h264.encoder"
codecMsg="硬編\n"
break
}
}
//小米12T Pro/Android 14 軟編時一切正常,但硬編有 bug
//硬編時 flush就沒有 key frame, 沒 flush 的話影片格式錯誤
if (cell_model=="Xiaomi:22081212UG:14"){
codecName="OMX.google.h264.encoder"
//codecName="OMX.qcom.video.encoder.avc"
codecMsg="軟体\n"
}
codecMsg+=codecName
Toast.makeText(context, codecMsg, Toast.LENGTH_SHORT).show()
return MediaCodec.createByCodecName(codecName)
}
}
DataVideo資料集
DataVideo 類別用來存放影片編碼後的資料,結構如下
