简介:demo8.rar是《第8课 利用FFmpeg将摄像头画面与麦克风数据合成后推送到rtmp服务器》的配套源文件包,面向学习FFmpeg、OpenCV与RTMP推流的开发者,解决摄像头画面与麦克风数据编码封装后推送至RTMP服务器的问题。相比第5课的流转推,本例直接在本地完成音视频采集、编码与封装,并引入编码器,有助理解推流链路。资源共43个文件,以dll、h、cpp、pcm为主:19个dll提供运行依赖,6个头文件和4个cpp构成主要源码,另含2个pcm测试音频、sln/vcxproj工程配置及可执行demo.exe,压缩包约46.56MB。已有358人学习下载。通过该资源可获取可直接运行的demo程序与完整VS工程,便于对照源码跟踪摄像头读取、音频编码、RTMP推送等步骤,借助其中OpenCV与FFmpeg配置可快速搭建实验环境,适合有C++基础的学习者实战。

1. 把摄像头与麦克风合成后推RTMP,核心不是“推”,而是“先合成”

很多第一次做这套链路的人,会先把视频录成mp4再推上去,或者把摄像头画面直接推流,等需要加麦克风时才发现音画是两条时间线,播放端要么没声音,要么声音和画面差半拍。这个标题里“合成”两个字才是关键:在推流之前,FFmpeg要把摄像头采集到的原始视频帧和麦克风采集到的原始音频帧,重新编码成RTMP能够承载的FLV流,并且让音频帧的时间戳落在视频帧的时间戳上。这样才能在VLC、FFplay或任意HLS转封装服务里稳定播放。

这套流程在课程录制、视频会议侧录、网络摄像头转推这些场景里非常常见。更现实的一层是,很多人栽在“ffmpeg不是内部或外部命令”上,装完才发现设备索引还要手动查;还有人把RTMP地址看成HTTP,端口写错一路排到防火墙才发现是地址拼错了。这篇文章面向手里有摄像头和麦克风、想把这条采集链路一次跑通的人。后面给的是搭这类采集推流任务时,从枚举设备到验证流的完整方案,Windows、Linux、macOS都能用,不需要额外硬件。

2. 推流前的设备枚举:dshow、v4l2 与 avfoundation 输入参数表

2.1 为什么必须先查设备索引

FFmpeg的采集输入,不像浏览器那样能弹出“选择摄像头”的界面。dshow、v4l2、avfoundation这三类输入设备,在命令行里都必须用设备名或编号来指定。设备名写错,报错是 Could not open video capture device ,这个提示经常被误判成驱动问题,其实只是名字对不上。

常见做法是先让FFmpeg列出当前机器的设备索引,再复制设备名:

# Windows:列出 dshow 可用的视频和音频设备
ffmpeg -list_devices true -f dshow -i dummy

# Linux:查看 V4L2 视频节点和 ALSA 声卡
v4l2-ctl --list-devices
arecord -l

# macOS:列出 avfoundation 设备索引
ffmpeg -f avfoundation -list_devices true -i ""

第一段代码里的 -f dshow 是把采集方式指定为DirectShow, -list_devices true 让FFmpeg把设备清单打印到stderr, -i dummy 是触发枚举的假输入。Linux那行用v4l2-ctl是因为它打印的“USB Camera: Integrated Camera”这类友好名称,比 /dev/video0 更容易对上物理设备。macOS用空串 -i "" 是avfoundation枚举的固定写法,设备会按 0 、 1 、 2 显示,视频和音频共用一套编号。执行前还应该先确认FFmpeg在PATH里,Windows下报“ffmpeg 不是内部或外部命令”时,要么把 ffmpeg\bin 加进系统PATH,要么直接到目录下执行命令。

提示:每换一个物理USB口或插入第二个摄像头,设备索引都可能变化。正式脚本里不要硬编码设备名,把枚举结果先存成变量,再拼进命令。

2.2 三类平台的最小采集参数

枚举完成后,把设备名直接拼接进 -i 参数。Windows设备名里带空格,要加引号;Linux用 /dev/videoN 或ALSA的 hw:0 ;macOS用数字索引, 0:1 表示视频设备0加音频设备1。下表是最小采集命令的对照,用途是确认设备能出画面、能收音,这一步不做任何编码:

