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

This tutorial walks through setting up ZLMediaKit via Docker, installing and configuring FFmpeg, creating a Spring Boot backend with stream configuration and service classes, and explains how to push and play live streams using RTMP, HTTP‑FLV, and HLS protocols.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Build a Live Streaming Service with ZLMediaKit, FFmpeg, and Spring Boot

1. Environment Preparation

1.1 ZLMediaKit Installation

Download and run the ZLMediaKit Docker image:

# 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

Configuration file (config.ini) example:

[hls]
 broadcastRecordTs=0
 deleteDelaySec=300
 fileBufSize=65536
 filePath=./www
 segDur=2
 segNum=1000
 segRetain=9999

1.2 FFmpeg Installation

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

2. Spring Boot Backend Implementation

2.1 Add Dependencies

<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;
    /** FFmpeg executable path */
    private String ffmpegPath;
    /** Video storage path */
    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.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;
            }
            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("推流完成, 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) {
        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 Push Flow

Start the ZLMediaKit service.

Upload video files to the server.

Call the push API with the video path and a stream key.

Spring Boot executes FFmpeg to push the stream to ZLMediaKit.

3.2 Play Flow

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

Use a compatible player to watch live or on‑demand streams.

Example FFmpeg command for manual testing:

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

Frontend playback can be implemented with flv.js; the core logic involves creating a player, attaching it to a video element, and handling play/pause/stop events.

Dockerlive streamingSpring BootffmpegZLMediaKit
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

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.