How to Stream Local Video with Spring Boot, FFmpeg, and ZLMediaKit

This guide walks through setting up ZLMediaKit in Docker, installing FFmpeg, adding a Spring Boot backend with configuration and service classes to launch and manage RTMP streams, and provides front‑end HTML/JavaScript for playing the live FLV or HLS streams.

Java Companion
Java Companion
Java Companion
How to Stream Local Video with Spring Boot, FFmpeg, and ZLMediaKit

Environment Preparation

ZLMediaKit installation

Pull the official Docker image and run a container exposing the required ports and mounting a custom config.ini file.

# Pull image
docker pull zlmediakit/zlmediakit:master

# Run container
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

Key config.ini settings control HLS segment length and retention.

[hls]
broadcastRecordTs=0
deleteDelaySec=300    # video retained for 5 minutes
fileBufSize=65536
filePath=./www       # storage path
segDur=2              # segment duration (seconds)
segNum=1000           # max segments in .m3u8
segRetain=9999        # actual retained segments on disk

FFmpeg installation

Download a Windows build from https://www.gyan.dev/ffmpeg/builds/ and add its bin directory (e.g., C:\ffmpeg\ffmpeg-7.0.2-essentials_build\bin) to the system PATH environment variable.

Spring Boot Backend Implementation

Maven dependency

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-exec</artifactId>
        <version>1.3</version>
    </dependency>
</dependencies>

Stream configuration class

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 {
    private String zlmHost;      // ZLMediaKit service address
    private Integer rtmpPort;   // RTMP port
    private Integer httpPort;    // HTTP‑FLV port
    private String ffmpegPath;   // FFmpeg executable path
    private String videoPath;    // Video storage path
}

Stream service class

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;

    // Store running processes
    private final Map<String, DefaultExecutor> streamProcesses = new ConcurrentHashMap<>();
    // Manual stop flags
    private final Map<String, Boolean> manualStopFlags = new ConcurrentHashMap<>();

    /** Start streaming */
    public boolean startStream(String videoPath, String streamKey) {
        try {
            File videoFile = new File(videoPath);
            if (!videoFile.exists()) {
                log.error("视频文件不存在: {}", videoPath);
                return false;
            }
            String rtmpUrl = String.format("rtmp://%s:%d/live/%s",
                    streamConfig.getZlmHost(), streamConfig.getRtmpPort(), streamKey);
            CommandLine cmdLine = getCommandLine(videoPath, rtmpUrl);
            DefaultExecutor executor = new DefaultExecutor();
            executor.setExitValue(0);
            ExecuteWatchdog watchdog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT);
            executor.setWatchdog(watchdog);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            executor.setStreamHandler(new PumpStreamHandler(outputStream));
            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"); // read at native frame rate
        cmdLine.addArgument("-i");
        cmdLine.addArgument(videoPath);
        cmdLine.addArgument("-c:v");
        cmdLine.addArgument("libx264"); // video codec
        cmdLine.addArgument("-c:a");
        cmdLine.addArgument("aac"); // audio codec
        cmdLine.addArgument("-f");
        cmdLine.addArgument("flv"); // output format
        cmdLine.addArgument("-flvflags");
        cmdLine.addArgument("no_duration_filesize");
        cmdLine.addArgument(rtmpUrl);
        return cmdLine;
    }

    /** Stop streaming */
    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;
        }
    }

    /** Get playback URL */
    public String getPlayUrl(String streamKey, String protocol) {
        switch (protocol.toLowerCase()) {
            case "flv":
                return String.format("http://%s:%d/live/%s.live.flv",
                        streamConfig.getZlmHost(), streamConfig.getHttpPort(), streamKey);
            case "hls":
                return String.format("http://%s:%d/live/%s/hls.m3u8",
                        streamConfig.getZlmHost(), streamConfig.getHttpPort(), streamKey);
            default:
                return null;
        }
    }

    /** Check if a stream is active */
    public boolean isStreaming(String streamKey) {
        return streamProcesses.containsKey(streamKey);
    }
}

Application configuration (application.yml)

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

Usage Instructions

Streaming workflow

Start the ZLMediaKit Docker container.

Upload the source video file to the server.

Call the startStream API with the video path and a unique stream key.

