当前位置: 首页 > news >正文

SpringBoot + FFmpeg + ZLMediaKit 实现本地视频推流

Java精选面试题(微信小程序):5000+道面试题和选择题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计、大厂真题等,在线随时刷题!

1. 环境准备

1.1 ZLMediaKit 安装配置

下载安装

# 拉取镜像 docker pull zlmediakit/zlmediakit:master # 启动 docker run -d \ --name zlm-server \ -p 1935:1935 \ -p 8099:80 \ -p 8554:554 \ -p 10000:10000 \ -p 10000:10000/udp \ -p 8000:8000/udp \ -v /docker-volumes/zlmediakit/conf/config.ini:/opt/media/conf/config.ini \ zlmediakit/zlmediakit:master

配置文件 (config.ini)

[hls] broadcastRecordTs=0 deleteDelaySec=300 # 推流的视频保存多久(5分钟) fileBufSize=65536 filePath=./www # 保存路径 segDur=2 # 单个.ts 切片时长(秒)。 segNum=1000 # 直播时.m3u8 里最多同时保留多少个切片。 segRetain=9999 # 磁盘上实际保留多少个历史切片

启动服务

# 查看启动状态 docker logs -f zlm-server
1.2 FFmpeg 安装
# 下载路径 https://www.gyan.dev/ffmpeg/builds/

这两个都可以选

配置环境变量

C:\ffmpeg\ffmpeg-7.0.2-essentials_build\bin

找到 bin 目录,将其配到 path 环境变量中。

出来版本就成功了。

2. Spring Boot 后端实现

