You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中如何将MediaCodec录制数据传入FFmpeg Kit生成HLS分片

Android中MediaCodec+FFmpeg Kit实现HLS分片失败问题排查与修复

核心问题分析

你当前能看到pipewrite日志但FFmpeg无法生成多分片,核心问题集中在数据传递完整性、时间戳同步、FFmpeg命令参数合理性、启动时机这几个维度,以下是具体修复方案:


一、修复Codec Config数据传递(H.264 SPS/PPS写入管道)

MediaCodec输出的BUFFER_FLAG_CODEC_CONFIG数据(H.264的SPS/PPS)是FFmpeg初始化解码器的必要条件,当前代码仅将其写入MediaMuxer,未传递给FFmpeg管道,导致FFmpeg无法正确解析后续视频帧。

修改Service.kt中Codec Config处理逻辑:

if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
    if (!codecConfigWritten) {
        if (bufferInfo.size != 0) {
            outputBuffer.position(bufferInfo.offset)
            outputBuffer.limit(bufferInfo.offset + bufferInfo.size)

            // 1. 先写入FFmpeg视频管道
            val configBytes = ByteArray(bufferInfo.size)
            outputBuffer.get(configBytes)
            videoPipeOut.write(configBytes)
            videoPipeOut.flush()
            Log.d("PipeWrite", "Wrote codec config (${configBytes.size} bytes) to video pipe")

            // 2. 恢复指针后写入MediaMuxer(如果需要保留旧逻辑)
            if (mediaMuxerStarted) {
                outputBuffer.position(bufferInfo.offset)
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
                mediaMuxer.writeSampleData(trackIndex, outputBuffer, bufferInfo)
            }
            codecConfigWritten = true
        }
    }
}

二、保留原始时间戳,避免序列混乱

当前代码手动修改presentationTimeUs会打乱MediaCodec输出的时间戳序列,导致FFmpeg无法正确计算分片时长。删除以下代码:

// 移除这行,保留MediaCodec输出的原始时间戳
// bufferInfo.presentationTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000

三、优化FFmpeg命令参数

1. 动态适配音频录制状态

关闭音频录制时,FFmpeg命令仍保留音频输入参数会导致FFmpeg阻塞等待音频数据,需动态调整命令:
修改StreamToFFmpeg.kt中命令构建逻辑:

// 构建基础命令(仅视频)
val ffmpegCommandBuilder = mutableListOf(
    "-f", "h264",
    "-i", videoPipePath,
    "-bsf:v", "h264_mp4toannexb",
    "-c:v", "copy",
    "-f", "hls",
    "-hls_time", "5",
    "-hls_list_size", "0",
    "-use_wallclock_as_timestamps", "1", // 强制使用系统时间作为时间戳,避免无戳异常
    "-hls_segment_filename", "${outputDir.absolutePath}/${recordingId}-video-%04d.ts",
    playlistFilePath ?: m3u8File.absolutePath
)

// 开启音频时添加音频参数
if (isAudioRecordingON) {
    // 插入音频输入参数到命令头部
    ffmpegCommandBuilder.add(1, audioPipePath)
    ffmpegCommandBuilder.add(1, "-i")
    ffmpegCommandBuilder.add(1, "aac")
    ffmpegCommandBuilder.add(1, "-f")
    ffmpegCommandBuilder.add("-c:a", "copy")
}

val ffmpegCommand = ffmpegCommandBuilder.joinToString(" ")

2. 移除无效参数

-preset ultrafast是编码参数,当前使用-c:v copy(流复制模式),该参数无效,直接删除。


四、调整FFmpeg启动时机

当前先启动FFmpeg再写入数据,会导致FFmpeg因等待输入而卡住,需在第一帧关键帧写入管道后再启动FFmpeg:
修改Service.kt逻辑:

// 新增标记:FFmpeg是否已启动
var ffmpegStarted = false

handlerMediacodec.post {
    val bufferInfo = MediaCodec.BufferInfo()
    try {
        while (isRecording) {
            val outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000)
            if (outputBufferIndex >= 0) {
                val outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex)
                outputBuffer?.let {
                    // ... 其他逻辑(Codec Config、写入管道等) ...

                    // 检测第一关键帧,启动FFmpeg
                    if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0 && !ffmpegStarted) {
                        ffmpegStarted = true
                        Log.d("FFmpegInit", "Starting FFmpeg after first keyframe")
                        // 在这里调用startFFmpegUpdated
                        ffmpegStreamer.startFFmpegUpdated(
                            scope = CoroutineScope(Dispatchers.IO),
                            outputDir = outputDir,
                            recordingId = recordingId,
                            playlistFilePath = playlistFilePath,
                            onNewChunkReady = { chunk, number ->
                                Log.d("FFmpegCallback", "Chunk #$number ready: ${chunk.name}")
                            },
                            onPlaylistUpdated = { playlist ->
                                Log.d("FFmpegCallback", "Playlist updated: ${playlist.name}")
                            },
                            onComplete = { success, error ->
                                if (success) {
                                    Log.d("FFmpegCallback", "Recording finished successfully.")
                                } else {
                                    Log.e("FFmpegCallback", "Recording failed: $error")
                                }
                            }
                        )
                    }

                    // ... MediaMuxer写入、释放Buffer等逻辑 ...
                }
            }
            // ... 格式变更处理逻辑 ...
        }
    } catch (e: IllegalStateException) {
        Log.e("RecordingService", "Recording stopped unexpectedly: ${e.message}")
    }
}

五、修正分片监控的正则匹配

当前正则无法正确匹配分片文件名(格式为{recordingId}-video-0000.ts),修改StreamToFFmpeg.kt中的正则:

// 原正则
// val match = Regex("""-(\d+)\.ts$""").find(chunkName)
// 修改为
val match = Regex("""-video-(\d+)\.ts$""").find(chunkName)

六、添加FFmpeg日志排查

在StreamToFFmpeg.kt中添加FFmpeg日志打印,方便定位底层错误:

Log.d("FFmpegCommand", "Executing command: $ffmpegCommand")

FFmpegKit.executeAsync(ffmpegCommand) { session ->
    job.cancel()

    // 打印FFmpeg输出日志
    Log.d("FFmpegOutput", "Output: ${session.output}")
    Log.e("FFmpegError", "Error Output: ${session.errorOutput}")

    if (ReturnCode.isSuccess(session.returnCode)) {
        onComplete(true, null)
    } else {
        onComplete(false, "Code: ${session.returnCode}, Error: ${session.errorOutput}")
    }
}

内容的提问来源于stack exchange,提问作者Mansi Shah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 22:00:52