MediaMuxer 儲存 mp4

      在〈MediaMuxer 儲存 mp4〉中尚無留言

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 類別用來存放影片編碼後的資料,結構如下

🔒 更多內容,請登入會員繼續閱讀。

立即登入

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *