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

ZLMediaKit + Spring Boot + FFmpeg 流媒体系统部署指南

1. 环境准备

1.1 ZLMediaKit 安装配置

下载安装
bash 复制代码
# 拉取镜像
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)
ini 复制代码
[hls]
broadcastRecordTs=0
deleteDelaySec=300    # 推流的视频保存多久(5分钟)
fileBufSize=65536
filePath=./www        # 保存路径
segDur=2              # 单个.ts 切片时长(秒)
segNum=1000           # 直播时.m3u8 里最多同时保留多少个切片
segRetain=9999        # 磁盘上实际保留多少个历史切片
启动服务
bash 复制代码
# 查看启动状态
docker logs -f zlm-server

1.2 FFmpeg 安装

Windows 安装步骤
  1. 下载路径: https://www.gyan.dev/ffmpeg/builds/

  2. 选择版本 : 推荐下载 ffmpeg-release-essentials.zip

  3. 解压路径 : C:\ffmpeg\

  4. 配置环境变量 :

    • C:\ffmpeg\bin 添加到系统 PATH 环境变量中
  5. 验证安装 :

    bash 复制代码
    ffmpeg -version

2. Spring Boot 后端实现

2.1 添加依赖

xml 复制代码
<dependencies>
    <!-- 进程管理 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-exec</artifactId>
        <version>1.3</version>
    </dependency>
</dependencies>

2.2 推流配置类

java 复制代码
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 推流服务类

java 复制代码
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 配置文件

yaml 复制代码
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 推流流程

  1. 启动 ZLMediaKit 服务
  2. 上传视频文件到服务器
  3. 调用推流接口,指定视频路径和推流密钥
  4. Spring Boot 执行 FFmpeg 命令推流到 ZLMediaKit
FFmpeg 推流命令示例
bash 复制代码
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

3.2 播放流程

  1. 获取推流地址(HTTP-FLV 或 HLS)
  2. 支持实时播放和回放
拉流地址格式
  • FLV格式 : http://192.168.159.129:8099/live/stream.live.flv
  • HLS格式 : http://192.168.159.129:8099/live/stream/hls.m3u8

前端播放示例

完整的前端播放器代码已提供,包含以下功能:

  • FLV.js 直播流播放
  • 播放/暂停/停止控制
  • 静音功能
  • 状态显示和错误处理
  • 跨浏览器兼容性检查
播放器配置要点:
javascript 复制代码
const flvPlayer = flvjs.createPlayer({
    type: 'flv',
    url: 'http://192.168.159.129:8099/live/stream.live.flv',
    isLive: true
}, {
    enableWorker: false,
    lazyLoad: true,
    lazyLoadMaxDuration: 3 * 60,
    deferLoadAfterSourceOpen: false,
    autoCleanupSourceBuffer: true,
    enableStashBuffer: false
});

系统架构图

复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   视频文件      │────▶│   Spring Boot   │────▶│    FFmpeg       │
│                 │    │    后端服务      │    │                 │
└─────────────────┘    └─────────────────┘    └────────┬────────┘
                                                        │
┌─────────────────┐    ┌─────────────────┐             ▼
│   前端播放器    │────▶│   ZLMediaKit    │◀─────────────
│                 │    │   流媒体服务器   │
└─────────────────┘    └─────────────────┘

注意事项

  1. 网络配置:确保所有端口(1935、8099等)在防火墙中开放
  2. 文件权限:确保视频存储目录有读写权限
  3. 资源监控:长时间推流需要监控系统资源使用情况
  4. 错误处理:实现完善的错误处理和重试机制
  5. 安全考虑:在生产环境中应考虑身份验证和访问控制
相关推荐
用户9083246027315 小时前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840821 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解1 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解1 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记2 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者2 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840822 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解2 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者3 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺3 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端