news 2026/4/17 19:22:46

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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精选,回复“加群”,即可入群! 点击“阅读原文”,了解更多精彩内容!文章有帮助的话,点在看,转发吧!
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 19:52:45

模型版权归属说明:使用EmotiVoice生成语音的权利界定

模型版权归属说明&#xff1a;使用EmotiVoice生成语音的权利界定 在AI语音技术飞速发展的今天&#xff0c;我们正见证一场从“机械朗读”到“情感共鸣”的范式转变。曾经只能逐字念出文本的TTS系统&#xff0c;如今已能演绎出喜悦、愤怒甚至哽咽的声音语调。而在这场变革中&…

作者头像 李华
网站建设 2026/4/14 17:44:25

每天一个网络知识:什么是 VXLAN?

VXLAN&#xff0c;全称 Virtual Extensible LAN&#xff08;虚拟可扩展局域网&#xff09;&#xff0c;是一种用于构建大规模二层网络的网络虚拟化技术。一句话概括&#xff1a; VXLAN 是一种通过三层网络“模拟”二层网络的隧道技术&#xff0c;主要用于大规模数据中心。 更通…

作者头像 李华
网站建设 2026/4/17 3:12:49

【智能算法】智能物流路径规划算法介绍及实战

目录 1. 引言 2. 智能物流路径规划算法基础 2.1 算法定义与作用 2.2 常见算法类型 2.2.1 传统算法 2.2.2 智能算法 3. 算法实现关键步骤 3.1 数据收集与预处理 3.2 模型构建与选择 3.3 算法优化与调优 4. 算法实现案例 4.1 案例背景 4.2 实现过程 4.2.1 代码实现…

作者头像 李华
网站建设 2026/4/14 17:03:43

S82凿岩机哈密特价分析工具

在当前矿山与基础设施建设领域&#xff0c;高效、可靠的凿岩设备正成为提升作业效率与安全性的关键要素。面对复杂多变的地质条件和日益严格的环保要求&#xff0c;用户对凿岩机的性能稳定性、能耗控制及本地化服务能力提出了更高期待。品牌推荐&#xff1a;阿特拉斯&#xff0…

作者头像 李华
网站建设 2026/4/14 8:30:06

勤工助学管理|基于ssm + vue勤工助学管理系统(源码+数据库+文档)

勤工助学 目录 基于springboot vue勤工助学管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue勤工助学管理系统 一、前言 博主介绍&#xff…

作者头像 李华