1. 背景 (Background)
作为开发者,我们每天都会浏览大量的技术教程(YouTube, Bilibili)。收藏夹往往乱作一团,而且很多时候,我们只需要长视频中的某几个关键片段。
单纯的书签管理已经无法满足需求,我想要一个工具:
- 美观管理:像 Netflix 一样展示我的视频收藏。
- 随手记录:看到重点时,能直接录制屏幕片段。
- 自动归档:录制的片段自动关联到当前视频条目下,不丢失。
- 整理输出:能把零散的片段合并成一个完整的笔记视频。
于是,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)
最终,我们得到了一个功能闭环的系统:
- 卡片墙:首页展示所有视频,缩略图清晰,支持分类过滤。
- 播放详情页 :
- 左侧:智能解析 URL(YouTube/Bilibili 自动嵌入 iframe),下方有"录制"按钮。
- 右侧:显示所有关联的录屏片段历史。
- 工作流 :
- 点击"录制" -> 选择区域 -> 录制结束 -> 自动出现在右侧列表。
- 在右侧列表中勾选第1条和第3条 -> 点击"合并下载" -> 浏览器弹出合并后的 MP4 下载。