How to Build a Live Streaming Service with ZLMediaKit, FFmpeg, and Spring Boot

This guide walks through setting up ZLMediaKit in Docker, configuring FFmpeg, creating a Spring Boot backend with streaming APIs, and building a front‑end FLV player, providing a complete end‑to‑end solution for live video streaming and playback.

Architecture Digest
Architecture Digest
Architecture Digest
How to Build a Live Streaming Service with ZLMediaKit, FFmpeg, and Spring Boot

1. Environment Preparation

1.1 ZLMediaKit installation

Pull the Docker image and run the container with the required ports and configuration 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

Example config.ini settings:

[hls]
broadcastRecordTs=0
deleteDelaySec=300   # keep video 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

1.2 FFmpeg installation

Download a build from https://www.gyan.dev/ffmpeg/builds/ and add the bin directory to the system PATH.

2. Spring Boot Backend Implementation

2.1 Add Maven dependency

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

2.2 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;
    /** FFmpeg executable path */
    private String ffmpegPath;
    /** Video storage path */
    private String videoPath;
}

2.3 Streaming service

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.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("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);
            executor.setStreamHandler(new PumpStreamHandler(new java.io.ByteArrayOutputStream()));
            executor.execute(cmdLine, new ExecuteResultHandler() {
                @Override
                public void onProcessComplete(int exitValue) {
                    log.info("Stream finished, key: {}, exit: {}", streamKey, exitValue);
                    streamProcesses.remove(streamKey);
                }
                @Override
                public void onProcessFailed(ExecuteException e) {
                    boolean manual = manualStopFlags.remove(streamKey);
                    if (manual) {
                        log.info("Stream manually stopped, key: {}", streamKey);
                    } else {
                        log.error("Stream failed, key: {}, error: {}", streamKey, e.getMessage());
                    }
                    streamProcesses.remove(streamKey);
                }
            });
            streamProcesses.put(streamKey, executor);
            log.info("Started streaming, key: {}, 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");
        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("No watchdog, cannot force stop, key: {}", streamKey);
                }
                streamProcesses.remove(streamKey);
                log.info("Stopped stream, key: {}", streamKey);
                return true;
            }
            return false;
        } catch (Exception e) {
            log.error("Failed to stop stream", e);
            return false;
        }
    }

    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;
        }
    }

    public boolean isStreaming(String streamKey) {
        return streamProcesses.containsKey(streamKey);
    }
}

2.4 Application configuration

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 workflow

Start ZLMediaKit service.

Upload video file to the server.

Call the streaming API with video path and stream key.

Spring Boot runs FFmpeg to push the stream to ZLMediaKit.

3.2 Playback workflow

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

Play live or on‑demand using a front‑end player.

Example FFmpeg command for pushing a local file:

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.3 Front‑end FLV player

The following HTML page uses flv.js to play the live stream:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FLV Live Player</title>
</head>
<body>
<div class="player-container">
<h1>FLV Live Player</h1>
<video id="videoElement" controls muted></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 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('Your browser does not support FLV playback.', 'error');
    playBtn.disabled = true;
}
playBtn.addEventListener('click', function () {
    if (flvPlayer) {
        flvPlayer.destroy();
    }
    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, (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');
    });
    videoElement.play().then(() => {
        updateStatus('Playing live stream', 'success');
        updateButtons(false, true, true);
    }).catch(error => {
        console.error('Play failed:', error);
        updateStatus('Play failed: ' + error.message, 'error');
    });
});
pauseBtn.addEventListener('click', function () {
    if (videoElement && !videoElement.paused) {
        videoElement.pause();
        updateStatus('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('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...', 'info'));
videoElement.addEventListener('canplay', () => updateStatus('Video ready', 'success'));
videoElement.addEventListener('playing', () => {
    updateStatus('Playing', 'success');
    updateButtons(false, true, true);
});
videoElement.addEventListener('pause', () => {
    updateStatus('Paused', 'info');
    updateButtons(true, false, true);
});
videoElement.addEventListener('error', () => {
    updateStatus('Playback error', 'error');
    updateButtons(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.

backendlive streamingSpring Bootffmpegvideo streamingZLMediaKit
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.