如何将麦克风Uint8List原始流输入FFmpeg并转存/推流至RTSP?
正确实现方案
你的核心问题在于:直接将Uint8List对象作为FFmpeg的输入路径,FFmpeg无法识别原始PCM数据流;同时每次接收音频片段就启动一个FFmpeg进程,既低效也无法实现连续的流处理。下面是基于mic_stream和ffmpeg_kit_flutter的正确实现方式,分为本地保存和RTSP推送两种场景。
前置说明
从mic_stream获取的是16位深度、双声道、44100采样率的原始PCM数据,必须在FFmpeg命令中明确指定这些参数,让FFmpeg正确解析输入流。
场景1:实时保存为MP3文件
需要启动一个持续的FFmpeg进程,将麦克风输入的PCM数据持续编码为MP3并写入文件。
完整代码实现
import 'dart:typed_data'; import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter/return_code.dart'; import 'package:mic_stream/mic_stream.dart'; Stream<Uint8List>? stream; StreamSubscription<Uint8List>? listener; // FFmpeg进程实例,用于持续写入数据 Session? ffmpegSession; Future<void> startRecordingToMp3(String outputPath) async { // 1. 初始化麦克风流 stream = await MicStream.microphone( audioSource: AudioSource.DEFAULT, sampleRate: 44100, channelConfig: ChannelConfig.CHANNEL_IN_STEREO, audioFormat: AudioFormat.ENCODING_PCM_16BIT, ); // 2. 构建FFmpeg命令:指定输入为标准输入(-i -),解析PCM参数,编码为MP3 final arguments = [ '-f', 's16le', // 输入格式:16位小端PCM '-ar', '44100', // 采样率 '-ac', '2', // 声道数 '-i', '-', // 输入源为标准输入 '-codec:a', 'libmp3lame', // MP3编码器 '-b:a', '128k', // 音频码率 '-y', // 覆盖已有文件 outputPath // 输出文件路径 ]; // 3. 启动FFmpeg异步进程,获取会话实例 ffmpegSession = await FFmpegKit.executeWithArgumentsAsync(arguments, (session) async { final returnCode = await session.getReturnCode(); if (ReturnCode.isSuccess(returnCode)) { print('MP3录制完成'); } else if (ReturnCode.isCancel(returnCode)) { print('MP3录制被取消'); } else { print('MP3录制失败,日志:${await session.getAllLogsAsString()}'); } }); // 4. 监听麦克风数据流,持续写入FFmpeg进程的标准输入 listener = stream!.listen((pcmData) async { if (ffmpegSession != null && !await ffmpegSession!.isClosed()) { // 将PCM数据写入FFmpeg的标准输入 await FFmpegKit.writeToStdin(ffmpegSession!, pcmData); } }); } // 停止录制的方法 Future<void> stopRecording() async { await listener?.cancel(); if (ffmpegSession != null) { // 发送EOF信号,让FFmpeg完成文件写入 await FFmpegKit.writeToStdin(ffmpegSession!, Uint8List(0), true); await ffmpegSession!.wait(); ffmpegSession = null; } }
场景2:实时推流到RTSP服务器
同样需要启动持续的FFmpeg进程,将PCM数据编码后推送到RTSP服务器,推荐使用TCP传输保证稳定性。
完整代码实现
import 'dart:typed_data'; import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter/return_code.dart'; import 'package:mic_stream/mic_stream.dart'; Stream<Uint8List>? stream; StreamSubscription<Uint8List>? listener; Session? ffmpegSession; Future<void> startRtspPush(String rtspUrl) async { // 1. 初始化麦克风流 stream = await MicStream.microphone( audioSource: AudioSource.DEFAULT, sampleRate: 44100, channelConfig: ChannelConfig.CHANNEL_IN_STEREO, audioFormat: AudioFormat.ENCODING_PCM_16BIT, ); // 2. 构建FFmpeg推流命令 final arguments = [ '-f', 's16le', // 输入格式:16位小端PCM '-ar', '44100', // 采样率 '-ac', '2', // 声道数 '-i', '-', // 输入源为标准输入 '-codec:a', 'aac', // 推流推荐用AAC编码器(RTSP常用) '-b:a', '128k', // 音频码率 '-f', 'rtsp', // 输出格式为RTSP '-rtsp_transport', 'tcp', // 使用TCP传输,避免丢包 rtspUrl // RTSP服务器地址 ]; // 3. 启动FFmpeg异步推流进程 ffmpegSession = await FFmpegKit.executeWithArgumentsAsync(arguments, (session) async { final returnCode = await session.getReturnCode(); if (ReturnCode.isSuccess(returnCode)) { print('RTSP推流结束'); } else if (ReturnCode.isCancel(returnCode)) { print('RTSP推流被取消'); } else { print('RTSP推流失败,日志:${await session.getAllLogsAsString()}'); } }); // 4. 持续写入PCM数据到FFmpeg进程 listener = stream!.listen((pcmData) async { if (ffmpegSession != null && !await ffmpegSession!.isClosed()) { await FFmpegKit.writeToStdin(ffmpegSession!, pcmData); } }); } // 停止推流的方法 Future<void> stopRtspPush() async { await listener?.cancel(); if (ffmpegSession != null) { await FFmpegKit.writeToStdin(ffmpegSession!, Uint8List(0), true); await ffmpegSession!.wait(); ffmpegSession = null; } }
关键注意事项
- 避免频繁启动FFmpeg进程:每次启动FFmpeg都会有初始化开销,实时流必须用单个持续进程处理所有音频数据。
- 指定正确的PCM参数:必须严格匹配
mic_stream的输出参数(采样率、声道数、位深),否则会出现音频杂音、速度异常等问题。 - 权限申请:确保已申请录音权限和存储权限(本地保存时),Android端需要在
AndroidManifest.xml中添加对应权限,iOS端在Info.plist中添加麦克风权限描述。 - 错误排查:如果出现失败,调用
session.getAllLogsAsString()获取完整日志,根据FFmpeg的错误提示调整参数或排查环境问题(比如FFmpeg是否包含所需编码器,如libmp3lame、aac)。
内容的提问来源于stack exchange,提问作者Uday
相关产品推荐
相关产品推荐