Spring Boot launches an FFmpeg command that pushes the video to ZLMediaKit via RTMP.

Playback workflow

Obtain a playback URL (HTTP‑FLV or HLS) via getPlayUrl.

Use a front‑end player (e.g., flv.js) to play the live stream or on‑demand replay.

Example FFmpeg command used by the service:

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

Front‑end player (HTML + JavaScript)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>FLV Live Player</title>
  <!-- Include flv.js library from CDN -->
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/flv.min.js"></script>
</head>
<body>
  <div class="player-container">
    <h2>FLV Live Player</h2>
    <video id="videoElement" controls muted>Your browser does not support video playback</video>
    <div class="controls">
      <button id="playBtn">Play</button>
      <button id="pauseBtn" disabled>Pause</button>
      <button id="stopBtn" disabled>Stop</button>
      <button id="muteBtn">Mute</button>
    </div>
    <div id="status" class="status info">Ready, click Play to start</div>
  </div>
  <script>
    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';
    let flvPlayer = null;

    function updateStatus(message, type) {
      statusDiv.textContent = message;
      statusDiv.className = `status ${type}`;
      console.log(`[${type.toUpperCase()}] ${message}`);
    }

    function updateButtons(play, pause, stop) {
      playBtn.disabled = !play;
      pauseBtn.disabled = !pause;
      stopBtn.disabled = !stop;
    }

    if (!flvjs.isSupported()) {
      updateStatus('Your browser does not support FLV playback.', 'error');
      playBtn.disabled = true;
    }

    playBtn.addEventListener('click', function() {
      try {
        if (flvPlayer) flvPlayer.destroy();
        flvPlayer = flvjs.createPlayer({type: 'flv', url: streamUrl, isLive: true}, {
          enableWorker: false,
          lazyLoad: true,
          lazyLoadMaxDuration: 180,
          deferLoadAfterSourceOpen: false,
          autoCleanupSourceBuffer: true,
          enableStashBuffer: false
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.on(flvjs.Events.ERROR, (type, detail, info) => {
          console.error('FLV player error:', type, detail, info);
          updateStatus(`Play error: ${detail}`, 'error');
        });
        flvPlayer.on(flvjs.Events.LOADING_COMPLETE, () => {
          updateStatus('Stream loaded', 'success');
        });
        videoElement.play().then(() => {
          updateStatus('Playing live stream', 'success');
          updateButtons(false, true, true);
        }).catch(err => {
          console.error('Play failed:', err);
          updateStatus('Play failed: ' + err.message, 'error');
        });
      } catch (e) {
        console.error('Create player failed:', e);
        updateStatus('Create player failed: ' + e.message, 'error');
      }
    });

    pauseBtn.addEventListener('click', function() {
      if (videoElement && !videoElement.paused) {
        videoElement.pause();
        updateStatus('Playback paused', 'info');
        updateButtons(true, false, true);
      }
    });

    stopBtn.addEventListener('click', function() {
      if (flvPlayer) {
        flvPlayer.pause();
        flvPlayer.unload();
        flvPlayer.destroy();
        flvPlayer = null;
      }
      videoElement.src = '';
      videoElement.load();
      updateStatus('Playback stopped', 'info');
      updateButtons(true, false, false);
    });

    muteBtn.addEventListener('click', function() {
      videoElement.muted = !videoElement.muted;
      muteBtn.textContent = videoElement.muted ? 'Unmute' : 'Mute';
      updateStatus(videoElement.muted ? 'Muted' : 'Unmuted', 'info');
    });

    videoElement.addEventListener('loadstart', () => updateStatus('Loading video stream...', 'info'));
    videoElement.addEventListener('canplay', () => updateStatus('Video ready', 'success'));
    videoElement.addEventListener('playing', () => {
      updateStatus('Playing live stream', 'success');
      updateButtons(false, true, true);
    });
    videoElement.addEventListener('pause', () => {
      updateStatus('Playback paused', 'info');
      updateButtons(true, false, true);
    });
    videoElement.addEventListener('error', () => {
      updateStatus('Video playback error', 'error');
      updateButtons(true, false, false);
    });
  </script>
</body>
</html>
JavaSpring Bootffmpegvideo streamingRTMPFLVZLMediaKit
Java Companion
Written by

Java Companion

A highly professional Java public account

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.