音视频开发实战:从流式原理到弱网对抗的全链路指南
音视频,这个词现在几乎无处不在——刷短视频时的自动播放、开会时的屏幕共享、远程上课的实时互动、甚至智能音箱里的一句“打开客厅灯”,背后都离不开音视频技术的支撑。它早已不是当年那种“装个RealPlayer看.rm文件”的小众技能,而是渗透进日常每一个数字触点的基础能力。如果你正在做小程序开发、自媒体内容分发、在线教育平台搭建、智能硬件产品设计,或者只是想把自家监控画面稳定推到手机上查看,那么“音视频”这三个字,就不是泛泛而谈的概念,而是你必须亲手调通、压得住延迟、扛得住弱网、经得起并发的真实工程模块。它不玄乎,但也不容糊弄:编解码选错一档参数,画质就糊成马赛克;传输协议配错一个超时阈值,连麦就卡成PPT;渲染逻辑少处理一帧时间戳,声音和画面就会“嘴型对不上”。我干这行十多年,从最早用FFmpeg硬啃H.264 Annex B格式,到后来搭WebRTC集群扛住十万路实时流,再到最近帮一家社区养老系统把4K慢动作回放压缩进3Mbps带宽——所有踩过的坑、调过的参数、验证过的方案,其实都围绕一个朴素目标:让声音和画面,在该出现的时候,以该有的样子,稳稳地出现在用户眼前耳中。这篇内容,就是我把这些年在真实项目里反复验证过的核心逻辑、关键决策点、实操配置和避坑清单,一条条拆开讲清楚。不讲虚的架构图,不堆术语黑话,只说“为什么这么选”“哪一行命令不能改”“哪个字段填错就全白忙”,适合刚接手音视频模块的开发者、想搞清底层逻辑的产品经理、需要自主调试推流问题的运营同学,以及任何不想再被“音画不同步”“花屏卡顿”“推流失败”反复折磨的实战者。
1. 音视频系统整体设计与思路拆解
1.1 为什么不能直接“传文件”?——理解实时性与流式处理的本质差异
很多人第一次接触音视频开发时,下意识会想:“不就是把MP4文件传过去播放吗?”这个想法很自然,但恰恰是绝大多数入门级故障的起点。MP4是一个封装格式,本质是把编码后的音视频数据、时间戳、元信息打包成一个静态文件。而我们日常说的“直播”“连麦”“远程桌面”,核心诉求是 低延迟、持续生成、动态适配 ——主播还在说话,画面还在动,网络状况每秒都在变,接收端必须边收边解、边解边播,不能等整个文件下载完才开始。这就决定了整套系统必须基于 流式传输(Streaming) 而非 文件传输(File Transfer) 构建。
流式处理的关键在于“管道化”:采集→编码→封装→传输→解码→渲染,每个环节都是持续吐出数据块(Packet),而不是一次性交付完整文件。举个生活化的例子:就像自来水厂供水,不是把一整池水运到你家再放出来,而是通过管道持续加压输送,你拧开水龙头,水就立刻出来。音视频流也一样——采集设备像水泵,编码器像加压泵,传输协议像管道材质与压力控制,播放器像水龙头和水表。任何一个环节压力不稳(比如编码帧率突变)、管道漏压(比如丢包未重传)、水表不准(比如时间戳解析错误),都会导致终端“没水”(黑屏)、“断流”(卡顿)或“水温不对”(音画不同步)。
因此,所有音视频系统的设计起点,不是选什么播放器,而是明确 业务对延迟、画质、稳定性三者的优先级排序 。比如在线教育,可接受500ms延迟,但绝不能容忍1秒以上的卡顿;而安防监控,允许2秒延迟,但要求7×24小时不中断;游戏开黑则必须压到200ms以内,否则“听声辨位”就失效了。这个排序直接决定后续所有技术选型:延迟敏感就倾向WebRTC(端到端直连,绕过服务器中转);稳定性优先就选SRS+HLS(服务端切片,天然抗丢包);兼顾两者则用SRT或QUIC自研协议。我见过太多团队一上来就堆高配GPU服务器跑WebRTC,结果弱网下连麦照样卡顿——不是技术不行,是没先厘清“我要解决什么问题”。
1.2 两大主流路径:基于协议栈的选型逻辑与适用边界
当前生产环境中的音视频链路,基本可归为两大技术路径: 基于标准协议栈的成熟方案 和 自研协议+自定义信令的深度定制方案 。前者如WebRTC、RTMP、HLS、SRT,后者如抖音自研的ByteRTC、腾讯会议背后的TRTC。作为一线开发者,我的经验是: 95%的项目,应该从标准协议栈起步;只有当标准协议无法满足特定性能或合规要求时,才考虑自研 。
-
WebRTC :浏览器原生支持,端到端加密,NAT穿透能力强,延迟最低(通常150~300ms)。但它对服务端压力小,对客户端CPU/内存要求高,且不支持服务端录制(需额外部署MCU或SFU)。适合1对1/小群组实时互动,比如在线问诊、远程面试。注意:它依赖STUN/TURN服务器做打洞,公网部署时TURN必须配好,否则内网用户间无法直连。
-
RTMP + HLS :RTMP负责低延迟推流(500ms~1s),HLS负责高兼容性播放(iOS/安卓/网页通吃,延迟3~10s)。这是目前最稳妥的“推拉分离”架构,Nginx-rtmp-module或SRS均可快速搭建。优势是成熟、文档多、CDN友好;劣势是RTMP在现代浏览器中需Flash或转封装,HLS首屏加载慢。适合泛娱乐直播、企业内训录播。
-
SRT :专为不可靠网络设计,内置前向纠错(FEC)和丢包重传(ARQ),在4G/弱WiFi下表现远超RTMP。但生态较新,播放端支持有限(需集成SRT SDK),服务端部署复杂。适合户外直播、无人机图传、广电级远程制作。
选择时有个硬指标: 看你的终端覆盖范围 。如果80%用户用手机App,且App可控(能集成SDK),SRT或自研协议是优选;如果大量用户通过微信公众号、企业微信、钉钉嵌入页面访问,那必须保HLS兼容性,RTMP+HLS组合仍是底线方案。我去年帮一家政务培训平台改造系统,他们坚持“所有功能必须微信里能用”,最后就是用SRS将RTMP流自动转成HLS,再用hls.js在微信内播放——看似多一层转换,实则省去所有终端适配成本。
1.3 端到端链路拆解:从麦克风到扬声器,每一环都可能成为瓶颈
一个完整的音视频链路,按数据流向可分为七层,每层都有其典型瓶颈和排查重点:
-
采集层 :摄像头/麦克风硬件驱动、权限获取、分辨率/帧率/码率设置。常见问题:安卓部分机型前置摄像头默认1080p但实际只支持720p,强行设高会导致预览黑屏;iOS麦克风隐私权限未声明,首次调用直接静音。
-
前处理层 :美颜、降噪、回声消除(AEC)。这里极易被忽视——很多团队以为“开了美颜SDK就行”,但没配对采样率(如采集48kHz,美颜处理却按16kHz),结果人声失真。AEC更是硬骨头,需精确匹配播放音频的延迟(通常50~200ms),否则越消除越回声。
-
编码层 :H.264/H.265视频编码,AAC/Opus音频编码。关键参数:Profile(Baseline/Main/High)、Level(决定最大分辨率/帧率)、CRF(恒定质量)或CBR(恒定码率)。新手常犯错:用High Profile编码,但低端安卓机解码器不支持,直接崩溃。
-
封装层 :将编码后的音视频帧打包成流格式(如FLV、MP4 Fragmented、RTP Packet)。注意时间戳(PTS/DTS)必须严格递增且连续,否则播放器无法同步。
-
传输层 :TCP/UDP选择、拥塞控制算法(如BBR、Cubic)、重传策略。WebRTC用UDP+自研拥塞控制,RTMP用TCP保证不丢包但易受网络抖动影响。
-
服务端处理层 :转码、混流、录制、鉴权。重点是负载均衡——单台SRS服务器理论支持2000路并发,但实际受磁盘IO(录制)和CPU(转码)限制,需压测验证。
-
播放层 :解码器选择(软解/硬解)、缓冲区大小、渲染同步逻辑。iOS硬解性能强但兼容性差(某些HEVC格式不支持),安卓碎片化严重,必须做机型分级适配。
我在某次电商大促直播保障中,发现凌晨流量峰值时大量用户反馈“画面卡顿但声音正常”,查到最后是播放层缓冲区设为5秒(为防卡顿),但CDN节点突发拥塞导致首帧加载超时,播放器误判为“流中断”而反复重试。解决方案不是加服务器,而是将缓冲策略改为“动态自适应”:网络好时缓2秒保流畅,网络差时缓8秒保不中断,用JS实时监听
video.buffered
变化来调整。
2. 核心细节解析与实操要点
2.1 编码参数黄金组合:如何在画质、码率、性能间找到平衡点
编码不是“越高越好”,而是“够用就好”。盲目追求4K/60fps,只会换来发热降频、电量骤减、低端机闪退。我总结了一套面向真实终端的编码参数基准表,已验证于200+款主流机型:
| 场景 | 推荐分辨率 | 帧率 | 视频编码 | 关键参数 | 典型码率 | 适用终端 |
|---|---|---|---|---|---|---|
| 手机连麦 | 640×480 | 15fps | H.264 Baseline | CRF=28, keyint=30 | 400~600kbps | 全机型兼容 |
| 教育直播 | 1280×720 | 24fps | H.264 Main | CRF=23, keyint=48 | 1.2~1.8Mbps | 中高端安卓/iOS |
| 游戏直播 | 1920×1080 | 30fps | H.264 High | CRF=20, keyint=60 | 3~4Mbps | 高性能手机/PC |
| 安防监控 | 3840×2160 | 15fps | H.265 Main | CRF=25, keyint=30 | 2~3Mbps | 支持HEVC的IPC/NVR |
关键参数解释:
- CRF(Constant Rate Factor) :比CBR更智能的质量控制方式。CRF越小画质越好,但码率波动大;CRF越大压缩越狠,但码率稳定。23是视觉无损临界点,28是清晰可辨底线。
- keyint(关键帧间隔) :决定I帧密度。设为帧率×2是通用安全值(如24fps设48),太密增加码率,太疏导致seek卡顿或弱网恢复慢。
- Profile选择 :Baseline Profile兼容性最好(所有H.264解码器都支持),Main Profile支持B帧提升压缩率,High Profile支持8×8 DCT但低端设备易崩溃。
实操中最大的坑是
忽略硬件编码器能力
。安卓MediaCodec在不同SoC上支持的Profile/Level差异极大:高通骁龙8系列支持H.265 Level 5.1,联发科Helio P系列可能只支持到Level 4.0。我的做法是:首次启动时用
MediaCodecList
查询本地支持的编码器列表,动态降级——先尝试High Profile,失败则切Main,再失败切Baseline。代码片段如下(Kotlin):
val codecInfo = MediaCodecList(MediaCodecList.REGULAR_CODECS)
.findEncoderForFormat(MediaFormat.createVideoFormat("video/avc", 1280, 720))
codecInfo?.let {
val profileLevels = it.capabilities.profileLevels
if (profileLevels.any { it.profile == CodecProfileLevel.AVCProfileHigh }) {
format.setInteger(MediaFormat.KEY_PROFILE, CodecProfileLevel.AVCProfileHigh)
} else if (profileLevels.any { it.profile == CodecProfileLevel.AVCProfileMain }) {
format.setInteger(MediaFormat.KEY_PROFILE, CodecProfileLevel.AVCProfileMain)
}
}
提示:iOS端更简单,AVFoundation自动选择最优编码器,但需注意
AVVideoProfileLevelKey必须显式设置,否则iOS 15+可能默认用High Profile导致老设备解码失败。
2.2 时间戳同步:音画不同步的根源与根治方法
音画不同步(AV Sync)是用户投诉第一大原因,但90%的问题不在传输,而在 时间戳生成与消费的错位 。视频帧和音频帧各自有独立的时间戳(PTS),播放器靠它们计算“该什么时候显示/播放”。一旦采集、编码、传输过程中任一环节时间戳被篡改或丢失,同步就崩了。
根本原因有三类:
- 采集时钟漂移 :摄像头和麦克风使用不同晶振,微秒级偏差累积成毫秒级不同步。实测某款USB摄像头与麦克风采集,1分钟内偏差达80ms。
- 编码器引入延迟 :B帧编码需等待后续帧,导致视频PTS晚于实际采集时间;音频编码器缓存满帧才输出,音频PTS滞后。
- 播放器渲染逻辑缺陷 :未按PTS严格调度,而是用系统时间硬凑,或忽略音视频队列长度差。
根治方案分三层:
-
采集层对齐 :强制音视频共用同一时钟源。安卓用
AudioRecord的getTimestamp()获取音频采集时间,再以此为基准触发摄像头帧捕获;iOS用AVCaptureSession的syncToVideo属性开启音视频同步采集。 -
编码层补偿 :在编码前修正PTS。例如音频采集PTS为t_a,视频采集PTS为t_v,计算偏差Δ=t_v-t_a,在视频编码前将PTS减去Δ。FFmpeg中可用
-vsync cfr -copyts保持原始时间戳,再用-async 1自动校正音频。 -
播放层兜底 :播放器需实现“音轨为主,视频跟随”策略。即以音频PTS为基准线,视频帧若早于音频则丢弃,若晚于则插入空白帧或重复上一帧。ExoPlayer默认启用此逻辑,但需确认
DefaultAudioSink的enableAudioTrackPlaybackParams为true。
我曾处理一个医疗手术直播项目,医生操作器械时金属反光导致视频编码器频繁插入I帧,I帧PTS被重置,而音频PTS连续,结果每插一次I帧,画面就跳快0.3秒。最终解决方案是在服务端SRS中启用
rtmp_play_buffer
并设为200ms,让播放器有足够缓冲空间重排PTS,同时前端用
video.getVideoPlaybackQuality()
监控
totalFrameDelay
,超过50ms自动触发重新同步。
2.3 弱网对抗:从丢包重传到自适应码率的全链路策略
弱网不是“网络差”,而是 带宽动态波动+高丢包率+高抖动 的组合拳。单纯提高码率或加大缓冲,只会让卡顿更剧烈。真正的弱网对抗,是贯穿采集、编码、传输、播放的全链路协同。
-
采集端降级 :检测到网络RTT>300ms或丢包率>5%,立即触发降级:分辨率↓、帧率↓、关键帧间隔↑。iOS可用
NWPathMonitor监听网络类型,安卓用ConnectivityManager结合NetworkCapabilities判断是否为4G/弱WiFi。 -
编码端动态码率(ABR) :不是简单开关,而是闭环控制。采集端每2秒上报当前码率、帧率、CPU占用,服务端根据历史数据预测带宽,下发新的编码参数。关键在于预测算法——我们用滑动窗口平均RTT+丢包率加权计算,比单纯看TCP吞吐更准。实测某教育App在地铁隧道中,ABR能在3秒内从1.5Mbps降至600kbps,避免雪崩式卡顿。
-
传输端FEC+ARQ :SRT协议内置FEC(前向纠错),对小规模丢包(<10%)无需重传即可恢复;对大丢包则启动ARQ(自动重传请求)。但ARQ有代价:重传耗时增加端到端延迟。因此我们设双阈值——丢包率<5%只开FEC,5%~15%开FEC+轻量ARQ(仅重传关键帧),>15%则切回TCP模式保连接不断。
-
播放端Jitter Buffer自适应 :传统固定缓冲(如2秒)在弱网下要么卡顿要么延迟高。我们采用动态算法:初始缓冲1秒,每收到一个关键帧,根据网络抖动(Jitter)值调整——抖动<50ms维持1秒,50~150ms升至1.5秒,>150ms升至2.5秒。同时监控
video.playbackRate,若持续<0.95说明缓冲不足,主动请求更高码率流。
注意:所有弱网策略必须有“退出机制”。曾有个项目ABR降级后卡在600kbps不动,原因是服务端未检测到网络恢复信号。后来我们在客户端加入“带宽探测流”:每10秒发送一个100字节的UDP探测包,根据响应时间判断带宽是否回升,回升持续3次则触发升级。
3. 实操过程与核心环节实现
3.1 搭建最小可行流媒体服务:SRS+FFmpeg推拉流全流程
不依赖云厂商,用开源组件搭一套可商用的流媒体服务,是验证音视频能力的必经之路。我推荐SRS(Simple Realtime Server)+ FFmpeg组合,因其轻量、文档全、中文支持好。以下是零基础搭建全过程,含所有避坑点。
第一步:安装SRS(Ubuntu 22.04)
# 下载最新稳定版(截至2024年,推荐v5.0.22)
wget https://github.com/ossrs/srs/releases/download/v5.0.22/srs.tar.gz
tar xzf srs.tar.gz && cd trunk
# 编译(需提前装g++ cmake)
./configure && make
# 启动(默认监听1935/8080端口)
./objs/srs -c conf/srs.conf
注意:SRS默认配置启用了HTTP API(http://localhost:1985/api/v1/streams),但未开启HTTPS。生产环境务必修改
conf/srs.conf中https段,配置SSL证书,否则Chrome等浏览器会拦截HTTP-FLV流。
第二步:FFmpeg推流(模拟摄像头)
# 用testsrc生成测试视频流,同时用anoisesrc生成测试音频
ffmpeg -f lavfi -i testsrc=size=1280x720:rate=24 -f lavfi -i anoisesrc=d=10:r=44100:a=stereo \
-c:v libx264 -preset fast -crf 23 -c:a aac -b:a 128k \
-f flv "rtmp://localhost:1935/live/livestream"
关键参数说明:
-
-preset fast:编码速度与压缩率平衡点,比ultrafast压缩率高30%,比medium快2倍; -
-crf 23:视觉无损质量,实测1280×720下码率约1.4Mbps; -
-b:a 128k:AAC音频码率,低于96k人声发闷,高于192k无明显提升。
第三步:HLS拉流验证
SRS默认将RTMP流自动转为HLS,访问
http://localhost:8080/live/livestream.m3u8
即可播放。但要注意:
-
HLS切片时长默认10秒,首屏加载慢。修改
srs.conf中hls_fragment为5,hls_window为10; -
iOS Safari要求HLS必须是AES-128加密或CMAF格式,否则拒绝播放。启用CMAF:在
srs.conf中添加hls_enabled on; hls_acodec copy; hls_vcodec copy; hls_m3u8_file [app]/[stream].m3u8; hls_ts_file [app]/[stream]-[seq].ts; hls_wait_keyframe on;。
第四步:Web端播放(hls.js)
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.9"></script>
<video id="video" controls></video>
<script>
const video = document.getElementById('video');
if (Hls.isSupported()) {
const hls = new Hls({
capLevelToPlayerSize: true, // 自动适配分辨率
maxBufferLength: 5, // 最大缓冲5秒
liveSyncDurationCount: 3 // 直播时同步最近3个切片
});
hls.loadSource('http://localhost:8080/live/livestream.m3u8');
hls.attachMedia(video);
}
</script>
实操心得:hls.js在微信内置浏览器中常因UserAgent被识别为不支持而fallback失败。解决方案是强制启用:
const hls = new Hls({ enableSoftwareDecoding: true });,并确保<video>标签有webkit-playsinline属性。
3.2 WebRTC一对一通话:信令交换与SDP协商的落地细节
WebRTC的难点不在API调用,而在 信令设计 和 SDP语义理解 。很多团队用WebSocket传SDP就以为搞定,结果跨运营商无法打洞、弱网下频繁重连。以下是经过千次压测验证的信令结构与SDP优化点。
信令消息格式(JSON)
{
"type": "offer", // offer/answer/candidate
"from": "userA", // 发送方ID
"to": "userB", // 接收方ID
"sdp": "v=0\r\no=- ...", // SDP内容
"timestamp": 1712345678901 // 时间戳,用于丢包重传判断
}
关键设计点:
- candidate分批发送 :STUN/TURN候选地址可能多达20+个,一次性发完易丢包。改为分3批:第一批发host candidate(本机IP),第二批发srflx(STUN映射IP),第三批发relay(TURN中继IP),每批间隔200ms。
-
offer/answer超时控制
:WebSocket消息可能延迟,设置3秒超时,超时后自动触发
createOffer重试,避免“对方已挂但自己还在等answer”。
SDP关键优化项
-
禁用不必要编解码器
:默认SDP包含VP8/VP9/H.264/AV1,但移动端VP9解码耗电高。在
createOffer前设置:const pc = new RTCPeerConnection(config); pc.addTransceiver('video', { direction: 'sendrecv', streams: [localStream] }); // 限制只用H.264 pc.getSenders()[0].setCodecPreferences([ { mimeType: 'video/H264', clockRate: 90000, parameters: { 'level-asymmetry-allowed': 1, 'packetization-mode': 1, 'profile-level-id': '42e01f' } } ]); -
启用NACK与FEC
:在SDP中添加
a=rtcp-fb:120 nack和a=rtcp-fb:120 nack pli,让接收端能请求关键帧重传;a=fmtp:120 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f中的profile-level-id必须匹配设备能力(42e01f对应Baseline Profile Level 3.1)。
NAT穿透实战技巧
-
STUN服务器必须用公网IP
:内网STUN(如192.168.x.x)无法穿透。推荐免费STUN:
stun.l.google.com:19302; -
TURN服务器是保底
:当STUN失败时,所有流量走TURN中继。部署Coturn,配置
/etc/turnserver.conf:listening-port=3478 tls-listening-port=5349 fingerprint realm=yourdomain.com user=username:password external-ip=YOUR_PUBLIC_IP no-multicast-peers no-cli注意:Coturn必须绑定公网IP,且防火墙开放3478/5349端口。测试是否生效:用
trickle-ice工具输入TURN地址,看到relay类型candidate即成功。
3.3 移动端推流SDK集成:Android CameraX + MediaCodec硬编码实战
移动端推流性能瓶颈常在采集与编码环节。CameraX简化了相机控制,但与MediaCodec硬编码衔接需精细处理。以下是Android 12+环境下稳定推流的完整链路。
步骤1:CameraX配置(Kotlin)
val preview = Preview.Builder().build()
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
// 关键:设置输出尺寸匹配编码器需求
val resolution = Size(1280, 720)
val previewUseCase = Preview.Builder()
.setTargetResolution(resolution)
.build()
preview.setSurfaceProvider(viewFinder.surfaceProvider)
注意:
setTargetResolution必须与后续MediaCodec配置的分辨率一致,否则dequeueInputBuffer返回-1(缓冲区不匹配)。
步骤2:MediaCodec硬编码初始化
val format = MediaFormat.createVideoFormat("video/avc", 1280, 720)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
format.setInteger(MediaFormat.KEY_BIT_RATE, 1400_000) // 1.4Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 24)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2) // 2秒关键帧
format.setInteger(MediaFormat.KEY_PROFILE, CodecProfileLevel.AVCProfileMain)
format.setInteger(MediaFormat.KEY_LEVEL, CodecProfileLevel.AVCLevel32)
val encoder = MediaCodec.createEncoderByType("video/avc")
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
val inputSurface = encoder.createInputSurface() // 供CameraX渲染
关键点:
-
COLOR_FormatSurface表示输入为Surface,CameraX可直接输出到此Surface; -
AVCLevel32对应1280×720@24fps,超出则编码器创建失败; -
KEY_I_FRAME_INTERVAL=2生成每2秒一个I帧,比默认10秒更利于弱网恢复。
步骤3:CameraX输出到MediaCodec
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)) { image ->
// 将ImageProxy转为ByteBuffer,送入MediaCodec
val yuvBytes = yuvToByteArray(image) // 自定义YUV转换
val bufferIndex = encoder.dequeueInputBuffer(10000)
if (bufferIndex >= 0) {
val inputBuffer = encoder.getInputBuffer(bufferIndex)
inputBuffer?.clear()
inputBuffer?.put(yuvBytes)
encoder.queueInputBuffer(bufferIndex, 0, yuvBytes.size, System.nanoTime() / 1000, 0)
}
image.close()
}
实操心得:
dequeueInputBuffer超时设为10000ms而非-1,避免线程阻塞;YUV转换必须用image.planes[0].buffer直接读取,不能用image.toBitmap()(性能差10倍)。
步骤4:RTMP推流(用librtmp) 将MediaCodec输出的H.264 Annex B格式NALU,按RTMP协议打包发送。关键逻辑:
- H.264 SPS/PPS必须在首个IDR帧前发送,且封装为AVCDecoderConfigurationRecord;
- 每个NALU前加4字节长度头(big-endian);
-
时间戳用
System.nanoTime()计算,单位为毫秒,需转换为RTMP的timestamp(相对首帧)。
4. 常见问题与排查技巧实录
4.1 卡顿/花屏/黑屏:按链路层级的速查表
音视频故障80%表现为卡顿、花屏、黑屏,但根因分散在不同层级。以下是我整理的“症状→定位→解决”速查表,按发生概率排序:
| 症状 | 可能根因(按概率) | 快速验证方法 | 解决方案 |
|---|---|---|---|
| 全黑无画面 |
1. 摄像头权限未授予
2. Surface配置错误(如ColorFormat不匹配) 3. SPS/PPS未发送 |
查logcat是否有
E/MediaCodec: error
用
adb shell dumpsys media.camera
看设备状态
|
AndroidManifest加
<uses-permission android:name="android.permission.CAMERA"/>
确认MediaFormat中
KEY_COLOR_FORMAT
与设备支持一致
在首个IDR帧前发送SPS/PPS,用Wireshark抓包验证FLV头 |
| 画面卡住不动 |
1. 关键帧丢失(I帧未到达)
2. PTS时间戳跳跃过大 3. 播放器缓冲区溢出 |
检查服务端SRS日志
[WARN] stream not found
用ffplay -v debug播放流,看是否报
Invalid timestamps
|
在SRS配置中启用
hls_wait_keyframe on
编码器确保
keyint
≤帧率×2
播放端设置
maxBufferLength=10
|
| 马赛克/模糊 |
1. 码率过低(CRF设太高)
2. 量化参数QP过大 3. 网络丢包导致B帧解码失败 |
用
ffprobe -v quiet -show_entries format=bit_rate -of default=nw=1
查实际码率
Wireshark过滤
rtmp
看packet loss
|
CRF下调2~3档(如28→25)
硬编码时设
-qmin 10 -qmax 42
限制QP范围
启用SRT的FEC或切换TCP传输 |
| 音画不同步 |
1. 采集时钟未对齐
2. 编码器B帧延迟未补偿 3. 播放器未启用音轨主同步 |
用
ffplay -autoexit -nodisp -i stream.flv -v quiet -show_entries frame=pkt_pts_time,pkt_dts_time
导出时间戳
对比音视频PTS差值 |
CameraX用
ImageAnalysis
同步采集音视频
编码前修正视频PTS=音频PTS+Δ ExoPlayer设
DefaultAudioSink
启用
enableAudioTrackPlaybackParams
|
提示:所有验证必须在 真实弱网环境 下进行。用
networksetup -setdnsservers Wi-Fi 127.0.0.1配合dnsmasq模拟DNS劫持,或用tc命令限速:sudo tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 100ms。
4.2 服务端高并发瓶颈:SRS性能调优实战
SRS单机理论并发2000路,但实际常卡在1000路左右。根本原因不是CPU,而是 磁盘IO(录制)和网络缓冲区(推流) 。以下是生产环境调优清单:
-
磁盘IO瓶颈 :开启录制时,SRS默认每流写一个文件,大量小文件IO拖垮磁盘。解决方案:
-
关闭单流录制,改用
exec调用FFmpeg统一录制:on_publish on_publish.sh,脚本中用ffmpeg -i rtmp://127.0.0.1:1935/$app/$stream -c copy -f flv /record/$stream_$(date +%s).flv; -
或用SRS的
http_hooks回调,由外部服务聚合录制。
-
关闭单流录制,改用
-
网络缓冲区溢出 :RTMP推流端发送过快,SRS接收缓冲区满导致丢包。调优
conf/srs.conf:listen 1935; max_connections 1000; # 限制总连接数 # 调整TCP缓冲区 tcp_nodelay on; tcp_recv_buffer 2M; # 接收缓冲区2MB tcp_send_buffer 2M; # 发送缓冲区2MB # 降低推流缓冲 publish_time_ms 300; # 推流超时300ms -
CPU热点 :H
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)