如何搭建按需生成HLS流的直播转码分发系统?
多源直播转码系统实现方案
一、服务器环境准备(Xubuntu)
先完成基础依赖安装:
- Nginx(带RTMP模块):
若系统默认包无RTMP模块,可通过源码编译安装:
也可通过PPA安装预编译版本: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 installsudo apt-add-repository ppa:nginx/stable sudo apt update && sudo apt install nginx - FFmpeg:
验证安装:sudo apt install ffmpegffmpeg -version - Node.js与npm:
或用nvm安装稳定版:sudo apt install nodejs npmcurl -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; # 禁用缓存保证实时性 } } }
完成后:
- 创建HLS文件存储目录:
sudo mkdir -p /var/www/hls - 设置目录权限:
sudo chown -R www-data:www-data /var/www/hls - 重启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。
五、核心逻辑优化建议
- 流有效性检测:启动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流不存在' }); } - 配置持久化:将配置存储到本地JSON文件或SQLite数据库,避免服务重启后丢失。
- 进程异常重启:监听FFmpeg进程退出事件,若为异常退出(非手动停止),可自动重启转码进程。
- 资源限制:通过
cpulimit工具限制FFmpeg进程的CPU使用率,避免单流占用过多资源影响其他流。
六、运维监控
- 日志收集:将Nginx日志、Node服务日志、FFmpeg输出统一收集到指定目录,定期清理。
- 资源监控:用
htop、prometheus+grafana监控服务器CPU、内存、磁盘IO状态。 - 磁盘清理:定期清理超过7天未使用的HLS流目录,避免磁盘空间耗尽。
内容的提问来源于stack exchange,提问作者Roberto
相关产品推荐
相关产品推荐