平台 视频输入 音频输入 最小示例
Windows -f dshow -i "video=USB Camera" -f dshow -i "audio=Microphone Array" ffmpeg -f dshow -i "video=USB Camera" -f dshow -i "audio=Microphone Array" test.mp4
Linux -f v4l2 -i /dev/video0 -f alsa -i hw:0 ffmpeg -f v4l2 -i /dev/video0 -f alsa -i hw:0 -t 10 test.mp4
macOS -f avfoundation -i "0" -f avfoundation -i ":0" ffmpeg -f avfoundation -i "0:0" test.mp4

这三行的共同点是不做任何编码,直接录制10秒原始流,用来确认设备名没写错,同时排除“麦克风有声音但采集不到”的硬件独占问题。如果Windows上dshow同时被QQ、微信或浏览器占用,FFmpeg会报 Another application is using this device ,这就是设备被占用的典型信号,先关掉可能占用摄像头的程序再重试。采集到的test.mp4如果既能播放画面又有声音,就可以进入下一步。

2.2.1 设备独占与权限引起的“假故障”

这里补一个容易忽略的点:Linux上v4l2设备被独占时,ffprobe能读到格式信息,但FFmpeg一开 -f v4l2 -i /dev/video0 就报 Device or resource busy 。这个报错不一定能靠换设备号解决,要先看 fuser /dev/video0 有没有占用的进程。macOS上第一次跑avfoundation会弹出麦克风权限框,需要在系统设置里允许终端使用麦克风,否则音频输入会是空的,但视频却正常。这种音画一有一无的现象,多半不是FFmpeg配置问题,而是操作系统权限。Windows的隐私设置里也有“允许桌面应用访问麦克风”开关,关闭状态下FFmpeg能打开设备但录不到数据,表现和macOS一样。

2.3 把枚举结果写进脚本

手动复制设备名容易漏引号,我一般用一个两层判断的脚本自动识别设备节点:

#!/bin/bash
# 以Linux为例:自动取第一个视频节点和第一块声卡
VIDEO_DEV=$(ls /dev/video* | head -n1)
AUDIO_DEV=$(arecord -l | grep -m1 'card' | sed 's/^card //;s/:.*//')
echo "==> video=${VIDEO_DEV}, audio=hw:${AUDIO_DEV},0"
ffmpeg -f v4l2 -i "${VIDEO_DEV}" -f alsa -i "hw:${AUDIO_DEV},0" -t 5 /tmp/check.mp4

这个脚本的价值是把设备枚举从“人看输出”变成“程序自动识别”。第二行 grep -m1 'card' 只取第一块声卡,避免多声卡机器上选错; sed 把 card 0: PCH [HDA Intel PCH] 里的卡号提出来。 -t 5 限制只录5秒,用来做连通性测试。能够生成 /tmp/check.mp4 再进入下一步,不能生成就停在排查阶段,不要急着推流。

3. FFmpeg 合成推流三件套:编码器、时间戳与 muxer 的参数取舍

3.1 为什么不能把摄像头原始流直接推上去

摄像头出来的通常是YUYV、MJPEG或H.264裸流,麦克风出来是PCM。RTMP服务器要的是FLV封装,FLV里的视频编码器一般是H.264,音频一般是AAC。也就是说合成不只是“混流”,还涉及转编码和转封装两步。MJPEG摄像头直接把帧塞进FLV是不行的,因为FLV规范不支持MJPEG;麦克风的PCM也不能直接进FLV,必须编码成AAC。

所以这套推流的基础命令长这样:

ffmpeg \
  -f dshow -i "video=USB Camera" \
  -f dshow -i "audio=Microphone Array" \
  -c:v libx264 -preset veryfast -tune zerolatency \
  -pix_fmt yuv420p -r 25 -g 50 \
  -c:a aac -b:a 128k -ar 44100 -ac 2 \
  -f flv -flvflags no_duration_filesize \
  rtmp://192.168.1.10:1935/live/demo8

