使用Node搭建一个直播服务器,实时直播当前桌面

发布于:2025-07-17 ⋅ 阅读:(12) ⋅ 点赞:(0)

初始条件

  • Node20+

    image-20250715173725158

  • 需要本机安装好ffmpeg,并且版本7.0+,可以查看我写的这个文章来安装

    image-20250715173949016

初始化项目

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

推流成功后的样子

image-20250715152749734

此时控制台输出

image-20250715152842661

使用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>

实现效果

image-20250715165423675

案例:实现一个直播当前电脑桌面的功能

编写后端代码

安装依赖

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>

效果展示

gifimg7


网站公告

今日签到

点亮在社区的每一天
去签到