You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何搭建按需生成HLS流的直播转码分发系统?

多源直播转码系统实现方案

一、服务器环境准备(Xubuntu)

先完成基础依赖安装:

  • Nginx(带RTMP模块):
    若系统默认包无RTMP模块,可通过源码编译安装:
    sudo apt install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev openssl libssl-dev
    wget http://nginx.org/download/nginx-1.25.3.tar.gz
    wget https://github.com/arut/nginx-rtmp-module/archive/master.zip
    unzip master.zip && tar -zxvf nginx-1.25.3.tar.gz
    cd nginx-1.25.3
    ./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-master
    make && sudo make install
    
    也可通过PPA安装预编译版本:
    sudo apt-add-repository ppa:nginx/stable
    sudo apt update && sudo apt install nginx
    
  • FFmpeg:
    sudo apt install ffmpeg
    
    验证安装:ffmpeg -version
  • Node.js与npm:
    sudo apt install nodejs npm
    
    或用nvm安装稳定版:
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
    nvm install --lts
    

二、Nginx配置(RTMP接收+HLS分发)

编辑Nginx主配置文件(通常为/usr/local/nginx/conf/nginx.conf或/etc/nginx/nginx.conf),添加以下内容:

# RTMP服务配置
rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        # 接收外部推送的RTMP流
        application live {
            live on;
            record off;
        }
    }
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        listen 8088;
        server_name localhost;

        # HLS流访问端点
        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /var/www/hls;
            add_header Access-Control-Allow-Origin *; # 允许前端跨域
            add_header Cache-Control no-cache; # 禁用缓存保证实时性
        }
    }
}

完成后:

  1. 创建HLS文件存储目录:sudo mkdir -p /var/www/hls
  2. 设置目录权限:sudo chown -R www-data:www-data /var/www/hls
  3. 重启Nginx:sudo systemctl restart nginx(或/usr/local/nginx/sbin/nginx -s reload)

三、Node.js Express服务实现

1. 项目初始化

mkdir live-transcode-service && cd live-transcode-service
npm init -y
npm install express body-parser cors child_process fs-extra

2. 核心服务代码(server.js)

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { spawn } = require('child_process');
const fs = require('fs-extra');

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors()); // 处理跨域请求

// 存储流配置与FFmpeg进程实例(key为流名称)
const streamConfigs = {};
const ffmpegProcesses = {};
const hlsRootDir = '/var/www/hls';

// 保存流配置并启动转码
app.post('/save-config', async (req, res) => {
    const { streamName, videoBitrate, codec, fps, resolution } = req.body;

    // 基础参数校验
    if (!streamName || !videoBitrate || !codec || !fps || !resolution) {
        return res.status(400).json({ success: false, msg: '缺少必填配置项' });
    }

    // 停止该流已存在的转码进程
    if (ffmpegProcesses[streamName]) {
        ffmpegProcesses[streamName].kill('SIGTERM');
        delete ffmpegProcesses[streamName];
    }

    // 保存配置到内存
    streamConfigs[streamName] = { videoBitrate, codec, fps, resolution };

    // 创建流专属HLS目录
    const streamHlsPath = `${hlsRootDir}/${streamName}`;
    await fs.ensureDir(streamHlsPath);

    // 构建FFmpeg转码命令
    const ffmpegArgs = [
        '-i', `rtmp://localhost:1935/live/${streamName}`,
        '-c:v', codec,
        '-b:v', videoBitrate,
        '-r', fps,
        '-s', resolution,
        '-c:a', 'aac',
        '-b:a', '128k',
        '-f', 'hls',
        '-hls_time', '10', // 每个TS片段时长10秒
        '-hls_list_size', '6', // 播放列表保留最近6个片段
        '-hls_flags', 'delete_segments', // 自动删除过期片段
        '-hls_segment_filename', `${streamHlsPath}/segment_%03d.ts`,
        `${streamHlsPath}/index.m3u8`
    ];

    // 启动FFmpeg进程
    const ffmpegProc = spawn('ffmpeg', ffmpegArgs);

    // 日志输出(用于排查问题)
    ffmpegProc.stdout.on('data', data => console.log(`[${streamName}] ${data.toString().trim()}`));
    ffmpegProc.stderr.on('data', data => console.error(`[${streamName}] ${data.toString().trim()}`));

    // 进程退出处理
    ffmpegProc.on('exit', (code) => {
        console.log(`[${streamName}] 转码进程退出,代码: ${code}`);
        delete ffmpegProcesses[streamName];
    });

    ffmpegProcesses[streamName] = ffmpegProc;

    res.json({
        success: true,
        msg: '转码已启动',
        hlsPlayUrl: `http://${req.headers.host.split(':')[0]}:8088/hls/${streamName}/index.m3u8`
    });
});

// 停止指定流的转码
app.post('/stop-stream', (req, res) => {
    const { streamName } = req.body;
    if (ffmpegProcesses[streamName]) {
        ffmpegProcesses[streamName].kill();
        delete ffmpegProcesses[streamName];
        res.json({ success: true, msg: '转码已停止' });
    } else {
        res.json({ success: false, msg: '该流未在转码' });
    }
});

