Kotlin 版本状态机实现及在MediaMuxer(拓展支持分段存储)中的使用

前言

之前基于Doubango实现TBCP协议时看到了其内部的状态机实现,觉得虽然构建状态时书写会很繁琐,但是代码逻辑很清晰,很不错,刚好这次觉得在音视频控制(拍摄、编码、录制、传输)时应该会比较适合使用,就翻成了kotlin版本,模块刚好写到合成,就在MediaMuxer上先强行试用了下

状态机实现代码

拷贝到项目中替换日志类后就可以直接使用

/**
 * 有限状态机.
 *
 * @see [Doubango/tsk_fsm.c]
 */

open class FsmAction /* constructor(open val actionId: Int) */ {
    object FSM_ACTION_ANY : FsmAction(/* -0xFFFF */)
}

open class FsmState /* constructor(open val stateId: Int) */ {
    object FSM_STATE_ANY : FsmState(/* -0xFFFF */)
    object FSM_STATE_CURRENT : FsmState(/* -0xFFF0 */)
    object FSM_STATE_NONE : FsmState(/* -0xFF00 */)
    object FSM_STATE_FINAL : FsmState(/* -0xF000 */)
}

/**
 * 状态切换时的执行函数
 */
fun interface ExecFunction {
    operator fun invoke(vararg params: Any?): Boolean
}

/**
 * 状态类
 *
 * @property from Int
 * @property action Int
 * @property condition Function2<Any, Any, Boolean>
 * @property to Int
 * @property exec Function<Int>
 * @property onTerminate Function1<Any, Int>
 * @constructor
 */
data class FsmStateEntry(
    val from: FsmState,
    val action: FsmAction,
    val condition: Function2<Any?, Any?, Boolean>,
    val to: FsmState,
    val exec: ExecFunction?,/* Replace With 'Function0<Boolean>?,' if no params */
    val description: String
)

class FiniteStateMachine constructor(
    var currentState: FsmState,
    private val terminateState: FsmState,
    private val fsmStateEntries: ArrayList<FsmStateEntry> = ArrayList(5),
    private val onTerminate: Function0<Unit>? = null,
) {
    private val lock: Lock = ReentrantLock()

    /**
     * Add entries (states) to the FSM.
     *
     * @param entries List<FsmStateEntry>
     */
    fun fsmSet(entries: List<FsmStateEntry>) {
        fsmStateEntries.addAll(entries)
    }

    /**
     * Execute an action. This action will probably change the current state of the FSM.
     *
     * @param action FsmAction
     * @param condData1 Any
     * @param condData2 Any
     */
    fun fsmAct(
        action: FsmAction,
        condData1: Any? = null,
        condData2: Any? = null,
        vararg params: Any? = emptyArray()
    ): Boolean {
        var terminates = false /* thread-safeness -> DO NOT REMOVE THIS VARIABLE */
        var execRet = true
        var found = false

        if (isFsmTerminated()) {
            MediaLogger.e(TAG_FSM, "The FSM is in the final state.")
            return false
        }

        //Or TryLock???
        lock.lock()

        fsmStateEntries.filter {
            it.from == FsmState.FSM_STATE_ANY
                    || it.from == FsmState.FSM_STATE_CURRENT
                    || it.from == currentState
        }.filter {
            it.action == FsmAction.FSM_ACTION_ANY || it.action == action
        }.onEach { fsmStateEntry ->
            with(fsmStateEntry) {
                //Check Condition
                if (condition(condData1, condData2)) {
                    MediaLogger.d(TAG_FSM, "State Machine:$description")

                    if (to != FsmState.FSM_STATE_ANY && to != FsmState.FSM_STATE_CURRENT) {
                        /* Stay at the current state if destination state is Any or Current */
                        currentState = to
                    }
                    exec?.run {
                        execRet =
                            invoke(params = params) /* if with params: Replace with  'invoke(params)' */
                        if (!execRet) {
                            MediaLogger.e(
                                TAG_FSM,
                                "State machine: Exec function failed. Moving to terminal state."
                            )
                        }
                    }

                    terminates = (!execRet || currentState == terminateState)
                    found = true
                    return@onEach
                }
            }
        }

        lock.unlock()

        if (terminates) {
            currentState = terminateState
            onTerminate?.invoke()
        }

        if (!found) {
            MediaLogger.e(
                TAG_FSM,
                "${currentState::class.simpleName}_X_${action::class.simpleName} No matching state found."
            )
        }
        return execRet
    }
    private fun isFsmTerminated(): Boolean {
        return currentState == terminateState
    }

    companion object {
        private fun fsmCondAlways(data1: Any?, data2: Any?): Boolean {
            return true
        }

        private val fsmExecNothing = ExecFunction { true }

        fun FSM_ADD_ALWAYS(
            from: FsmState,
            action: FsmAction,
            to: FsmState,
            exec: ExecFunction,/* Replace With 'Function0<Boolean>?,' if not use params */
            description: String
        ): FsmStateEntry {
            return FsmStateEntry(from, action, this::fsmCondAlways, to, exec, description)
        }

        fun FSM_ADD_NOTHING(
            from: FsmState,
            action: FsmAction,
            condition: (Any?, Any?) -> Boolean,
            description: String
        ): FsmStateEntry {
            return FsmStateEntry(from, action, condition, from, fsmExecNothing, description)
        }

        fun FSM_ADD_ALWAYS_NOTHING(
            from: FsmState,
            description: String
        ): FsmStateEntry {
            return FsmStateEntry(
                from,
                FsmAction.FSM_ACTION_ANY,
                this::fsmCondAlways,
                from,
                fsmExecNothing,
                description
            )
        }
    }

}

