
1. 项目背景与核心价值在当前的视频应用开发中实现高效稳定的本地视频推流是一个常见需求。通过SpringBootFFmpegZLMediaKit这套技术组合开发者可以快速搭建一个功能完善的视频推流服务。这套方案特别适合需要将本地存储的视频文件实时推送到流媒体服务器的场景比如在线教育平台的课程回放、安防监控系统的录像调阅、企业内部培训视频的实时分发等。我曾在多个企业级项目中采用这个技术方案实测下来它的优势主要体现在三个方面首先是性能稳定ZLMediaKit作为国产优秀的流媒体服务器能够高效处理RTMP、HLS等主流流媒体协议其次是灵活性高FFmpeg强大的视频处理能力可以满足各种转码需求最后是集成方便SpringBoot的轻量级特性让整个系统的开发和部署都非常便捷。2. 环境准备与工具安装2.1 ZLMediaKit的安装与配置ZLMediaKit是一款开源的流媒体服务器支持RTSP/RTMP/HLS/HTTP-FLV等多种协议。推荐使用Docker方式安装这是最快捷且不易出错的方式# 拉取最新版镜像 docker pull zlmediakit/zlmediakit:master # 运行容器注意端口映射 docker run -d \ --name zlm-server \ -p 1935:1935 \ # RTMP协议默认端口 -p 8080:8080 \ # HTTP API端口 -p 80:80 \ # HTTP-FLV/WS-FLV端口 -p 443:443 \ # HTTPS端口 -p 554:554 \ # RTSP端口 -p 10000:10000 \ # RTP over UDP端口 zlmediakit/zlmediakit:master安装完成后可以通过访问http://服务器IP:8080/index/api/getServerConfig来验证服务是否正常运行。这里有个小技巧如果是在本地开发环境测试建议把上述所有端口都映射到宿主机避免后续调试时遇到端口不通的问题。2.2 FFmpeg的安装与验证FFmpeg是视频处理的核心工具Windows用户可以直接下载编译好的二进制版本Linux用户建议通过包管理器安装# Ubuntu/Debian sudo apt update sudo apt install ffmpeg -y # CentOS/RHEL sudo yum install epel-release -y sudo yum install ffmpeg ffmpeg-devel -y安装完成后运行ffmpeg -version检查是否安装成功。我建议至少使用4.0以上版本因为新版本对H.265编码的支持更好。在实际项目中遇到过版本兼容性问题特别是需要处理H.265编码的视频时旧版FFmpeg可能会出现无法解码的情况。2.3 SpringBoot项目初始化创建一个基础的SpringBoot项目添加必要的依赖。除了常规的web依赖外还需要加入用于执行系统命令的工具包dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 用于执行命令行 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-exec/artifactId version1.3/version /dependency !-- 日志记录 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies3. 核心推流功能实现3.1 FFmpeg推流命令解析FFmpeg推流的基本命令格式如下ffmpeg -re -i input.mp4 -c:v libx264 -preset ultrafast -tune zerolatency -f flv rtmp://server_ip:1935/live/stream_key这个命令有几个关键参数需要注意-re以原始帧率读取输入避免推流过快-c:v libx264使用H.264编码兼容性最好-preset ultrafast编码速度优先适合实时推流-tune zerolatency最小化延迟-f flv指定输出格式为FLV在实际项目中我发现-preset参数的选择对CPU负载影响很大。对于性能较低的服务器使用ultrafast虽然画质稍差但能保证流畅性而在高性能服务器上可以使用medium或slow获得更好的画质。3.2 Java中执行FFmpeg命令在SpringBoot中我们可以通过Runtime.getRuntime().exec()来执行FFmpeg命令但更推荐使用Apache Commons Exec库它提供了更好的进程管理和错误处理Slf4j Service public class StreamService { public boolean pushStream(String inputPath, String outputUrl) { String command String.format(ffmpeg -re -i %s -c:v libx264 -preset ultrafast -f flv %s, inputPath, outputUrl); CommandLine cmdLine CommandLine.parse(command); DefaultExecutor executor new DefaultExecutor(); ExecuteWatchdog watchdog new ExecuteWatchdog(60000); executor.setWatchdog(watchdog); try { int exitValue executor.execute(cmdLine); return exitValue 0; } catch (IOException e) { log.error(推流失败, e); return false; } } }这里有个重要的经验一定要设置ExecuteWatchdog来监控进程执行时间避免FFmpeg进程挂起导致资源泄漏。我在实际项目中遇到过因为忘记设置watchdog导致服务器上积累了数十个僵尸FFmpeg进程的情况。3.3 推流地址的生成规则ZLMediaKit的推流地址遵循标准RTMP URL格式rtmp://[服务器IP]:1935/live/[流密钥]其中流密钥可以自定义客户端通过对应的播放地址来拉流。播放地址支持多种协议RTMP:rtmp://[服务器IP]:1935/live/[流密钥]HTTP-FLV:http://[服务器IP]:80/live/[流密钥].flvHLS:http://[服务器IP]:80/live/[流密钥]/hls.m3u8在实际开发中我建议将推流地址的生成逻辑封装成独立的方法public class ZLMediaKitUtil { private static final String RTMP_FORMAT rtmp://%s:1935/live/%s; private static final String HTTP_FLV_FORMAT http://%s:80/live/%s.flv; public static String generateRtmpUrl(String serverIp, String streamKey) { return String.format(RTMP_FORMAT, serverIp, streamKey); } public static String generateHttpFlvUrl(String serverIp, String streamKey) { return String.format(HTTP_FLV_FORMAT, serverIp, streamKey); } }4. 高级功能与优化4.1 视频转码与参数调整根据不同的网络条件和客户端需求可能需要对视频进行转码处理。以下是一些常用的转码参数示例# 调整分辨率保持原始宽高比 ffmpeg -re -i input.mp4 -vf scaleif(gt(a,16/9),1280,-1):if(gt(a,16/9),-1,720) -c:v libx264 -b:v 1500k -f flv rtmp://server/live/stream # 调整帧率 ffmpeg -re -i input.mp4 -r 25 -c:v libx264 -f flv rtmp://server/live/stream # 添加水印 ffmpeg -re -i input.mp4 -i watermark.png -filter_complex overlay10:10 -c:v libx264 -f flv rtmp://server/live/stream在实际应用中我发现视频参数的设置需要根据目标观众的网络状况进行调整。对于移动端用户建议将码率控制在1Mbps以内分辨率设置为720p而对于固定网络用户可以使用更高的码率和1080p分辨率。4.2 使用ZLMediaKit的API管理流ZLMediaKit提供了丰富的HTTP API用于流管理。我们可以通过SpringBoot集成这些API来实现更强大的功能RestController RequestMapping(/api/stream) public class StreamController { Value(${zlm.api-secret}) private String apiSecret; Value(${zlm.server-ip}) private String serverIp; GetMapping(/list) public ResponseEntity? getStreamList() { String apiUrl String.format(http://%s:8080/index/api/getMediaList?secret%s, serverIp, apiSecret); // 使用RestTemplate调用API RestTemplate restTemplate new RestTemplate(); Map response restTemplate.getForObject(apiUrl, Map.class); return ResponseEntity.ok(response); } PostMapping(/stop/{streamId}) public ResponseEntity? stopStream(PathVariable String streamId) { String apiUrl String.format( http://%s:8080/index/api/close_streams?secret%sstream%s, serverIp, apiSecret, streamId); RestTemplate restTemplate new RestTemplate(); Map response restTemplate.getForObject(apiUrl, Map.class); return ResponseEntity.ok(response); } }4.3 推流监控与自动恢复在实际生产环境中网络波动或进程异常都可能导致推流中断。我们可以实现一个监控机制来自动恢复推流Scheduled(fixedRate 30000) public void checkStreamStatus() { String checkUrl String.format(http://%s:8080/index/api/isMediaOnline?secret%sstream%s, serverIp, apiSecret, currentStreamId); RestTemplate restTemplate new RestTemplate(); Map response restTemplate.getForObject(checkUrl, Map.class); if (response ! null 0.equals(response.get(code).toString())) { log.warn(流 {} 已断开尝试重新推流, currentStreamId); pushStream(currentVideoPath, generateRtmpUrl(serverIp, currentStreamId)); } }这个定时任务每30秒检查一次流状态如果发现流断开就自动重新推流。在实际部署时建议将这个间隔调整为10-15秒以更快地响应中断情况。5. 常见问题排查5.1 FFmpeg推流失败排查当FFmpeg推流失败时可以按照以下步骤排查检查ZLMediaKit服务是否正常运行docker ps -a | grep zlmediakit检查端口是否开放telnet server_ip 1935查看FFmpeg详细日志添加-loglevel debug参数ffmpeg -loglevel debug -re -i input.mp4 -f flv rtmp://server/live/stream检查ZLMediaKit日志docker logs zlm-server5.2 延迟优化技巧视频推流的延迟是常见问题以下几个方法可以有效降低延迟在FFmpeg命令中添加低延迟参数ffmpeg -re -i input.mp4 -c:v libx264 -preset ultrafast -tune zerolatency -x264-params keyint30:min-keyint30 -f flv rtmp://server/live/stream在ZLMediaKit配置中调整以下参数config.ini[rtmp] gop_cache0 [rtp] low_latency1客户端使用HTTP-FLV协议播放比HLS延迟更低5.3 内存泄漏问题长时间运行的推流服务可能会出现内存泄漏特别是频繁创建和销毁FFmpeg进程时。解决方法包括使用进程池管理FFmpeg进程避免频繁创建销毁定期重启ZLMediaKit服务可以通过cron job实现监控服务器内存使用情况设置自动报警我在一个实际项目中遇到过内存泄漏问题最终发现是因为没有正确关闭FFmpeg的输入输出流。解决方案是在Java代码中显式地销毁进程并回收资源public class SafeProcessDestroyer { public static void destroyProcess(Process process) { try { // 先关闭输入输出流 if (process.getInputStream() ! null) { process.getInputStream().close(); } if (process.getErrorStream() ! null) { process.getErrorStream().close(); } if (process.getOutputStream() ! null) { process.getOutputStream().close(); } // 然后销毁进程 process.destroy(); // 等待进程结束 process.waitFor(5, TimeUnit.SECONDS); // 强制终止 if (process.isAlive()) { process.destroyForcibly(); } } catch (Exception e) { log.error(销毁进程失败, e); } } }6. 项目部署与性能调优6.1 生产环境部署建议对于生产环境部署我有以下几点建议服务器选择视频推流对CPU要求较高建议选择计算优化型实例。对于1080p推流至少需要4核CPU4K推流则需要8核以上。网络配置确保服务器有足够的带宽。一个1080p视频流大约需要3-5Mbps的上行带宽。安全措施修改ZLMediaKit的默认API密钥配置防火墙规则只开放必要的端口启用HTTPS加密播放地址监控方案使用PrometheusGrafana监控服务器资源使用情况实现推流状态监控面板设置异常报警机制6.2 性能优化参数在config.ini中以下参数对性能影响较大[general] thread_num8 # 根据CPU核心数调整 [rtmp] gop_cache0 # 关闭GOP缓存降低延迟 [hls] file_buf_size1048576 # 文件缓冲区大小 [http] send_buf1048576 # 发送缓冲区大小 [rtp] low_latency1 # 开启低延迟模式对于高并发场景还需要调整Linux系统参数# 增加最大文件描述符数量 echo fs.file-max 100000 /etc/sysctl.conf # 增加端口范围 echo net.ipv4.ip_local_port_range 1024 65535 /etc/sysctl.conf # 应用修改 sysctl -p6.3 负载均衡与集群部署当单台服务器无法满足需求时可以考虑集群部署方案源站-边缘架构将ZLMediaKit部署为源站服务器使用Nginx做边缘节点负载均衡DNS轮询通过DNS解析将请求分发到不同服务器CDN集成将流推送到CDN网络利用CDN的分发能力Nginx配置示例负载均衡rtmp { server { listen 1935; application live { live on; # 推流到多个ZLMediaKit服务器 push rtmp://zlm-server1/live/stream; push rtmp://zlm-server2/live/stream; push rtmp://zlm-server3/live/stream; } } }7. 完整项目示例7.1 项目结构src/main/java ├── com/example/videostream │ ├── config │ │ └── ZLMediaKitConfig.java # 配置文件 │ ├── controller │ │ ├── StreamController.java # API接口 │ │ └── PlayerController.java # 播放页面 │ ├── service │ │ ├── StreamService.java # 推流服务 │ │ └── StreamMonitor.java # 流监控 │ ├── util │ │ └── ZLMediaKitUtil.java # 工具类 │ └── VideoStreamApplication.java # 启动类 src/main/resources ├── static │ ├── player.html # 播放器页面 │ └── js │ └── flv.js # flv.js播放器 ├── templates └── application.yml # 配置文件7.2 核心代码实现StreamService.java的完整实现Service Slf4j public class StreamService { Value(${zlm.server-ip}) private String serverIp; Value(${ffmpeg.path}) private String ffmpegPath; private MapString, Process processMap new ConcurrentHashMap(); public boolean startStream(String videoPath, String streamKey) { String outputUrl ZLMediaKitUtil.generateRtmpUrl(serverIp, streamKey); String command String.format(%s -re -i %s -c:v libx264 -preset ultrafast -tune zerolatency -f flv %s, ffmpegPath, videoPath, outputUrl); try { CommandLine cmdLine CommandLine.parse(command); DefaultExecutor executor new DefaultExecutor(); ExecuteWatchdog watchdog new ExecuteWatchdog(60000); executor.setWatchdog(watchdog); // 使用自定义ProcessDestroyer确保资源释放 Process process executor.execute(cmdLine, new ProcessDestroyer()); processMap.put(streamKey, process); return true; } catch (IOException e) { log.error(启动推流失败, e); return false; } } public boolean stopStream(String streamKey) { Process process processMap.get(streamKey); if (process ! null) { SafeProcessDestroyer.destroyProcess(process); processMap.remove(streamKey); return true; } return false; } public ListString getRunningStreams() { return new ArrayList(processMap.keySet()); } private static class ProcessDestroyer implements ExecuteResultHandler { Override public void onProcessComplete(int exitValue) { log.info(推流进程正常结束退出码{}, exitValue); } Override public void onProcessFailed(ExecuteException e) { log.error(推流进程异常结束, e); } } }7.3 前端播放器示例使用flv.js实现的前端播放页面player.html!DOCTYPE html html head title视频播放器/title script srchttps://cdn.bootcdn.net/ajax/libs/flv.js/1.6.2/flv.min.js/script /head body video idvideoElement controls width800/video script const streamKey new URLSearchParams(window.location.search).get(stream); if (flvjs.isSupported() streamKey) { const videoElement document.getElementById(videoElement); const flvPlayer flvjs.createPlayer({ type: flv, url: http://${window.location.hostname}/live/${streamKey}.flv }); flvPlayer.attachMediaElement(videoElement); flvPlayer.load(); flvPlayer.play(); // 错误处理 flvPlayer.on(flvjs.Events.ERROR, (errType, errDetail) { console.error(播放错误:, errType, errDetail); alert(播放失败请检查流状态); }); } else { alert(浏览器不支持FLV播放或缺少流参数); } /script /body /html8. 扩展功能思路8.1 视频处理流水线可以扩展视频处理功能实现自动化的处理流水线public void processAndStream(String inputPath, String streamKey) { // 步骤1视频转码预处理 String tempPath /tmp/processed_ System.currentTimeMillis() .mp4; String processCmd String.format(%s -i %s -vf scale1280:720 -c:v libx264 -preset fast %s, ffmpegPath, inputPath, tempPath); // 步骤2启动推流 String outputUrl ZLMediaKitUtil.generateRtmpUrl(serverIp, streamKey); String streamCmd String.format(%s -re -i %s -c copy -f flv %s, ffmpegPath, tempPath, outputUrl); // 使用PipedInputStream/PipedOutputStream连接两个命令 // ... }8.2 动态水印添加实现动态水印功能比如显示当前时间ffmpeg -re -i input.mp4 -vf drawtexttext%{localtime}:x10:y10:fontsize24:fontcolorwhite -c:v libx264 -f flv rtmp://server/live/stream8.3 多码率自适应推流使用FFmpeg生成多种码率的流配合ZLMediaKit实现自适应码率# 生成高清流 ffmpeg -re -i input.mp4 -c:v libx264 -b:v 3000k -f flv rtmp://server/live/hd_stream # 生成标清流 ffmpeg -re -i input.mp4 -c:v libx264 -b:v 1500k -s 1280x720 -f flv rtmp://server/live/sd_stream # 生成流畅流 ffmpeg -re -i input.mp4 -c:v libx264 -b:v 800k -s 854x480 -f flv rtmp://server/live/ld_stream然后在播放端根据网络条件切换不同的流。9. 项目总结与经验分享在实际实施这个方案的过程中我积累了一些宝贵的经验版本兼容性FFmpeg、ZLMediaKit和SpringBoot的版本组合需要仔细测试。曾经遇到过因为FFmpeg版本过旧导致H.265编码失败的问题最终通过升级到最新版解决。资源管理FFmpeg进程的资源释放非常重要。初期没有做好进程管理导致服务器内存泄漏后来引入了进程池和严格的资源释放机制才解决。监控告警生产环境必须建立完善的监控体系。我们使用Prometheus监控服务器指标Grafana做可视化并设置了当推流中断或服务器负载过高时的自动告警。文档记录所有自定义参数和配置变更都要详细记录。曾经因为忘记记录某个关键参数的调整导致后续排查问题时花费了大量时间。测试策略建议在实施前制定全面的测试方案包括不同视频格式的兼容性测试长时间运行的稳定性测试高并发场景下的压力测试网络波动情况下的容错测试对于想要进一步优化这个方案的开发者我建议可以从以下几个方向入手实现动态码率调整根据网络状况自动切换视频质量增加视频内容分析功能如人脸识别、物体检测等开发管理后台提供更直观的流管理和监控界面集成更完善的权限控制系统确保视频内容安全这个技术方案我们已经成功应用于在线教育、企业培训、安防监控等多个领域系统稳定运行超过2年单台服务器最高支持过200路并发推流。希望这些经验能够帮助到正在实施类似项目的开发者。