// 获取流配置
app.get('/get-config/:streamName', (req, res) => {
    const { streamName } = req.params;
    res.json({ success: true, config: streamConfigs[streamName] || null });
});

const PORT = 3000;
app.listen(PORT, () => {
    console.log(`转码服务运行在端口 ${PORT}`);
});

启动服务:node server.js,建议用pm2托管进程实现自动重启:

npm install -g pm2
pm2 start server.js --name live-transcode-service

四、前端交互实现

1. 配置表单与播放器示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>直播流配置</title>
</head>
<body>
    <div style="max-width: 800px; margin: 20px auto;">
        <h2>直播流配置</h2>
        <form id="configForm">
            <div style="margin: 10px 0;">
                <label>流名称:</label>
                <input type="text" name="streamName" placeholder="如stream1" required>
            </div>
            <div style="margin: 10px 0;">
                <label>视频码率:</label>
                <input type="text" name="videoBitrate" placeholder="如1500k" required>
            </div>
            <div style="margin: 10px 0;">
                <label>编码格式:</label>
                <select name="codec" required>
                    <option value="libx264">H.264</option>
                    <option value="libx265">H.265</option>
                </select>
            </div>
            <div style="margin: 10px 0;">
                <label>帧率:</label>
                <input type="number" name="fps" value="25" min="1" required>
            </div>
            <div style="margin: 10px 0;">
                <label>分辨率:</label>
                <input type="text" name="resolution" value="1280x720" required>
            </div>
            <button type="submit">保存配置并启动转码</button>
            <button type="button" id="stopBtn" style="margin-left: 10px; display: none;">停止转码</button>
        </form>

        <div id="playerWrap" style="margin-top: 20px; display: none;">
            <h3>直播播放</h3>
            <video id="hlsPlayer" controls width="100%"></video>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    <script>
        const form = document.getElementById('configForm');
        const stopBtn = document.getElementById('stopBtn');
        const playerWrap = document.getElementById('playerWrap');
        const player = document.getElementById('hlsPlayer');
        let currentStreamName = '';
        let hlsInstance = null;

        // 提交配置
        form.addEventListener('submit', async (e) => {
            e.preventDefault();
            const formData = new FormData(form);
            const config = Object.fromEntries(formData);
            currentStreamName = config.streamName;

            try {
                const res = await fetch('http://<服务器IP>:3000/save-config', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(config)
                });
                const data = await res.json();
                if (data.success) {
                    alert(data.msg);
                    stopBtn.style.display = 'inline-block';
                    // 加载HLS流
                    loadHlsStream(data.hlsPlayUrl);
                } else {
                    alert(data.msg);
                }
            } catch (err) {
                console.error(err);
                alert('配置提交失败,请检查服务状态');
            }
        });

        // 停止转码
        stopBtn.addEventListener('click', async () => {
            try {
                const res = await fetch('http://<服务器IP>:3000/stop-stream', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ streamName: currentStreamName })
                });
                const data = await res.json();
                alert(data.msg);
                stopBtn.style.display = 'none';
                playerWrap.style.display = 'none';
                if (hlsInstance) {
                    hlsInstance.destroy();
                    hlsInstance = null;
                }
            } catch (err) {
                console.error(err);
                alert('停止转码失败');
            }
        });

        // 加载HLS流
        function loadHlsStream(url) {
            playerWrap.style.display = 'block';
            if (Hls.isSupported()) {
                hlsInstance = new Hls();
                hlsInstance.loadSource(url);
                hlsInstance.attachMedia(player);
                hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => player.play());
            } else if (player.canPlayType('application/vnd.apple.mpegurl')) {
                player.src = url;
                player.play();
            }
        }
    </script>
</body>
</html>

注意:将<服务器IP>替换为实际服务器地址,若前端与服务端同源可省略IP。

五、核心逻辑优化建议

  1. 流有效性检测:启动FFmpeg前,用ffprobe检测RTMP流是否存在,避免进程空等:
    const { execSync } = require('child_process');
    try {
        execSync(`ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 rtmp://localhost:1935/live/${streamName}`);
    } catch (err) {
        return res.status(400).json({ success: false, msg: '指定RTMP流不存在' });
    }
    
  2. 配置持久化:将配置存储到本地JSON文件或SQLite数据库,避免服务重启后丢失。
  3. 进程异常重启:监听FFmpeg进程退出事件,若为异常退出(非手动停止),可自动重启转码进程。
  4. 资源限制:通过cpulimit工具限制FFmpeg进程的CPU使用率,避免单流占用过多资源影响其他流。

六、运维监控

  • 日志收集:将Nginx日志、Node服务日志、FFmpeg输出统一收集到指定目录,定期清理。
  • 资源监控:用htop、prometheus+grafana监控服务器CPU、内存、磁盘IO状态。
  • 磁盘清理:定期清理超过7天未使用的HLS流目录,避免磁盘空间耗尽。

内容的提问来源于stack exchange,提问作者Roberto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 03:59:53