基于SpringBoot+FFmpeg+ZLMediaKit實(shí)現(xiàn)本地視頻推流
1. 環(huán)境準(zhǔn)備
1.1 ZLMediaKit 安裝配置
下載安裝
# 拉取鏡像 docker pull zlmediakit/zlmediakit:master # 啟動(dòng) 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 # 單個(gè).ts 切片時(shí)長(zhǎng)(秒)。 segNum=1000 # 直播時(shí).m3u8 里最多同時(shí)保留多少個(gè)切片。 segRetain=9999 # 磁盤上實(shí)際保留多少個(gè)歷史切片
啟動(dòng)服務(wù)
# 查看啟動(dòng)狀態(tài) docker logs -f zlm-server
1.2 FFmpeg 安裝
# 下載路徑 https://www.gyan.dev/ffmpeg/builds/
這兩個(gè)都可以選

配置環(huán)境變量
C:\ffmpeg\ffmpeg-7.0.2-essentials_build\bin
找到 bin 目錄,將其配到 path 環(huán)境變量中。

出來(lái)版本就成功了。
2. Spring Boot 后端實(shí)現(xiàn)
2.1 添加依賴
<dependencies>
<!-- 進(jìn)程管理 -->
<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服務(wù)地址
*/
private String zlmHost;
/**
* RTMP推流端口
*/
private Integer rtmpPort;
/**
* HTTP-FLV拉流端口
*/
private Integer httpPort;
/**
* FFmpeg可執(zhí)行文件路徑
*/
private String ffmpegPath;
/**
* 視頻存儲(chǔ)路徑
*/
private String videoPath;
}
2.3 推流服務(wù)類
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;
// 存儲(chǔ)推流進(jìn)程
private final Map<String, DefaultExecutor> streamProcesses = new ConcurrentHashMap<>();
// 添加手動(dòng)停止標(biāo)記
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);
returnfalse;
}
// 構(gòu)建RTMP推流地址
String rtmpUrl = String.format("rtmp://%s:%d/live/%s",
streamConfig.getZlmHost(), streamConfig.getRtmpPort(), streamKey);
// 構(gòu)建FFmpeg命令
CommandLine cmdLine = getCommandLine(videoPath, rtmpUrl);
// 創(chuàng)建執(zhí)行器
DefaultExecutor executor = new DefaultExecutor();
executor.setExitValue(0);
// 設(shè)置watchdog用于進(jìn)程管理
ExecuteWatchdog watchdog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT);
executor.setWatchdog(watchdog);
// 設(shè)置輸出流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
executor.setStreamHandler(streamHandler);
// 異步執(zhí)行
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("推流已手動(dòng)停止, streamKey: {}", streamKey);
} else {
log.error("推流失敗, streamKey: {}, error: {}", streamKey, e.getMessage());
}
streamProcesses.remove(streamKey);
}
});
// 保存進(jìn)程引用
streamProcesses.put(streamKey, executor);
log.info("開始推流, streamKey: {}, rtmpUrl: {}", streamKey, rtmpUrl);
returntrue;
} catch (Exception e) {
log.error("推流啟動(dòng)失敗", e);
returnfalse;
}
}
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) {
// 設(shè)置手動(dòng)停止標(biāo)記
manualStopFlags.put(streamKey, true);
ExecuteWatchdog watchdog = executor.getWatchdog();
if (watchdog != null) {
watchdog.destroyProcess();
} else {
log.warn("進(jìn)程沒(méi)有watchdog,無(wú)法強(qiáng)制終止, streamKey: {}", streamKey);
}
streamProcesses.remove(streamKey);
log.info("停止推流成功, streamKey: {}", streamKey);
returntrue;
}
returnfalse;
} catch (Exception e) {
log.error("停止推流失敗", e);
returnfalse;
}
}
/**
* 獲取拉流地址
*/
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;
};
}
/**
* 檢查推流狀態(tài)
*/
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. 使用說(shuō)明
3.1 推流流程
- • 啟動(dòng) ZLMediaKit 服務(wù)
- • 上傳視頻文件到服務(wù)器
- • 調(diào)用推流接口,指定視頻路徑和推流密鑰
- • Spring Boot 執(zhí)行 FFmpeg 命令推流到 ZLMediaKit
3.2 播放流程
- • 獲取推流地址(HTTP-FLV 或 HLS)
- • 支持實(shí)時(shí)播放和回放
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">
準(zhǔn)備就緒,點(diǎn)擊播放開始觀看直播
</div>
</div>
<!-- 使用flv.js庫(kù) -->
<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播放,請(qǐng)使用Chrome、Firefox或Edge瀏覽器', 'error');
playBtn.disabled = true;
}
// 播放功能
playBtn.addEventListener('click', function () {
try {
if (flvPlayer) {
flvPlayer.destroy();
}
// 創(chuàng)建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();
// 監(jiān)聽事件
flvPlayer.on(flvjs.Events.ERROR, function (errorType, errorDetail, errorInfo) {
console.error('FLV播放器錯(cuò)誤:', errorType, errorDetail, errorInfo);
updateStatus(`播放錯(cuò)誤: ${errorDetail}`, 'error');
});
flvPlayer.on(flvjs.Events.LOADING_COMPLETE, function () {
updateStatus('流加載完成', 'success');
});
flvPlayer.on(flvjs.Events.RECOVERED_EARLY_EOF, function () {
updateStatus('從早期EOF恢復(fù)', 'info');
});
// 開始播放
videoElement.play().then(() => {
updateStatus('正在播放直播流', 'success');
updateButtons(false, true, true);
}).catch(error => {
console.error('播放失敗:', error);
updateStatus('播放失敗: ' + error.message, 'error');
});
} catch (error) {
console.error('創(chuàng)建播放器失敗:', error);
updateStatus('創(chuàng)建播放器失敗: ' + 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');
});
// 視頻事件監(jiān)聽
videoElement.addEventListener('loadstart', function () {
updateStatus('開始加載視頻流...', 'info');
});
videoElement.addEventListener('canplay', function () {
updateStatus('視頻流已準(zhǔn)備就緒', '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('視頻播放出錯(cuò)', 'error');
updateButtons(true, false, false);
});
</script>
</body>
</html>
以上就是基于SpringBoot+FFmpeg+ZLMediaKit實(shí)現(xiàn)本地視頻推流的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot FFmpeg本地視頻推流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ArrayList源碼探秘之Java動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)
這篇文章將帶大家從ArrayList源碼來(lái)探秘一下Java動(dòng)態(tài)數(shù)組的實(shí)現(xiàn),文中的示例代碼講解詳細(xì),對(duì)我們深入了解JavaScript有一定的幫助,需要的可以參考一下2023-08-08
Java使用Gateway自定義負(fù)載均衡過(guò)濾器
這篇文章主要介紹了Java使用Gateway自定義負(fù)載均衡過(guò)濾器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
spring cloud實(shí)現(xiàn)Eureka注冊(cè)中心的HA的方法
本篇文章主要介紹了spring cloud實(shí)現(xiàn)Eureka注冊(cè)中心的HA的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
springBoot接入阿里云oss的實(shí)現(xiàn)步驟
這篇文章主要介紹了springBoot接入阿里云oss的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
JAVA 字符串加密、密碼加密實(shí)現(xiàn)方法
這篇文章主要介紹了JAVA 字符串加密、密碼加密實(shí)現(xiàn)方法的相關(guān)資料,需要的朋友可以參考下2016-10-10
關(guān)于MyBatis中映射對(duì)象關(guān)系的舉例
這篇文章主要介紹了關(guān)于MyBatis中映射對(duì)象關(guān)系的舉例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06

