⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/weixin_73916358 「白开水不加冰」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

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>
文章目录
  1. 1. 1. 环境准备
    1. 1.1. 1.1 ZLMediaKit 安装配置
    2. 1.2. 1.2 FFmpeg 安装
  2. 2. 2. Spring Boot 后端实现
    1. 2.1. 2.1 添加依赖
    2. 2.2. 2.2 推流配置类
    3. 2.3. 2.3 推流服务类
    4. 2.4. 2.4 配置文件
  3. 3. 3. 使用说明
    1. 3.1. 3.1 推流流程
    2. 3.2. 3.2 播放流程