拆开看参数: -c:v libx264 把摄像头原始帧压成H.264; -preset veryfast 降低编码延迟,适合直播而不是离线压制; -tune zerolatency 关掉x264内部缓冲,让画面更跟手; -pix_fmt yuv420p 是兼容性关键,很多Web播放器不支持yuv444p; -r 25 控制输出帧率,采集设备是30fps时加这个参数能避免编码器堆积; -g 50 是关键帧间隔,25fps下是2秒一个I帧,拉流端拖动进度条或首帧解码的等待时间不会太长。音频端 -b:a 128k 对语音和现场环境声都够用, -ar 44100 对齐常见声卡采样率,避免额外重采样。

提示: libx264 在部分Linux发行版里需要FFmpeg编译时带 --enable-libx264 。执行时如果报 Unknown encoder 'libx264' ,先跑 ffmpeg -encoders 确认编码器列表里有没有这个名字,别急着换编译包。

3.1.1 采集尺寸、像素格式与MJPEG的预处理

摄像头这边还有一个经常被忽略的输入参数。MJPEG摄像头出厂默认分辨率可能是1280x720或1920x1080,直接用dshow/v4l2采集,FFmpeg会先做一次MJPEG解码,CPU占用一下就上去了。更合理的做法是把解码工作交给设备本身,用 -input_format mjpeg 告诉驱动直接出MJPEG帧,再配合 -video_size 1280x720 -framerate 25 把采集规格固定下来:

ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 25 \
  -i /dev/video0 \
  -f alsa -i hw:0 \
  -c:v libx264 -preset veryfast -tune zerolatency \
  -c:a aac -b:a 128k \
  -f flv rtmp://192.168.1.10:1935/live/demo8

-input_format mjpeg 在树莓派OV5647这类CSI/USB摄像头模组上也常见,OV5647输出YUV或JPEG两种模式,用MJPEG模式能明显降低CPU占用。 -video_size 和 -framerate 必须写在 -i 前面,它们是输入参数而不是输出参数,写错位置会不生效。有些教程把这两个参数放到输出端,会直接改变编码器输出分辨率,和采集端无关,排查时要先分清是哪一端。

还有一种情况是把IP摄像头的RTSP流也塞进这套命令里,这时要移除dshow/v4l2输入,换成 -rtsp_transport tcp -i rtsp://user:pass@ip:554/stream1 ,其余编码参数照用。RTSP输入自带时间戳,比USB摄像头稳定, -fflags +genpts 就不一定需要了。

3.2 帧率、GOP与码率:直播场景的三组默契参数

参数之间不是独立的,它们要一起配合。MJPEG源采集出来的是全帧,编码压力全部在CPU上;H.264 USB摄像头则经常输出30fps,但帧类型不稳定。这时 -r 25 加在输入后是限流,加在输出端是强制改帧率。更简单的做法是统一放在输出端,让FFmpeg按时间戳抽帧,避免采集和编码抢占线程。

直播场景的参数表如下,取值逻辑比默认值更重要:

参数 常用取值 作用与边界
-preset veryfast 延迟低、同码率下画质略差; medium 画质好但延迟高
-tune zerolatency 降低编码延迟,适合交互;静态课程录播可去掉
-g 50 (25fps下2秒) 拉流端拖动进度条的定位间隔;太小码率浪费
-b:v 2000k~4000k 1080p课堂场景2000k勉强,运动画面至少3000k
-maxrate/-bufsize 2500k/5000k 限制码率尖峰,适合低上行带宽

码率这里有个常见误解: -b:v 设成4000k,但没设 -maxrate ,VBR模式下画面大范围运动时码率会冲高,而RTMP是长连接,服务器和客户端之间的缓冲追不上就花屏。局域网推流我一般把 -g 缩短到25,公网直播再放回50甚至100,因为公网上I帧太密会挤占P帧预算,拉流端反而更卡。摄像头是MJPEG模式时,编码器拿到的是全帧,码率波动比H.264源小, -b:v 可以比平时低两成,比如720p场景2000k就够。

3.3 音画时间戳对齐:-fflags +genpts、-re 与 -async 的适用场景

合成推流报错最多的不是编码失败,而是音画不同步。实时设备的时间戳起点不统一,dshow和v4l2的时钟源不同,AAC编码器按自己的节奏产帧,两条流的pts可能错开几十到几百毫秒。最常见做法是在输出前让FFmpeg统一生成时间戳:

