去年参与一个在线教育平台开发时,我们团队遇到了直播流处理的性能瓶颈。当并发用户超过500时,传统方案开始出现延迟和卡顿。经过多轮技术选型,最终采用FFmpeg+Java的组合实现了稳定支持3000+并发的直播系统。这套方案的核心优势在于:
mermaid复制graph TD
A[采集设备] -->|RTMP| B(FFmpeg)
B -->|HLS切片| C[存储服务器]
C --> D[Java服务]
D -->|HTTP| E[播放端]
(注:应要求删除mermaid图表,改为文字说明)
系统数据流:
| 处理环节 | 单节点吞吐量 | 延迟控制 | 硬件消耗 |
|---|---|---|---|
| 视频采集 | 1080p@30fps | <200ms | 15% CPU |
| FFmpeg转码 | 8路并行 | 300-500ms | 70% CPU |
| Java服务 | 3000QPS | <50ms | 2GB内存 |
bash复制ffmpeg -i rtmp://input.stream \
-c:v libx264 -preset ultrafast -tune zerolatency \
-c:a aac -b:a 128k \
-f hls -hls_time 2 -hls_list_size 5 \
/var/nginx/html/stream_%v/playlist.m3u8
参数详解:
-preset ultrafast:牺牲约10%压缩率换取30%速度提升-tune zerolatency:关键配置!降低直播延迟至0.8-1.2秒-hls_time 2:分片时长2秒,平衡卡顿率和延迟bash复制ffmpeg -i input.mp4 \
-map 0:v:0 -map 0:a:0 \
-c:v libx264 -b:v:0 2000k -maxrate:v:0 2200k \
-c:v libx264 -b:v:1 1000k -maxrate:v:1 1100k \
-c:a aac -b:a 128k \
-var_stream_map "v:0,a:0 v:1,a:0" \
-f hls -master_pl_name master.m3u8 \
out_%v.m3u8
重要提示:建议至少生成3档码率(1080p/720p/480p),带宽波动时可自动切换
java复制@RestController
public class StreamController {
@GetMapping("/live/{streamId}.m3u8")
public ResponseEntity<Resource> getPlaylist(
@PathVariable String streamId,
@RequestHeader String token) {
if(!authService.validateToken(token)){
return ResponseEntity.status(403).build();
}
Resource resource = storageService.loadAsResource(streamId+".m3u8");
return ResponseEntity.ok()
.header("Cache-Control", "no-cache")
.body(resource);
}
}
性能优化要点:
ResourceHttpRequestHandler处理静态文件java复制@Configuration
public class LoadBalanceConfig {
@Bean
public ServerList<Server> ribbonServerList() {
List<Server> list = new ArrayList<>();
// 动态获取FFmpeg节点状态
ffmpegNodes.forEach(node -> {
if(node.getCpuUsage() < 80){
list.add(new Server(node.getIp()));
}
});
return new StaticServerList<>(list.toArray(new Server[0]));
}
}
经过200+次压力测试验证的最佳参数组:
bash复制-threads 0 -vf "scale=w=min(iw\,1280):h=-2" \
-crf 23 -g 60 -keyint_min 60 \
-bufsize 1000k -maxrate 1500k \
-flags +global_header -movflags faststart
调优效果对比:
| 参数 | 默认值 | 优化值 | 提升效果 |
|---|---|---|---|
| threads | auto | 0(自动) | CPU利用率↑20% |
| keyint | 250 | 60 | 卡顿率↓40% |
| bufsize | 2000k | 1000k | 延迟↓35% |
java复制@Bean
public NettyReactiveWebServerFactory nettyFactory() {
NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory();
factory.addServerCustomizers(builder ->
builder.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true));
return factory;
}
properties复制# application.properties
spring.netty.max-direct-memory=512MB
分片不连续:
-hls_list_size是否≥5-hls_flags append_list时间戳异常:
ffprobe -show_frames stream.ts-use_wallclock_as_timestamps 1关键帧缺失:
ffprobe -show_frames -select_streams v stream.ts | grep key_frame=1-g值是帧率的整数倍文件描述符耗尽:
bash复制# 查看当前限制
ulimit -n
# 永久修改
echo "* soft nofile 65535" >> /etc/security/limits.conf
TIME_WAIT堆积:
bash复制sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=30
python复制#!/usr/bin/python3
import requests
import subprocess
def check_ffmpeg():
proc = subprocess.run(['pgrep', '-x', 'ffmpeg'],
capture_output=True)
return proc.returncode == 0
def check_hls():
resp = requests.get('http://localhost/live/test.m3u8',
timeout=3)
return resp.status_code == 200
if not check_ffmpeg() or not check_hls():
# 触发告警
requests.post('http://alert-system/api',
json={'msg': '直播流异常'})
yaml复制# prometheus.yml
scrape_configs:
- job_name: 'ffmpeg'
static_configs:
- targets: ['ffmpeg-exporter:9111']
- job_name: 'java'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['java-app:8080']
关键监控项:
ffmpeg_transcode_latency_secondshls_segment_gap_secondsjvm_memory_used_bytes{area="heap"}java复制// 在视频流中插入SEI信息
ffmpeg -i video -i audio \
-bsf:v "h264_metadata=sei_user_data='{\"whiteboard\":\"${json}\"}'" \
output.ts
javascript复制// 播放器事件监听
player.on('fragmentloaded', (e) => {
const sei = parseSEI(e.data);
if(sei.quiz) showQuiz(sei.quiz);
});
bash复制# 每30秒插入商品卡点
ffmpeg -i input -vf \
"drawtext=text='%{pts\:hms}':x=w-tw-10:y=h-th-10:fontsize=30" \
-force_key_frames "expr:gte(n,n_forced*900)"
java复制// 基于用户互动数据自动剪辑
String cmd = String.format("ffmpeg -i input.mp4 -ss %s -t 60 -c copy highlight.mp4",
peakTimeRepository.findTop1ByLiveIdOrderByClickDesc(liveId));
runtime.exec(cmd);
| 组件 | 基础版 | 进阶版 | 专业版 |
|---|---|---|---|
| CPU | Xeon E-2236 | i9-10900K | EPYC 7763 |
| 内存 | 32GB DDR4 | 64GB DDR4 | 128GB DDR4 |
| 显卡 | Intel UHD 630 | RTX 3060 | Tesla T4 |
| 网络 | 1Gbps | 2.5Gbps | 10Gbps |
选型原则:
| 厂商 | 转码单价 | 最低延迟 | 特色功能 |
|---|---|---|---|
| AWS | $0.012/分钟 | 1.2s | 自动伸缩 |
| 阿里云 | ¥0.04/分钟 | 0.8s | 窄带高清 |
| 腾讯云 | ¥0.03/分钟 | 1.0s | 连麦互动 |
自建成本对比:
dockerfile复制# docker-compose.yml
services:
ffmpeg:
image: jrottenberg/ffmpeg
devices:
- /dev/dri:/dev/dri
command: [
"-i", "rtmp://localhost:1935/live/test",
"-c:v", "h264_vaapi", "-vf", "format=nv12,hwupload",
"-f", "hls", "/var/hls/output.m3u8"
]
nginx:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./hls:/var/hls
调试技巧:使用
-loglevel debug参数输出详细处理日志
bash复制ffprobe -v error -show_format -show_streams rtmp://localhost/live/stream
bash复制ffmpeg -re -i test.mp4 -c copy -f flv rtmp://server/live/stream
bash复制iperf3 -c server_ip -t 30 -b 100M