实战:从零构建一个支持屏幕录制与片段合并的视频管理系统 (Node.js + FFmpeg)

1. 背景 (Background)

作为开发者,我们每天都会浏览大量的技术教程(YouTube, Bilibili)。收藏夹往往乱作一团,而且很多时候,我们只需要长视频中的某几个关键片段

单纯的书签管理已经无法满足需求,我想要一个工具:

  1. 美观管理:像 Netflix 一样展示我的视频收藏。
  2. 随手记录:看到重点时,能直接录制屏幕片段。
  3. 自动归档:录制的片段自动关联到当前视频条目下,不丢失。
  4. 整理输出:能把零散的片段合并成一个完整的笔记视频。

于是,VideoHub Pro 诞生了。

C:\myApp\myvideo-manager

2. 目标 (Goal)

我们需要构建一个 B/S 架构 的全栈应用,无需繁重的数据库安装,开箱即用。

  • 前端:原生 HTML/CSS/JS(极致轻量),实现卡片式展示、剪贴板截图粘贴、Web 录屏 API 调用。
  • 后端:Node.js (Express),负责数据存储(JSON)、文件上传(Multer)以及视频处理(FFmpeg)。
  • 核心痛点解决 :解决 Web 端录屏的持久化存储,以及多段视频的后端合并下载。

3. 方法 (Method)

  • 技术栈

    • Server: Node.js + Express
    • Media Processing : fluent-ffmpeg (调用系统安装的 FFmpeg)
    • Uploads : multer
    • Frontend: HTML5, CSS3 (Grid + Glassmorphism), Native JavaScript (ES6+)
    • Database : 本地 data.json 文件 (模拟 NoSQL)
  • 数据模型设计

    一个视频对象(Video)包含基本信息和一个片段(Clips)数组:

    json 复制代码
    {
      "id": "uuid-...",
      "title": "Node.js 教程",
      "url": "https://...",
      "thumbnail": "base64...",
      "clips": [
        { "filename": "17000.webm", "createdAt": "...", "size": 1024 }
      ]
    }

4. 过程与核心代码解析 (Process)

4.1 前端交互与视觉设计

我们采用了 深色模式 (Dark Mode)毛玻璃 (Glassmorphism) 风格,利用 CSS Grid 实现响应式的卡片布局。

关键点在于粘贴上传缩略图。为了提升体验,我们监听了粘贴事件:

javascript 复制代码
// 监听粘贴事件,自动读取剪贴板中的图片流并转为 Base64
document.getElementById('paste-area').addEventListener('paste', e => {
    const items = e.clipboardData.items;
    for(let item of items) {
        if(item.type.startsWith('image')) {
            const blob = item.getAsFile();
            const reader = new FileReader();
            reader.onload = ev => {
                // 直接展示并赋值给隐藏域
                document.getElementById('v-thumb').value = ev.target.result; 
                // ...预览逻辑
            };
            reader.readAsDataURL(blob);
        }
    }
});

4.2 核心难点一:Web 录屏与自动上传

这是本项目的核心功能。传统的 Web 录屏往往只保存在浏览器内存中,刷新即丢失。我们的改进方案是:录制停止 -> 自动上传 -> 服务器持久化

前端实现 (index.html):

使用 navigator.mediaDevices.getDisplayMedia 获取屏幕流,通过 MediaRecorder 录制。

javascript 复制代码
async function startRec() {
    // 1. 唤起浏览器原生屏幕分享弹窗
    const stream = await navigator.mediaDevices.getDisplayMedia({ video:true, audio:true });
    
    // 2. 创建录制器,指定 MIME 类型
    const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp9' });
    let chunks = []; // 临时存储二进制块

    mediaRecorder.ondataavailable = e => { if(e.data.size > 0) chunks.push(e.data); };
    
    // 3. 监听停止事件
    mediaRecorder.onstop = async () => {
        // 将 chunks 转为 Blob 对象
        const blob = new Blob(chunks, { type: 'video/webm' });
        
        // 4. 构建 FormData,准备上传
        const fd = new FormData();
        fd.append('clip', blob, `rec_${Date.now()}.webm`);
        fd.append('videoId', currentVideoId); // 关键:带上当前视频的 ID

        // 5. 立即上传服务器
        await fetch(`${API}/upload-clip`, { method: 'POST', body: fd });
        
        // 停止流,释放资源
        stream.getTracks().forEach(t => t.stop());
        refreshClipsPanel(); // 刷新 UI
    };

    mediaRecorder.start();
}

后端处理 (server.js):

