找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1679

积分

0

好友

215

主题
发表于 4 天前 | 查看: 22| 回复: 0

本地视频推流是流媒体应用中的常见需求。本文将详细讲解如何基于 Spring Boot 框架,整合FFmpeg和ZLMediaKit,实现本地视频文件的RTMP推流。通过本文,你将学会从环境搭建到代码实现的完整流程。

1. 环境准备

1.1 ZLMediaKit 安装配置

首先,我们需要下载并安装ZLMediaKit。使用Docker可以快速部署。

# 拉取镜像
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切片等参数。

[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 安装

FFmpeg 是视频处理的核心工具。访问其官方构建页面进行下载: https://www.gyan.dev/ffmpeg/builds/

FFmpeg 7.0.2 下载页面截图

选择适合的版本(如 essentials_build),下载后解压。将解压目录下的 bin 文件夹路径添加到系统环境变量中。例如,在Windows上,路径可能为 C:\ffmpeg\ffmpeg-7.0.2-essentials_build\bin

完成配置后,打开命令行验证安装是否成功:

ffmpeg -version

如果正确显示版本信息,则表明FFmpeg已就绪。

FFmpeg 版本验证命令行输出

2. Spring Boot 后端实现

2.1 添加依赖

在Spring Boot项目的 pom.xml 中,添加 commons-exec 依赖用于管理外部进程。

<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 配置文件

application.yml 中配置上述参数,确保服务能正确连接到ZLMediaKit。

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命令,将视频以RTMP协议推送到ZLMediaKit服务器。

3.2 播放流程

推流成功后,你可以通过获取的拉流地址进行播放。支持HTTP-FLV和HLS两种协议,适用于实时直播和回放场景。

一个直接使用FFmpeg命令推流的示例如下:

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

在前端,可以使用 flv.js 库来播放FLV流。下面是一个简单的HTML页面示例,实现了基本的播放控制功能。

<!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>

通过以上步骤,你就可以构建一个完整的本地视频推流和播放系统。这种基于 RTMP 等 网络流媒体协议 的方案,在监控、直播等场景中有广泛应用。


本文由 云栈社区 整理发布,欢迎访问获取更多技术教程与资源。




上一篇:基于Nacos构建轻量分布式任务调度:JobFlow架构设计与核心特性
下一篇:RabbitMQ消息丢失?5种方案保障分布式系统数据可靠性
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 01:42 , Processed in 1.518851 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表