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. 安全考虑:在生产环境中应考虑身份验证和访问控制
相关推荐
+VX:Fegn089513 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
识君啊14 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端
CaracalTiger14 小时前
如何解决Unexpected token ‘<’, “<!doctype “… is not valid JSON 报错问题
java·开发语言·jvm·spring boot·python·spring cloud·json
苏渡苇15 小时前
Java + Redis + MySQL:工业时序数据缓存与持久化实战(适配高频采集场景)
java·spring boot·redis·后端·spring·缓存·架构
Hx_Ma1615 小时前
Springboot整合mybatis注解版
java·spring boot·mybatis
t***442316 小时前
Spring boot整合quartz方法
java·前端·spring boot
蛐蛐蜉蝣耶16 小时前
互联网大厂Java面试实录:当严肃面试官遇上搞笑程序员谢飞机
spring boot·微服务·java面试·电商系统·分布式系统·技术面试·程序员面试
enjoy嚣士17 小时前
springboot 之 时区问题
java·spring boot·后端·时区
TEC_INO17 小时前
Linux_19:RV1126的OSD模块和SDL_TTF结合输出H264文件
linux·运维·ffmpeg
沙河板混18 小时前
@RequestMapping的参数
java·spring boot·spring