FFmpeg和ZLMediaKit 实现本地视频推流

文章目录

  • [1 本地视频推流](#1 本地视频推流)
    • [1.1 简介](#1.1 简介)
      • [1.1.1 FFmpeg](#1.1.1 FFmpeg)
      • [1.1.2 ZLMediaKit](#1.1.2 ZLMediaKit)
    • [1.2 环境准备](#1.2 环境准备)
      • [1.2.1 ZLMediaKit 安装配置](#1.2.1 ZLMediaKit 安装配置)
      • [1.2.2 FFmpeg 安装(可选)](#1.2.2 FFmpeg 安装(可选))
    • [1.3 整合](#1.3 整合)
      • [1.3.1 pom和配置](#1.3.1 pom和配置)
      • [1.3.2 推流配置类](#1.3.2 推流配置类)
      • [1.3.3 推流服务类](#1.3.3 推流服务类)
      • [1.3.4 前端部分](#1.3.4 前端部分)

1 本地视频推流

1.1 简介

FFmpeg 是一套强大的开源音视频处理工具,能够对本地视频进行解码、转码和推流。ZLMediaKit 则是一个轻量级、高效的流媒体服务器,负责接收并分发视频流。

两者结合时,通常由FFmpeg将处理后的视频流,以RTMPRTSP等协议推送至ZLMediaKit服务器,再由ZLMediaKit实现多终端、多协议的实时直播分发。

1.1.1 FFmpeg

FFmpeg 音视频编码、转换、处理的瑞士军刀,是一套可以记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它提供了录制、转换以及流化音视频的完整解决方案

主要网址:

特点:

  • 完整的音视频解决方案
  • 先进的编解码库libavcodec
  • 跨平台支持

核心工具:FFmpeg主要包含三个强大的命令行工具:

  • ffmpeg:用于音视频转码、转换。例如,将一个AVI文件转换为TS文件并设置视频码率:ffmpeg -i input.avi -b:v 640k output.ts
  • ffplay:一个简单的媒体播放器,可以用来快速播放音视频文件。例如:ffplay test.avi
  • ffprobe:用于查看多媒体文件的信息,比如格式、编码、流详情等。

1.1.2 ZLMediaKit

ZLMediaKit:运营级流媒体服务器与服务框架,基于C++11的高性能运营级流媒体服务框架主要网址:

特点:

  • 基于C++11,高性能
  • 支持多种流媒体协议及互转
  • 支持全平台,低延迟
  • 核心定位:它是一个流媒体服务器,负责接收、分发和转换各种格式的媒体流。
  • 协议支持:非常全面,支持RTSPRTMPHLSHTTP-FLVWebSocket-FLVWebRTC等主流协议,并能在这些协议间互相转换。

1.2 环境准备

1.2.1 ZLMediaKit 安装配置

下载安装

bash 复制代码
# 拉取镜像
docker pull zlmediakit/zlmediakit:master

# 启动
docker run -d \
  --name zlm-server \
  -p 1935:1935 \
  -p 8080: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)

bash 复制代码
[hls]
broadcastRecordTs=0
deleteDelaySec=600    # 推流的视频保存多久(10分钟)
fileBufSize=65536
filePath=./www    # 保存路径
segDur=2    # 单个.ts 切片时长(秒)。
segNum=1000  # 直播时.m3u8 里最多同时保留多少个切片。
segRetain=9999    # 磁盘上实际保留多少个历史切片

启动服务

bash 复制代码
# 查看启动状态
docker logs -f zlm-server

1.2.2 FFmpeg 安装(可选)

下载路径:https://www.gyan.dev/ffmpeg/builds/

选择正式版的ffmpeg,这两个都可以选

配置环境变量找到 bin 目录,将其配到 path 环境变量中,执行 ffmpeg --version 验证

与java集合方面:

  • Jaffree:需要本地/服务器已经有 ffmpeg 命令(需要配置路径)。
  • bytedeco/javacpp-presets:依赖里自带 FFmpeg 原生库(无需单独安装 ffmpeg)。
  • Commons Exec / ProcessBuilder:要安装 ffmpeg

1.3 整合

1.3.1 pom和配置

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-exec</artifactId>
    <version>1.3</version>
</dependency>

<!-- jaffree 需要的 -->
<dependency>
    <groupId>com.github.kokorin.jaffree</groupId>
    <artifactId>jaffree</artifactId>
    <version>0.11.0</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.25</version> <!-- 版本可调整 -->
</dependency>
<!-- bytedeco -->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>ffmpeg-platform</artifactId>
    <version>6.0-1.5.10</version> <!-- 版本号示例,请检查最新 -->
</dependency>

配置

yml 复制代码
# 文件上传配置
spring:
  servlet:
    multipart:
      max-file-size: 1GB
      max-request-size: 1GB
    
stream:
  zlm-host: 127.0.0.1
  rtmp-port: 1935
  http-port: 8099
  ffmpeg-path: ffmpeg
  video-path: \videos\      

1.3.2 推流配置类

java 复制代码
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;

}

1.3.3 推流服务类

java 复制代码
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;
        }
    }
    //此处是用 直接 runtime 方式 构建也可以用 Jaffree 方式
    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);
    }
}

1.3.4 前端部分

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://127.0.0.1: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>
相关推荐
liliangcsdn3 小时前
基于ollama运行27b gemma3解决ffmpeg命令生成问题
人工智能·ffmpeg
wwwzhouhui7 小时前
85-dify案例分享-不用等 OpenAI 邀请,Dify+Sora2工作流实测:写实动漫视频随手做,插件+教程全送
人工智能·音视频·sora2
SongYuLong的博客1 天前
ubuntu24.04 实现DLNA音频推送
音视频
浮生如梦_1 天前
图片转视频
图像处理·人工智能·计算机视觉·音视频
胡耀超1 天前
音频降噪技术:从原理到工具的完整指南(scipy librosa noisereduce soundfile pedalboard)
音视频·音频·scipy·降噪·soundfile·noisereduce·pedalboard
Everbrilliant891 天前
音视频编解码全流程之用Extractor后Decodec
ffmpeg·视频编解码·mediacodec·音视频解码·ffmpeg编解码·decodec·ndkmediacodec
零一iTEM1 天前
NS4168输出音频通过ESP32C3测试
c++·单片机·嵌入式硬件·mcu·音视频·智能家居
Industio_触觉智能2 天前
瑞芯微RK35XX系列FFmpeg硬件编解码实测,详细性能对比!
ffmpeg·rk3588·rk3568·编解码·rk3562·rk3576
小狮子安度因2 天前
FFmpeg暂停、逐帧和音量
ffmpeg