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

This guide walks through installing ZLMediaKit via Docker, setting up FFmpeg, configuring a Spring Boot backend with streaming services, and using a simple HTML player with flv.js to push and play local video streams over RTMP.

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

1. Environment Preparation

Pull the ZLMediaKit Docker image and start the container:

# 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

Typical config.ini settings for HLS:

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

Install FFmpeg from https://www.gyan.dev/ffmpeg/builds/ and add its bin directory to the system PATH so that ffmpeg is callable from the command line.

2. Spring Boot Backend Implementation

2.1 Add Dependency

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

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

2.3 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 stream processes
    private final Map<String, DefaultExecutor> streamProcesses = new ConcurrentHashMap<>();
    // Flag for manual stop
    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("Video file not found: {}", 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("Stream finished, streamKey: {}, exitValue: {}", streamKey, exitValue);
                    streamProcesses.remove(streamKey);
                }
                @Override
                public void onProcessFailed(ExecuteException e) {
                    boolean isManualStop = manualStopFlags.remove(streamKey);
                    if (isManualStop) {
                        log.info("Stream manually stopped, streamKey: {}", streamKey);
                    } else {
                        log.error("Stream failed, streamKey: {}, error: {}", streamKey, e.getMessage());
                    }
                    streamProcesses.remove(streamKey);
                }
            });
            streamProcesses.put(streamKey, executor);
            log.info("Started streaming, streamKey: {}, rtmpUrl: {}", streamKey, rtmpUrl);
            return true;
        } catch (Exception e) {
            log.error("Failed to start stream", e);
            return false;
        }
    }

    private CommandLine getCommandLine(String videoPath, String rtmpUrl) {
        CommandLine cmdLine = new CommandLine(streamConfig.getFfmpegPath());
        cmdLine.addArgument("-re"); // read input 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("Process has no watchdog, cannot force stop, streamKey: {}", streamKey);
                }
                streamProcesses.remove(streamKey);
                log.info("Stopped stream successfully, streamKey: {}", streamKey);
                return true;
            }
            return false;
        } catch (Exception e) {
            log.error("Failed to stop stream", e);
            return false;
        }
    }

    /** Get playback URL */
    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;
        };
    }

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

2.4 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

3. Usage Instructions

3.1 Streaming Flow

Start the ZLMediaKit service (Docker container).

Upload a local video file to the server.

Call the Spring Boot streaming API with the video path and a stream key.

The service builds an FFmpeg command and pushes the stream to ZLMediaKit via RTMP.

3.2 Playback Flow

Obtain the playback URL (HTTP‑FLV or HLS) from getPlayUrl.

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

Example FFmpeg command generated 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

Simple HTML player using flv.js:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>FLV Live Player</title>
  <style>
    .player-container { max-width:800px; margin:auto; background:white; padding:20px; border-radius:8px; box-shadow:0 2px 10px rgba(0,0,0,0.1); }
    video { width:100%; height:450px; background:#000; border-radius:4px; }
    .controls button { margin:5px; padding:10px 20px; border:none; border-radius:4px; background:#007bff; color:white; cursor:pointer; }
    .controls button:disabled { background:#ccc; cursor:not-allowed; }
    .status { margin-top:10px; padding:10px; border-radius:4px; text-align:center; }
    .status.success { background:#d4edda; color:#155724; }
    .status.error { background:#f8d7da; color:#721c24; }
    .status.info { background:#d1ecf1; color:#0c5460; }
  </style>
</head>
<body>
  <div class="player-container">
    <h1>FLV Live Player</h1>
    <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 src="https://cdn.jsdelivr.net/npm/[email protected]/dist/flv.min.js"></script>
  <script>
    let flvPlayer = null;
    const videoEl = 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(msg, type) {
      statusDiv.textContent = msg;
      statusDiv.className = `status ${type}`;
    }
    function setButtons(play, pause, stop) {
      playBtn.disabled = !play;
      pauseBtn.disabled = !pause;
      stopBtn.disabled = !stop;
    }
    if (!flvjs.isSupported()) {
      updateStatus('Browser does not support FLV playback', 'error');
      playBtn.disabled = true;
    }
    playBtn.addEventListener('click', () => {
      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(videoEl);
      flvPlayer.load();
      flvPlayer.on(flvjs.Events.ERROR, (type, detail, info) => {
        console.error('FLV error', type, detail, info);
        updateStatus(`Play error: ${detail}`, 'error');
      });
      flvPlayer.on(flvjs.Events.LOADING_COMPLETE, () => {
        updateStatus('Stream loaded', 'success');
      });
      videoEl.play().then(() => {
        updateStatus('Playing live stream', 'success');
        setButtons(false, true, true);
      }).catch(err => {
        console.error('Play failed', err);
        updateStatus('Play failed: ' + err.message, 'error');
      });
    });
    pauseBtn.addEventListener('click', () => {
      if (videoEl && !videoEl.paused) {
        videoEl.pause();
        updateStatus('Paused', 'info');
        setButtons(true, false, true);
      }
    });
    stopBtn.addEventListener('click', () => {
      if (flvPlayer) {
        flvPlayer.pause();
        flvPlayer.unload();
        flvPlayer.destroy();
        flvPlayer = null;
      }
      videoEl.src = '';
      videoEl.load();
      updateStatus('Stopped', 'info');
      setButtons(true, false, false);
    });
    muteBtn.addEventListener('click', () => {
      videoEl.muted = !videoEl.muted;
      muteBtn.textContent = videoEl.muted ? 'Unmute' : 'Mute';
      updateStatus(videoEl.muted ? 'Muted' : 'Unmuted', 'info');
    });
    videoEl.addEventListener('error', () => {
      updateStatus('Video playback error', 'error');
      setButtons(true, false, false);
    });
  </script>
</body>
</html>
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

spring-bootFFmpegvideo streamingrtmpflv.jszlmediakit
Java Architect Handbook
Written by

Java Architect Handbook

Focused on Java interview questions and practical article sharing, covering algorithms, databases, Spring Boot, microservices, high concurrency, JVM, Docker containers, and ELK-related knowledge. Looking forward to progressing together with you.

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.