2.1 添加依赖
<dependencies> <!-- 进程管理 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-exec</artifactId> <version>1.3</version> </dependency> </dependencies>
2.2 推流配置类
package com.lyk.plugflow.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "stream") public class StreamConfig { /** * ZLMediaKit服务地址 */ private String zlmHost; /** * RTMP推流端口 */ private Integer rtmpPort; /** * HTTP-FLV拉流端口 */ private Integer httpPort; /** * FFmpeg可执行文件路径 */ private String ffmpegPath; /** * 视频存储路径 */ private String videoPath; }
2.3 推流服务类
package com.lyk.plugflow.service; import com.lyk.plugflow.config.StreamConfig; import lombok.extern.slf4j.Slf4j; import org.apache.commons.exec.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; import java.io.File; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service public class StreamService { @Autowired private StreamConfig streamConfig; // 存储推流进程 private final Map<String, DefaultExecutor> streamProcesses = new ConcurrentHashMap<>(); // 添加手动停止标记 private final Map<String, Boolean> manualStopFlags = new ConcurrentHashMap<>(); /** * 开始推流 */ public boolean startStream(String videoPath, String streamKey) { try { // 检查视频文件是否存在 File videoFile = new File(videoPath); if (!videoFile.exists()) { log.error("视频文件不存在: {}", videoPath); return false; } // 构建RTMP推流地址 String rtmpUrl = String.format("rtmp://%s:%d/live/%s", streamConfig.getZlmHost(), streamConfig.getRtmpPort(), streamKey); // 构建FFmpeg命令 CommandLine cmdLine = getCommandLine(videoPath, rtmpUrl); // 创建执行器 DefaultExecutor executor = new DefaultExecutor(); executor.setExitValue(0); // 设置watchdog用于进程管理 ExecuteWatchdog watchdog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT); executor.setWatchdog(watchdog); // 设置输出流 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream); executor.setStreamHandler(streamHandler); // 异步执行 executor.execute(cmdLine, new ExecuteResultHandler() { @Override public void onProcessComplete(int exitValue) { log.info("推流完成, streamKey: {}, exitValue: {}", streamKey, exitValue); streamProcesses.remove(streamKey); } @Override public void onProcessFailed(ExecuteException e) { boolean isManualStop = manualStopFlags.remove(streamKey); if (isManualStop) { log.info("推流已手动停止, streamKey: {}", streamKey); } else { log.error("推流失败, streamKey: {}, error: {}", streamKey, e.getMessage()); } streamProcesses.remove(streamKey); } }); // 保存进程引用 streamProcesses.put(streamKey, executor); log.info("开始推流, streamKey: {}, rtmpUrl: {}", streamKey, rtmpUrl); return true; } catch (Exception e) { log.error("推流启动失败", e); return false; } } private CommandLine getCommandLine(String videoPath, String rtmpUrl) { CommandLine cmdLine = new CommandLine(streamConfig.getFfmpegPath()); cmdLine.addArgument("-re"); // 按原始帧率读取 cmdLine.addArgument("-i"); cmdLine.addArgument(videoPath); cmdLine.addArgument("-c:v"); cmdLine.addArgument("libx264"); // 视频编码 cmdLine.addArgument("-c:a"); cmdLine.addArgument("aac"); // 音频编码 cmdLine.addArgument("-f"); cmdLine.addArgument("flv"); // 输出格式 cmdLine.addArgument("-flvflags"); cmdLine.addArgument("no_duration_filesize"); cmdLine.addArgument(rtmpUrl); return cmdLine; } /** * 停止推流 */ public boolean stopStream(String streamKey) { try { DefaultExecutor executor = streamProcesses.get(streamKey); if (executor != null) { // 设置手动停止标记 manualStopFlags.put(streamKey, true); ExecuteWatchdog watchdog = executor.getWatchdog(); if (watchdog != null) { watchdog.destroyProcess(); } else { log.warn("进程没有watchdog,无法强制终止, streamKey: {}", streamKey); } streamProcesses.remove(streamKey); log.info("停止推流成功, streamKey: {}", streamKey); return true; } return false; } catch (Exception e) { log.error("停止推流失败", e); return false; } } /** * 获取拉流地址 */ public String getPlayUrl(String streamKey, String protocol) { return switch (protocol.toLowerCase()) { case "flv" -> String.format("http://%s:%d/live/%s.live.flv", streamConfig.getZlmHost(), streamConfig.getHttpPort(), streamKey); case "hls" -> String.format("http://%s:%d/live/%s/hls.m3u8", streamConfig.getZlmHost(), streamConfig.getHttpPort(), streamKey); default -> null; }; } /** * 检查推流状态 */ public boolean isStreaming(String streamKey) { return streamProcesses.containsKey(streamKey); } }
2.4 配置文件
stream: zlm-host: 192.168.159.129 rtmp-port: 1935 http-port: 8099 ffmpeg-path: ffmpeg video-path: \videos\ # 文件上传配置 spring: servlet: multipart: max-file-size: 1GB max-request-size: 1GB

3. 使用说明

3.1 推流流程
  • • 启动 ZLMediaKit 服务

  • • 上传视频文件到服务器

  • • 调用推流接口,指定视频路径和推流密钥

  • • Spring Boot 执行 FFmpeg 命令推流到 ZLMediaKit

3.2 播放流程
  • • 获取推流地址(HTTP-FLV 或 HLS)

  • • 支持实时播放和回放

ffmpeg -re -i "C:\Users\lyk19\Videos\8月9日.mp4" -c:v libx264 -preset ultrafast -tune zerolatency -c:a aac -ar 44100 -b:a 128k -f flv rtmp://192.168.159.129:1935/live/stream
  • • 前端播放

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>FLV直播播放器</title> <style> body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background-color: #f0f0f0; } .player-container { max-width: 800px; margin: 0 auto; background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } #videoElement { width: 100%; height: 450px; background-color: #000; border-radius: 4px; } .controls { margin-top: 15px; text-align: center; } button { padding: 10px 20px; margin: 0 5px; border: none; border-radius: 4px; background-color: #007bff; color: white; cursor: pointer; font-size: 14px; } button:hover { background-color: #0056b3; } button:disabled { background-color: #ccc; cursor: not-allowed; } .status { margin-top: 10px; padding: 10px; border-radius: 4px; text-align: center; } .status.success { background-color: #d4edda; color: #155724; } .status.error { background-color: #f8d7da; color: #721c24; } .status.info { background-color: #d1ecf1; color: #0c5460; } </style> </head> <body> <div class="player-container"> <h1>FLV直播播放器</h1> <video id="videoElement" controls muted> 您的浏览器不支持视频播放 </video> <div class="controls"> <button id="playBtn">播放</button> <button id="pauseBtn" disabled>暂停</button> <button id="stopBtn" disabled>停止</button> <button id="muteBtn">静音</button> </div> <div id="status" class="status info"> 准备就绪,点击播放开始观看直播 </div> </div> <!-- 使用flv.js库 --> <script src="https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js"></script> <script> let flvPlayer = null; const videoElement = document.getElementById('videoElement'); const playBtn = document.getElementById('playBtn'); const pauseBtn = document.getElementById('pauseBtn'); const stopBtn = document.getElementById('stopBtn'); const muteBtn = document.getElementById('muteBtn'); const statusDiv = document.getElementById('status'); // 你的流地址 const streamUrl = 'http://192.168.159.129:8099/live/stream.live.flv'; function updateStatus(message, type) { statusDiv.textContent = message; statusDiv.className = `status ${type}`; console.log(`[${type.toUpperCase()}] ${message}`); } function updateButtons(playEnabled, pauseEnabled, stopEnabled) { playBtn.disabled = !playEnabled; pauseBtn.disabled = !pauseEnabled; stopBtn.disabled = !stopEnabled; } // 检查浏览器支持 if (!flvjs.isSupported()) { updateStatus('您的浏览器不支持FLV播放,请使用Chrome、Firefox或Edge浏览器', 'error'); playBtn.disabled = true; } // 播放功能 playBtn.addEventListener('click', function () { try { if (flvPlayer) { flvPlayer.destroy(); } // 创建FLV播放器 flvPlayer = flvjs.createPlayer({ type: 'flv', url: streamUrl, isLive: true }, { enableWorker: false, lazyLoad: true, lazyLoadMaxDuration: 3 * 60, deferLoadAfterSourceOpen: false, autoCleanupSourceBuffer: true, enableStashBuffer: false }); flvPlayer.attachMediaElement(videoElement); flvPlayer.load(); // 监听事件 flvPlayer.on(flvjs.Events.ERROR, function (errorType, errorDetail, errorInfo) { console.error('FLV播放器错误:', errorType, errorDetail, errorInfo); updateStatus(`播放错误: ${errorDetail}`, 'error'); }); flvPlayer.on(flvjs.Events.LOADING_COMPLETE, function () { updateStatus('流加载完成', 'success'); }); flvPlayer.on(flvjs.Events.RECOVERED_EARLY_EOF, function () { updateStatus('从早期EOF恢复', 'info'); }); // 开始播放 videoElement.play().then(() => { updateStatus('正在播放直播流', 'success'); updateButtons(false, true, true); }).catch(error => { console.error('播放失败:', error); updateStatus('播放失败: ' + error.message, 'error'); }); } catch (error) { console.error('创建播放器失败:', error); updateStatus('创建播放器失败: ' + error.message, 'error'); } }); // 暂停功能 pauseBtn.addEventListener('click', function () { if (videoElement && !videoElement.paused) { videoElement.pause(); updateStatus('播放已暂停', 'info'); updateButtons(true, false, true); } }); // 停止功能 stopBtn.addEventListener('click', function () { if (flvPlayer) { flvPlayer.pause(); flvPlayer.unload(); flvPlayer.destroy(); flvPlayer = null; } videoElement.src = ''; videoElement.load(); updateStatus('播放已停止', 'info'); updateButtons(true, false, false); }); // 静音功能 muteBtn.addEventListener('click', function () { videoElement.muted = !videoElement.muted; muteBtn.textContent = videoElement.muted ? '取消静音' : '静音'; updateStatus(videoElement.muted ? '已静音' : '已取消静音', 'info'); }); // 视频事件监听 videoElement.addEventListener('loadstart', function () { updateStatus('开始加载视频流...', 'info'); }); videoElement.addEventListener('canplay', function () { updateStatus('视频流已准备就绪', 'success'); }); videoElement.addEventListener('playing', function () { updateStatus('正在播放直播流', 'success'); updateButtons(false, true, true); }); videoElement.addEventListener('pause', function () { updateStatus('播放已暂停', 'info'); updateButtons(true, false, true); }); videoElement.addEventListener('error', function (e) { updateStatus('视频播放出错', 'error'); updateButtons(true, false, false); }); </script> </body> </html>

来源:https://blog.csdn.net/weixin_73916358

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理! 最近有很多人问,有没有读者或者摸鱼交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群! 点击“阅读原文”,了解更多精彩内容!文章有帮助的话,点在看,转发吧!
http://www.jsqmd.com/news/104097/

相关文章:

  • 2025年长沙好一点的美容学校推荐,去美容学校要多少钱? - mypinpai
  • 自动化测试框架搭建:持续验证EmotiVoice输出质量
  • vue基于springboot的大学生校园跑腿服务系统的设计与实现沙箱支付
  • 【TMS320开发】基于TMS320F28377SPTPS的SPI通信开发实战
  • EmotiVoice语音合成与音乐背景融合技巧:制作电台节目
  • 情感语音数据库建设:助力EmotiVoice持续迭代
  • 国内用户福利:一站式使用全球主流AI大模型,无需魔法,无限畅享
  • 基于SpringBoot+Vue的大学生一体化服务系统源码文档部署文档代码讲解等
  • 2025年全日制托管学校权威指南:破解成长困境,择校更需专业 - 深度智识库
  • 每天一个网络知识:什么是 VXLAN?
  • vue基于springboot的高校两校区通勤校车预约系统的设计与实现 论文
  • EmotiVoice语音合成系统自动化测试框架搭建经验
  • 2025年男孩取名机构联系方式汇总:全国知名机构官方联系通道与专业选择指南 - 品牌推荐
  • 结合ASR构建完整对话系统:EmotiVoice的角色定位
  • 断网也不丢数据:北斗形变监测的多链路冗余与断网续传实战解析
  • 基于SpringBoot+Vue的宠物医疗管理系统的设计与实现源码文档部署文档代码讲解等
  • 不间断电源UPS所有的知识点都总结好了,值得收藏!
  • 敏感肌沐浴露十大品牌排名推荐!沐浴露哪个牌子低敏靠谱?地黄植萃净痘修护效果好 - 博客万
  • MAX-M10S-00B,超低功耗GNSS接收器
  • 实用指南:智能化制造与工业互联网的未来:企业数字化转型的关键力量
  • 攻击者利用React2Shell漏洞部署Linux后门程序,日本成重点攻击目标
  • Hadoop 从入门到精通:生态解析、核心原理与实战优化
  • 2025年末叛逆学校权威推荐:心理赋能+科学矫正,5家正规机构实测榜单 - 深度智识库
  • DevExtreme JS ASP.NET Core v25.2预览 - DataGrid/TreeList全新升级
  • 基于SpringBoot+Vue的传统文化交流交易平台系统源码文档部署文档代码讲解等
  • 新手教程!Windows Server装雷池WAF,图形化操作护好内网OA系统
  • 襄阳装修公司推荐和口碑、服务指南:严控增项、保障环保,五大口碑品牌深度解析 - 品牌企业推荐师(官方)
  • 暗网Omertà市场因真实服务器IP泄露被迫关停
  • 破局大数据测试:技术挑战与体系化应对策略
  • 2025年靠谱的鹤壁装修风格/鹤壁装修真实用户推荐榜 - 行业平台推荐