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
相关产品推荐
相关产品推荐