后端接收文件,保存到 uploads 目录,并更新 data.json 中的对应记录。

javascript 复制代码
app.post('/api/upload-clip', upload.single('clip'), (req, res) => {
    const { videoId } = req.body;
    // ...读取 JSON 数据
    const idx = videos.findIndex(v => v.id === videoId);
    
    if (idx !== -1) {
        // 构建片段元数据对象
        const clipInfo = {
            filename: req.file.filename, // Multer 生成的物理文件名
            originalName: req.file.originalname,
            size: req.file.size,
            createdAt: new Date()
        };
        // 关联数据:将片段信息 push 到该视频的 clips 数组中
        videos[idx].clips.push(clipInfo);
        writeDB(videos); // 写入磁盘
        res.json({ success: true });
    }
});

分析:这种"ID关联法"使得录屏文件不再是孤立的,而是永远从属于某个具体的视频条目。


4.3 核心难点二:多片段合并 (FFmpeg)

当用户录制了片段 A、片段 B、片段 C 后,可能只想把 A 和 C 合并下载。

策略变化

早期版本是前端把 Blob 传给后端合并,这很浪费流量。现在的策略是:文件已经在服务器上了,前端只需要传"文件名列表",后端在本地进行合并

前端逻辑

用户勾选复选框,获取文件名数组:

javascript 复制代码
const cbs = document.querySelectorAll('.clip-cb:checked');
const filenames = Array.from(cbs).map(cb => cb.value); // ['file1.webm', 'file3.webm']

await fetch(`${API}/merge-files`, {
    body: JSON.stringify({ filenames }) // 只发文件名,极快
});

后端逻辑 (server.js):

使用 fluent-ffmpeg 链式调用。

javascript 复制代码
app.post('/api/merge-files', (req, res) => {
    const { filenames } = req.body;
    const outputFilename = `merged-${Date.now()}.mp4`;
    const outputPath = path.join(UPLOAD_DIR, outputFilename);
    
    const command = ffmpeg();

    // 1. 遍历添加输入源 (Input)
    filenames.forEach(name => {
        // 拼接服务器的绝对路径
        command.input(path.join(UPLOAD_DIR, name));
    });

    // 2. 执行合并 (Merge)
    command
        .on('error', (err) => res.status(500).send('Merge failed'))
        .on('end', () => {
            // 3. 返回合并后的文件 URL
            res.json({ url: `/uploads/${outputFilename}` });
        })
        .mergeToFile(outputPath, UPLOAD_DIR); // 输出
});

分析:这个设计将繁重的视频处理工作全部交给了服务器(Node.js 调用底层 FFmpeg),前端只需要发送轻量级的指令,体验非常流畅。


5. 结果展示 (Result)

最终,我们得到了一个功能闭环的系统:

  1. 卡片墙:首页展示所有视频,缩略图清晰,支持分类过滤。
  2. 播放详情页
    • 左侧:智能解析 URL(YouTube/Bilibili 自动嵌入 iframe),下方有"录制"按钮。
    • 右侧:显示所有关联的录屏片段历史。
  3. 工作流
    • 点击"录制" -> 选择区域 -> 录制结束 -> 自动出现在右侧列表
    • 在右侧列表中勾选第1条和第3条 -> 点击"合并下载" -> 浏览器弹出合并后的 MP4 下载。
相关推荐
这儿有个昵称2 小时前
Java面试场景:从音视频到微服务的技术深挖
java·spring boot·spring cloud·微服务·面试·kafka·音视频
shughui2 小时前
2026最新版Node.js下载安装、版本选择 及 环境配置教程(详细图文附安装包)
node.js
小李子呢02112 小时前
Node.js
开发语言·前端·学习·node.js
心.c2 小时前
文件上传 - 入门篇
前端·javascript·vue.js·node.js·js
winfredzhang3 小时前
自动化视频制作:深入解析 FFmpeg 图片转视频脚本
ffmpeg·自动化·音视频·命令行·bat·图片2视频
xiaoxue..3 小时前
Nest.js 框架 企业级开发通关手册
面试·typescript·node.js·开发框架·nest.js
s09071363 小时前
FPGA视频编码器:H.264/H.265实现核心技术解析
图像处理·算法·fpga开发·音视频·h.264
月月玩代码16 小时前
抖音视频无水印下载工具,电脑端工具,下载非常方便,使用方法简单,一看就会!
音视频·视频下载·抖音无水印视频下载工具·视频手动下载·视频1080p下载
行业探路者19 小时前
健康宣教二维码是什么?主要有哪些创新优势?
人工智能·学习·音视频·二维码·产品介绍