ffmpeg -fflags +genpts \
  -f dshow -i "video=USB Camera" \
  -f dshow -i "audio=Microphone Array" \
  -c:v libx264 -preset veryfast -tune zerolatency \
  -c:a aac -b:a 128k \
  -af "aresample=async=1:first_pts=0" \
  -f flv rtmp://192.168.1.10:1935/live/demo8

-fflags +genpts 让FFmpeg在输入时间戳缺失或乱序时按顺序生成演示时间戳,能直接消掉“Non-monotonic DTS”报错。 -af aresample=async=1:first_pts=0 调用音频重采样器,async=1会在音频帧落后时插入静音垫帧、超前时丢弃部分帧,把音频时间轴往视频上靠。旧文档里常见的 -async 1 是早期参数,对输入设备级音频的效果不如aresample稳定,新项目我统一用aresample。

这里要说明 -re 的定位:它是“以原始帧率读取输入”,主要用途是从本地文件模拟实时流,例如 ffmpeg -re -i demo.mp4 -f flv rtmp://... 。对摄像头实时源加 -re 没有意义,设备本身就是实时的,加了反而可能因为限流导致采集缓冲区溢出。很多教程把这几个参数混在一起抄,从文件推流时 -re 正确,采集推流时把 -re 留着就是隐患。排查音画不同步时,先去掉 -re ,再加 -fflags +genpts ,最后才考虑aresample,按这个顺序加参数能少走弯路。

4. 本地拉起 RTMP 服务器验证链路,再从 ffprobe 看流是否正常

4.1 一个能用的 RTMP 服务端:nginx-rtmp 与 SRS 的取舍

推流前要有一个接收端。最常见做法是本地装nginx配合nginx-rtmp-module模块,另一个常用方案是SRS。对验证这条采集推流链路来说,nginx-rtmp在配置与资源占用上更轻;SRS的优势是HTTP-FLV、WebRTC、HLS这些协议一次给全,适合后面要接播放器集群的工程。这里只说服务端最小配置,下载编译包时注意区分“原版nginx”和“带rtmp模块的nginx”,Windows下直接找带rtmp模块的编译包,Linux下用源码编译时加上 --add-module=/path/to/nginx-rtmp-module 。

nginx.conf里加一段rtmp配置:

rtmp {
    server {
        listen 1935;
        chunk_size 4096;
        application live {
            live on;
            record off;
        }
    }
}

chunk_size 4096 是RTMP块大小,默认值偏小会导致高码率流分片过多、CPU空转; record off 关掉服务端自动录像,避免磁盘被长时间推流写满。这段配置的意思是:接收推流地址为 rtmp://<本机IP>:1935/live/<任意流名> ,流名由推流端自行决定,比如demo8、camera1都行。

配置好后启动nginx,用 netstat -an | grep 1935 (Windows用 netstat -ano | findstr 1935 )确认端口在监听,再回到第3章的命令推送。推流端和服务器在同一网段时,把RTMP地址里的IP换成服务器的局域网IP;如果看不到端口监听,先看nginx是否启动成功,再看配置里 rtmp{} 是不是写在了 http{} 外面——位置写错会导致指令不生效。

4.2 ffprobe 验证:判断流是否真的能拉

推流端显示 muxer 和 avio_writer 刷日志,不代表拉流端能正确解码。验证得从“接收视角”看。常见做法是用ffprobe直接探测RTMP流的元数据:

ffprobe -v error -rtmp_flags live \
  -show_format -show_streams \
  -i rtmp://127.0.0.1:1935/live/demo8

-rtmp_flags live 告诉ffprobe这是直播流,避免按下命令后死等。重点看输出的 codec_name 是不是 h264 和 aac ,再看 duration 是否为N/A。直播流的duration字段经常是N/A,这是正常现象,不能因为duration是N/A就认定编码失败。真正可靠的指标是 nb_frames 和 start_time 在反复探测时是否增长。

再进一步是拉流播放验证,临时用ffplay:

ffplay -fflags nobuffer -i rtmp://127.0.0.1:1935/live/demo8

