初始条件
Node20+
需要本机安装好ffmpeg,并且版本7.0+,可以查看我写的这个文章来安装
初始化项目
mkdir node-live
cd node-live
npm init -y
安装依赖
npm install node-media-server
Node-Media-Server 是一款基于 Nodejs 开发的高性能/低延迟/开源直播服务器
编写代码
新建 main.js
import NodeMediaServer from "node-media-server";
const server = new NodeMediaServer({
bind: "192.168.124.144",
// 推流
rtmp: {
port: 1935,
chunk_size: 60000, //传输大小 60kb
gop_cache: true, //是否缓存
ping: 60, //心跳
ping_timeout: 30, //心跳超时
},
// 拉流
http: {
port: 8000,
allow_origin: '*',
},
})
server.run();
console.log('Server is running on port 8000');
启动
使用ffmpeg推流
推送本地视频文件到 NMS
假设你有一个本地视频文件 test.mp4,可以用如下命令推流:
ffmpeg -re -i test.mp4 -c copy -f flv rtmp://192.168.124.144:1935/live/stream
-re:以实时速度读取文件(用于模拟直播)
-i test.mp4:输入文件
-c copy:音视频流直接拷贝,不重新编码(也可以用 -c:v libx264 -c:a aac 重新编码)
-f flv:输出格式为 FLV(RTMP 需要)
rtmp://192.168.124.144/live/stream:推流地址,/live/stream 是自定义的应用名和流名
推送摄像头实时画面
假设你用的是 Windows,摄像头设备名一般为 video=Integrated Camera(可用 ffmpeg -list_devices true -f dshow -i dummy 查看设备名):
ffmpeg -f dshow -i video="Integrated Camera" -vcodec libx264 -preset veryfast -tune zerolatency -f flv rtmp://192.168.124.144:1935/live/stream
推送桌面画面
ffmpeg -f gdigrab -i desktop -vcodec libx264 -preset veryfast -tune zerolatency -f flv rtmp://192.168.124.144:1935/live/stream
推流成功后的样子
此时控制台输出
使用flvjs播放直播视频
安装
npm install --save mpegts.js
mpegts 是用 TypeScript 和 JavaScript 编写的 HTML5 MPEG2-TS 流播放器。
mpegts.js 针对低延迟实时流播放进行了优化,例如 DVB/ISDB 电视或监控摄像头。
本项目基于flv.js
实现源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./node_modules/mpegts.js/dist/mpegts.js"></script>
</head>
<body>
<button id="playButton">播放直播</button>
<video id="videoElement" style="width: 100%; height: 100%;" preload="auto" autoplay controls></video>
<script>
document.getElementById('playButton').addEventListener('click', function () {
if (mpegts.getFeatureList().mseLivePlayback) {
var videoElement = document.getElementById('videoElement');
var player = mpegts.createPlayer({
type: 'flv', // could also be mpegts, m2ts, flv
isLive: true,
url: 'http://192.168.124.144:8000/live/stream.flv',
hasAudio: false,
});
player.attachMediaElement(videoElement);
player.load();
player.play();
}
});
</script>
</body>
</html>
实现效果
案例:实现一个直播当前电脑桌面的功能
编写后端代码
安装依赖
npm install express
新建 server.js
import express from 'express';
import { spawn } from 'child_process';
import { randomUUID } from 'crypto';
const app = express();
const port = 3100;
const liveAddress = '192.168.124.144'; // 直播服务器的地址,这里就是服务器的ip地址
const livePort = 1935; // 推流端口
const liveFlvPort = 8000; // 拉流端口
// 允许跨域
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
next();
});
app.use(express.static('.'));
// 保存每个直播的 ffmpeg 进程
const liveProcesses = {};
app.get('/start-live', (req, res) => {
const streamKey = randomUUID();
const rtmpUrl = `rtmp://${liveAddress}:${livePort}/live/${streamKey}`;
const flvUrl = `http://${liveAddress}:${liveFlvPort}/live/${streamKey}.flv`;
// 下面的功能是使用ffmpeg采集桌面,并进行推流。可以根据自己的需求修改参数
const ffmpegArgs = [
'-f', 'gdigrab', '-i', 'desktop',
'-f', 'lavfi', '-i', 'anullsrc',
'-vcodec', 'libx264', '-pix_fmt', 'yuv420p',
'-preset', 'veryfast', '-tune', 'zerolatency',
'-acodec', 'aac', '-ar', '44100', '-ac', '2',
'-f', 'flv', rtmpUrl
];
// 用 spawn 启动 ffmpeg
const ffmpegProcess = spawn('ffmpeg', ffmpegArgs, { stdio: 'ignore' });
liveProcesses[streamKey] = ffmpegProcess;
res.json({
streamKey,
rtmpUrl,
flvUrl
});
});
// 关闭直播接口
app.get('/stop-live', (req, res) => {
const { streamKey } = req.query;
const proc = liveProcesses[streamKey];
if (proc) {
proc.kill();
delete liveProcesses[streamKey];
res.json({ success: true, message: '直播已关闭' });
} else {
res.json({ success: false, message: '未找到对应的直播进程' });
}
});
app.listen(port, () => {
console.log(`Live control server running at http://localhost:${port}`);
});
前提:要先把上面的 main.js 启动起来
启动后端服务
node server.js
编写前端页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>一键开播 · Geek Mode</title>
<!-- 本地 flv.js;如用 CDN 可换成 https://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js -->
<script src="./node_modules/flv.js/dist/flv.min.js"></script>
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--primary: #39ff14;
--danger: #ff073a;
--text: #c9d1d9;
--font: 'Fira Code', 'Consolas', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.card {
background: var(--card);
border: 1px solid #30363d;
border-radius: 12px;
padding: 32px 40px;
width: 100%;
max-width: 720px;
box-shadow: 0 0 20px rgba(57, 255, 20, .15);
}
h1 {
text-align: center;
font-size: 1.4rem;
margin-bottom: 26px;
color: var(--primary);
letter-spacing: .1em;
}
.code-block {
font-size: .9rem;
background: #0d1117;
padding: 12px 16px;
border-left: 3px solid var(--primary);
border-radius: 4px;
margin: 12px 0;
word-break: break-all;
overflow-x: auto;
color: #adbac7;
}
#video {
display: none;
width: 100%;
aspect-ratio: 16/9;
background: #000;
border-radius: 6px;
margin-bottom: 20px;
box-shadow: 0 0 12px rgba(57, 255, 20, .25);
}
.btn-row {
display: flex;
gap: 14px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
font-family: var(--font);
font-size: 1rem;
padding: 12px 28px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: .25s;
color: #000;
font-weight: 600;
letter-spacing: .05em;
}
.btn.primary {
background: var(--primary);
box-shadow: 0 0 8px var(--primary);
}
.btn.primary:hover {
filter: brightness(1.2)
}
.btn.danger {
background: var(--danger);
box-shadow: 0 0 8px var(--danger);
}
.btn.danger:hover {
filter: brightness(1.2)
}
@media(max-width:600px) {
.card {
padding: 20px
}
.btn {
font-size: .9rem;
padding: 10px 20px
}
}
</style>
</head>
<body>
<div class="card">
<h1>一键开播 · Geek Mode</h1>
<video id="video" controls muted></video>
<div id="info"></div>
<div class="btn-row">
<button id="startBtn" class="btn primary">开直播</button>
<button id="watchBtn" class="btn primary" style="display:none;">查看直播</button>
<button id="closeBtn" class="btn danger" style="display:none;">关闭直播</button>
</div>
</div>
<script>
/* 配置区:把下面两个地址换成你的真实接口 */
const API_START = 'http://192.168.124.144:3100/start-live';
const API_STOP = 'http://192.168.124.144:3100/stop-live?streamKey=';
let flvUrl = '';
const $ = sel => document.querySelector(sel);
const info = $('#info');
const video = $('#video');
$('#startBtn').onclick = async () => {
info.innerHTML = '<span style="color:#39ff14">正在启动推流服务…</span>';
try {
const res = await fetch(API_START);
if (!res.ok) throw new Error('网络异常');
const data = await res.json();
flvUrl = data.flvUrl;
window._streamKey = data.streamKey;
info.innerHTML = `
<div class="code-block">推流地址 (RTMP):<br>${data.rtmpUrl}</div>
<div class="code-block">播放地址 (FLV):<br>${data.flvUrl}</div>
`;
$('#watchBtn').style.display = '';
$('#closeBtn').style.display = '';
} catch (err) {
info.innerHTML = `<span style="color:#ff073a">启动失败:${err.message}</span>`;
}
};
$('#watchBtn').onclick = () => {
if (!flvjs.isSupported()) {
alert('当前浏览器不支持 flv.js'); return;
}
video.style.display = 'block';
const player = flvjs.createPlayer({ type: 'flv', url: flvUrl });
player.attachMediaElement(video);
player.load();
player.play();
window._flvPlayer = player;
};
$('#closeBtn').onclick = async () => {
if (!window._streamKey) return;
try {
await fetch(API_STOP + window._streamKey);
info.innerHTML = '<span style="color:#39ff14">直播已关闭</span>';
$('#closeBtn').style.display = 'none';
$('#watchBtn').style.display = 'none';
if (window._flvPlayer) {
window._flvPlayer.unload();
window._flvPlayer.detachMediaElement();
window._flvPlayer.destroy();
window._flvPlayer = null;
}
} catch (err) {
info.innerHTML = `<span style="color:#ff073a">关闭失败:${err.message}</span>`;
}
};
</script>
</body>
</html>