在MediaMuxer上的运用

先贴状态图(可供商榷):
MediaMuxerStateMachine
代码实现:


@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
class Mp4Muxer {
    /**
     * 文件保存路径(绝对路径)
     */
    private lateinit var filePath: String
    private val muxerStateMachine: FiniteStateMachine

    private var muxer: MediaMuxer? = null
    private var metaDataTrackIndex = INVALID_TRACK_INDEX
    private var videoTrackIndex = INVALID_TRACK_INDEX
    private var audioTrackIndex = INVALID_TRACK_INDEX


    //region MediaMuxer Action Function
    fun createMp4Muxer(filePath: String) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_CreateMux,
            params = arrayOf(filePath)
        )
    }

    fun addVideoTrack(videoFormat: MediaFormat) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_AddVideoTrack,
            params = arrayOf(videoFormat)
        )
    }

    fun addAudioTrack(audioFormat: MediaFormat) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_AddAudioTrack,
            params = arrayOf(audioFormat)
        )
    }

    fun addMetaDataTrack(metaDataFormat: MediaFormat) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_AddMetaDataTrack,
            params = arrayOf(metaDataFormat)
        )
    }

    fun setLocation(latitude: Float, longitude: Float) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_SetLocation,
            params = arrayOf(latitude, longitude)
        )
    }

    fun setOrientation(orientation: Int) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_SetOrientationHint,
            params = arrayOf(orientation)
        )
    }

    fun startMux() {
        muxerStateMachine.fsmAct(MuxerAction.FSM_ACTION_StartMux)
    }

    fun stopMux() {
        muxerStateMachine.fsmAct(MuxerAction.FSM_ACTION_StopMux)
    }

    fun writeAudioSampleData(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_WriteData,
            params = arrayOf(audioTrackIndex, buffer, bufferInfo)
        )
    }

    fun writeVideoSampleData(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_WriteData,
            params = arrayOf(videoTrackIndex, buffer, bufferInfo)
        )
    }

    fun writeMetaDataSampleData(buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
        muxerStateMachine.fsmAct(
            MuxerAction.FSM_ACTION_WriteData,
            params = arrayOf(metaDataTrackIndex, buffer, bufferInfo)
        )
    }

    fun release() {
        muxerStateMachine.fsmAct(MuxerAction.FSM_ACTION_ReleaseMux)
    }
    //endregion


    //region Muxer Functions
    private fun setLocationInternal(latitude: Float, longitude: Float): Boolean {
        return kotlin.runCatching {
            muxer!!.setLocation(latitude, longitude)
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Failed To Set Location:${it.message}")
        }.isSuccess
    }

    private fun setOrientationHintInternal(orientation: Int): Boolean {
        return kotlin.runCatching {
            muxer!!.setOrientationHint(orientation)
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Failed To Set OrientationHint:${it.message}")
        }.isSuccess
    }

    private fun addMediaFormat(mediaFormat: MediaFormat): Result<Int> {
        return runCatching {
            muxer!!.addTrack(mediaFormat)
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Failed To Add Video Track:${it.message}")
        }
    }

    private fun writeSampleData(
        trackIndex: Int,
        sampleData: ByteBuffer,
        bufferInfo: MediaCodec.BufferInfo
    ): Boolean {
        //出现异常不中断状态机?!
        kotlin.runCatching {
            muxer!!.writeSampleData(trackIndex, sampleData, bufferInfo)
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Write Track:$trackIndex Sample Data Exception:${it.message}")
        }
        //Always True
        return true
    }

    private fun releaseMuxer(): Boolean {
        return kotlin.runCatching {
            muxer?.release()
            muxer = null
            filePath = ""
            metaDataTrackIndex = INVALID_TRACK_INDEX
            videoTrackIndex = INVALID_TRACK_INDEX
            audioTrackIndex = INVALID_TRACK_INDEX
        }.onFailure {
            MediaLogger.w(TAG_MUXER, "Release Mp4Muxer Exception:${it.message}!!!")
        }.isSuccess
    }
    //endregion

    //region ********************** Muxer State Machine **********************
    fun onTerminated() {
        MediaLogger.w(TAG_MUXER, "On Mp4Muxer State Machine Terminated!!!!")
    }

    /**
     * Any -> (release) -> Terminated
     */
    private val mp4Mux_Any_2_Terminated_X_releaseMux = ExecFunction {
        releaseMuxer()
    }

    /**
     * Started -> (createMux) -> Terminated
     */
    private val mp4Mux_Started_2_HasNoMedia_X_createMux = ExecFunction {
        val filePath = it[0] as String
        runCatching {
            MediaLogger.d(TAG_MUXER, "saving mp4 file to $filePath")
            muxer = MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        }.onSuccess {
            this.filePath = filePath
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Failed To Create Muxer:${it.message}")
        }.isSuccess
    }

    /**
     * HasNoMedia -> (setLocation) -> HasNoMedia
     */
    private val mp4Mux_HasNoMedia_2_HasNoMedia_X_setLocation = ExecFunction {
        val latitude = it[0] as Float
        val longitude = it[1] as Float
        setLocationInternal(latitude, longitude)
    }

    /**
     * HasNoMedia -> (setOrientationHint) -> HasNoMedia
     */
    private val mp4Mux_HasNoMedia_2_HasNoMedia_X_setOrientationHint = ExecFunction {
        val orientationDegree = it[0] as Int
        setOrientationHintInternal(orientationDegree)
    }

    private val mp4Mux_HasMedia_2_Muxing_X_startMux: ExecFunction = ExecFunction {
        kotlin.runCatching {
            muxer!!.start()
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Failed To Start Muxer:${it.message}")
        }.isSuccess
    }

    private val mp4Mux_HasMedia_2_HasMedia_X_addMetaDataTrack: ExecFunction = ExecFunction {
        val metaDataFormat = it[0] as MediaFormat

        addMediaFormat(metaDataFormat)
            .onSuccess {
                metaDataTrackIndex = it
            }.isSuccess
    }

    private val mp4Mux_HasMedia_2_HasMedia_X_addVideoTrack: ExecFunction = ExecFunction {
        val videoFormat = it[0] as MediaFormat

        addMediaFormat(videoFormat)
            .onSuccess {
                videoTrackIndex = it
            }.isSuccess
    }

    private val mp4Mux_HasMedia_2_HasMedia_X_addAudioTrack: ExecFunction = ExecFunction {
        val audioFormat = it[0] as MediaFormat
        addMediaFormat(audioFormat)
            .onSuccess {
                audioTrackIndex = it
            }.isSuccess
    }

    private val mp4Mux_HasMedia_2_HasMedia_X_setOrientationHint: ExecFunction = ExecFunction {
        val orientation = it[0] as Int
        setOrientationHintInternal(orientation)
    }
    private val mp4Mux_HasMedia_2_HasMedia_X_setLocation: ExecFunction = ExecFunction {
        val latitude = it[0] as Float
        val longitude = it[1] as Float
        setLocationInternal(latitude, longitude)
    }
    private val mp4Mux_HasNoMedia_2_HasMedia_X_addMetaDataTrack: ExecFunction = ExecFunction {
        val metaDataFormat = it[0] as MediaFormat

        addMediaFormat(metaDataFormat)
            .onSuccess {
                metaDataTrackIndex = it
            }.isSuccess
    }
    private val mp4Mux_HasNoMedia_2_HasMedia_X_addVideoTrack: ExecFunction = ExecFunction {
        val videoFormat = it[0] as MediaFormat

        addMediaFormat(videoFormat)
            .onSuccess {
                videoTrackIndex = it
            }.isSuccess
    }
    private val mp4Mux_HasNoMedia_2_HasMedia_X_addAudioTrack: ExecFunction = ExecFunction {
        val audioFormat = it[0] as MediaFormat
        addMediaFormat(audioFormat)
            .onSuccess {
                audioTrackIndex = it
            }.isSuccess
    }

    private val mp4Mux_Muxing_2_Terminated_X_stopMux: ExecFunction = ExecFunction {
        runCatching {
            muxer!!.stop()
        }.onFailure {
            MediaLogger.e(TAG_MUXER, "Stop Mux Exception:${it.message}")
        }.isSuccess
    }
    private val mp4Mux_Muxing_2_Muxing_X_writeData: ExecFunction = ExecFunction {
        val trackIndex: Int = it[0] as Int
        val sampleData: ByteBuffer = it[1] as ByteBuffer
        val bufferInfo: MediaCodec.BufferInfo = it[2] as MediaCodec.BufferInfo

        writeSampleData(trackIndex, sampleData, bufferInfo)
    }

    //endregion


    init {
        muxerStateMachine = FiniteStateMachine(
            FSM_STATE_Started,
            FSM_STATE_Terminated,
            onTerminate = this::onTerminated,
        ).also {
            MediaLogger.d(TAG_MUXER, "Set Up Muxer State Machine")
            it.fsmSet(
                listOf(
                    // Any -> (releaseMux) -> Terminated
                    FSM_ADD_ALWAYS(
                        from = FsmState.FSM_STATE_ANY,
                        action = MuxerAction.FSM_ACTION_ReleaseMux,
                        to = FSM_STATE_Terminated,
                        exec = mp4Mux_Any_2_Terminated_X_releaseMux,
                        description = "mp4Mux_Any_2_Terminated_X_releaseMux"
                    ),
                    // Started -> (createMux) -> HasNoMedia
                    FSM_ADD_ALWAYS(
                        from = FSM_STATE_Started,
                        action = MuxerAction.FSM_ACTION_CreateMux,
                        to = MuxerState.FSM_STATE_HasNoMedia,
                        exec = mp4Mux_Started_2_HasNoMedia_X_createMux,
                        description = "mp4Mux_Started_2_HasNoMedia_X_createMux"
                    ),
                    // HasNoMedia -> (setLocation) -> HasNoMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasNoMedia,
                        action = MuxerAction.FSM_ACTION_SetLocation,
                        to = MuxerState.FSM_STATE_HasNoMedia,
                        exec = mp4Mux_HasNoMedia_2_HasNoMedia_X_setLocation,
                        description = "mp4Mux_HasNoMedia_2_HasNoMedia_X_setLocation"
                    ),
                    // HasNoMedia -> (setOrientationHint) -> HasNoMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasNoMedia,
                        action = MuxerAction.FSM_ACTION_SetOrientationHint,
                        to = MuxerState.FSM_STATE_HasNoMedia,
                        exec = mp4Mux_HasNoMedia_2_HasNoMedia_X_setOrientationHint,
                        description = "mp4Mux_HasNoMedia_2_HasNoMedia_X_setOrientationHint"
                    ),
                    // HasNoMedia -> (addAudioTrack) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasNoMedia,
                        action = MuxerAction.FSM_ACTION_AddAudioTrack,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasNoMedia_2_HasMedia_X_addAudioTrack,
                        description = "mp4Mux_HasNoMedia_2_HasMedia_X_addAudioTrack"
                    ),
                    // HasNoMedia -> (addVideoTrack) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasNoMedia,
                        action = MuxerAction.FSM_ACTION_AddVideoTrack,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasNoMedia_2_HasMedia_X_addVideoTrack,
                        description = "mp4Mux_HasNoMedia_2_HasMedia_X_addVideoTrack"
                    ),
                    // HasNoMedia -> (addMetaDataTrack) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasNoMedia,
                        action = MuxerAction.FSM_ACTION_AddMetaDataTrack,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasNoMedia_2_HasMedia_X_addMetaDataTrack,
                        description = "mp4Mux_HasNoMedia_2_HasMedia_X_addMetaDataTrack"
                    ),
                    // HasMedia -> (setLocation) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasMedia,
                        action = MuxerAction.FSM_ACTION_SetLocation,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasMedia_2_HasMedia_X_setLocation,
                        description = "mp4Mux_HasMedia_2_HasMedia_X_setLocation"
                    ),
                    // HasMedia -> (setOrientationHint) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasNoMedia,
                        action = MuxerAction.FSM_ACTION_SetOrientationHint,
                        to = MuxerState.FSM_STATE_HasNoMedia,
                        exec = mp4Mux_HasMedia_2_HasMedia_X_setOrientationHint,
                        description = "mp4Mux_HasMedia_2_HasMedia_X_setOrientationHint"
                    ),
                    // HasMedia -> (addAudioTrack) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasMedia,
                        action = MuxerAction.FSM_ACTION_AddAudioTrack,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasMedia_2_HasMedia_X_addAudioTrack,
                        description = "mp4Mux_HasMedia_2_HasMedia_X_addAudioTrack"
                    ),
                    // HasMedia -> (addVideoTrack) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasMedia,
                        action = MuxerAction.FSM_ACTION_AddVideoTrack,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasMedia_2_HasMedia_X_addVideoTrack,
                        description = "mp4Mux_HasMedia_2_HasMedia_X_addVideoTrack"
                    ),
                    // HasMedia -> (addMetaDataTrack) -> HasMedia
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasMedia,
                        action = MuxerAction.FSM_ACTION_AddMetaDataTrack,
                        to = MuxerState.FSM_STATE_HasMedia,
                        exec = mp4Mux_HasMedia_2_HasMedia_X_addMetaDataTrack,
                        description = "mp4Mux_HasMedia_2_HasMedia_X_addMetaDataTrack"
                    ),
                    // HasMedia -> (startMux) -> Muxing
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_HasMedia,
                        action = MuxerAction.FSM_ACTION_StartMux,
                        to = MuxerState.FSM_STATE_Muxing,
                        exec = mp4Mux_HasMedia_2_Muxing_X_startMux,
                        description = "mp4Mux_HasMedia_2_Muxing_X_startMux"
                    ),
                    // Muxing -> (writingData) -> Muxing
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_Muxing,
                        action = MuxerAction.FSM_ACTION_WriteData,
                        to = MuxerState.FSM_STATE_Muxing,
                        exec = mp4Mux_Muxing_2_Muxing_X_writeData,
                        description = "mp4Mux_Muxing_2_Muxing_X_writeData"
                    ),
                    // Muxing -> (stopMux) -> Terminated
                    FSM_ADD_ALWAYS(
                        from = MuxerState.FSM_STATE_Muxing,
                        action = MuxerAction.FSM_ACTION_StopMux,
                        to = MuxerState.FSM_STATE_Terminated,
                        exec = mp4Mux_Muxing_2_Terminated_X_stopMux,
                        description = "mp4Mux_Muxing_2_Terminated_X_stopMux"
                    )
                )
            )
        }
    }

    companion object {
        private const val INVALID_TRACK_INDEX = -1

        /**
         * Muxer 状态类型
         *
         * @constructor
         */
        sealed class MuxerState : FsmState() {
            object FSM_STATE_Started : FsmState()
            object FSM_STATE_HasNoMedia : FsmState()
            object FSM_STATE_HasMedia : FsmState()
            object FSM_STATE_Muxing : FsmState()
            object FSM_STATE_Terminated : FsmState()
        }

        /**
         * Muxer 动作类型
         *
         * @constructor
         */
        sealed class MuxerAction : FsmAction() {
            object FSM_ACTION_CreateMux : FsmAction()
            object FSM_ACTION_SetLocation : FsmAction()
            object FSM_ACTION_SetOrientationHint : FsmAction()
            object FSM_ACTION_AddAudioTrack : FsmAction()
            object FSM_ACTION_AddVideoTrack : FsmAction()
            object FSM_ACTION_AddMetaDataTrack : FsmAction()
            object FSM_ACTION_StartMux : FsmAction()
            object FSM_ACTION_WriteData : FsmAction()
            object FSM_ACTION_StopMux : FsmAction()
            object FSM_ACTION_ReleaseMux : FsmAction()
        }
    }
}

拓展MediaMuxer支持分段存储功能

先贴更新后的状态图
分段存储

MediaMuxer Start之后会自动丢弃非关键帧直到获取到一个关键帧,然后开启录制。遇到的绿屏及马赛克问题是因为按webrtc的硬编码那样将编码器输出普通帧数据头前面填充了config frame头!
代码实现可在以上基础上根据状态图进行添加

补充下用到的所有的状态图

OPENGL渲染队列

CAMERA 状态

AUDIO RECORD状态

MUX 状态

预录延录状态

直播状态

编码状态

RTSP状态

posted on 2022-06-11 20:04  kelisi_king  阅读(175)  评论(0编辑  收藏  举报