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上的运用
先贴状态图(可供商榷):
代码实现:
@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头!
代码实现可在以上基础上根据状态图进行添加