-fflags nobuffer 减少播放器缓冲,本地验证时能更快看到画面。如果ffplay有画面有声音,链路没问题;如果画面正常但声音破音,多半是麦克风输入增益过高,先降系统录音音量,而不是加 -af loudnorm 去硬拉。这里还要注意,本地用127.0.0.1验证与远端拉流验证有区别:公网播放时播放器缓冲策略和本机不同,本地流畅不代表弱网环境不卡,验证的重点应该是时间戳是否单调递增,这决定后续转HLS或录制是否会出现花屏。

4.3 推流失败的三个排查层次

经常遇到的“连接失败”,要从下往上拆。第一层是端口不通,用telnet看1935端口能不能连上。第二层是RTMP握手失败,报 Handshake failed ,确认推流客户端和服务器用的都是标准RTMP,地址没拼成 rtmp://IP:1935 少了一段应用路径。第三层是能握手但提交流中断,这类一般伴随 connection reset by peer ,重点查服务器上的chunk_size是否过小、推流端码率是否超过上行带宽。

报错片段 原因 优先处理
Could not write header for output file RTMP地址路径或应用名错误 检查 /live/ 是否对应rtmp配置里的 application
Connection refused 服务端未启动或端口不对 netstat 确认1935监听
write failed: Broken pipe 推流过程中服务器断开 看nginx错误日志,检查chunk_size与带宽
Non-monotonic DTS 输入时间戳乱序 加 -fflags +genpts 重试

这一层不处理好,换任何播放器都白搭。排查始终从“本地能不能拉”开始,不要一上来就让远端用户测试。服务器日志在nginx的 logs/error.log 里,SRS在 ./objs/srs.log 里,Broken pipe前后几行往往隐藏着真正的断开原因。

5. 收尾技巧:用信号优雅断开推流,以及 tee 分流保留副本

5.1 用SIGINT优雅结束而不是kill -9

推流任务经常在后台挂着,Ctrl+C中断时FFmpeg会尝试发送RTMP deleteStream消息,服务器端随之清理会话。直接kill -9的话,服务器会在超时后才断开,出现“流明明没人推,播放器却还能看几秒”的假象。日常脚本里应该发SIGINT来结束:

ffmpeg -f dshow -i "video=USB Camera" \
  -f dshow -i "audio=Microphone Array" \
  -c:v libx264 -preset veryfast -tune zerolatency \
  -c:a aac -b:a 128k \
  -f flv rtmp://127.0.0.1:1935/live/demo8 &
FFMPEG_PID=$!
kill -INT $FFMPEG_PID

kill -INT 等价于在终端按Ctrl+C,FFmpeg收到后会先结束编码器、冲刷缓冲,再关闭RTMP会话。 & 把进程放到后台只是演示,实际部署时一般用nohup或systemd包一层,但退出信号仍然是SIGINT/SIGTERM。验证收尾是否生效,可以看服务器日志里有没有收到 deleteStream 记录。

5.2 推流同时落一份本地副本

有些场景比如课程录像、设备留档,要求推流的同时另存一份mp4。不需要起两个FFmpeg进程,用tee muxer一份编码数据输出到两个目标:

ffmpeg -f dshow -i "video=USB Camera" \
  -f dshow -i "audio=Microphone Array" \
  -c:v libx264 -preset veryfast -tune zerolatency \
  -c:a aac -b:a 128k \
  -f tee \
  -map 0:v -map 1:a \
  "[f=flv]rtmp://127.0.0.1:1935/live/demo8|[f=mp4]/tmp/demo8.mp4"

-f tee 把同一份编码后的音视频流同时写到两个目标, | 分隔的输出里用 [f=flv] 和 [f=mp4] 分别指定封装格式。Linux下本地路径写 /tmp/demo8.mp4 ,Windows下换成盘符路径如 D:/record/demo8.mp4 。这里有个坑:mp4边推流边写有损坏风险,进程非正常退出时文件没写完整moov,需要加 -movflags frag_keyframe+empty_moov ,否则文件只能在推流结束后才能正常播放。加了 frag_keyframe 后mp4采用碎帧写入,即使进程中断,已经写入的片段也能被播放器识别。

这样整个链路就从设备枚举、合成推流、服务端验证到优雅退出,变成了一套可以反复使用的模板。下次搭采集推流任务,只需要换设备名、RTMP地址和本地副本路径,编码参数和时间戳配置不需要再动。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