尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

视频课程创作应用全链路:录制、上传、转码与播放实践

视频课程创作应用全链路:录制、上传、转码与播放实践 视频课程创作工具最近被频繁讨论YC S24 批次里的 Keet 把目标定得很直接做一个能让用户针对任何话题创建视频课程的应用。这类产品的价值不在录屏本身而在于把录制、剪辑、上传、转码、发布、播放这条链路压缩到普通讲师也能顺畅操作的程度。从工程角度看这条链路涉及浏览器多媒体能力、对象存储、转码服务、CDN 分发和播放器兼容性每一环都有不少容易踩坑的细节。这篇文章不评价 Keet 的具体产品而是从“如果要自己构建一款视频课程创作应用技术方案该怎么设计”的角度把录制、上传、转码、存储、播放、排错这条完整链路讲清楚。适合独立开发者、小团队技术负责人以及刚接触音视频方向的前后端工程师参考。1. 先拆解视频课程创作应用的核心工作流1.1 从录制到播放一条完整内容链路一款课程创作 App 的输入是讲师打开摄像头、共享屏幕、说话输出是学员在手机或浏览器里点开视频并流畅观看。中间每一步都会影响最终体验采集端要处理摄像头、麦克风、屏幕共享、课件画面叠加。上传端要处理大文件、弱网、断点续传、文件校验。服务端要做视频转码、切片、封面生成、字幕处理。分发端要做 CDN 加速、防盗链、鉴权、多码率适配。如果把 Keet 这类工具拆到最小闭环至少需要四件事录制、上传、转码、播放。后续所有章节都围绕这条主线展开。1.2 功能模块与技术要求对照表模块核心职责关键技术点常见产出录制采集音视频getUserMedia / MediaRecorder / 屏幕捕获原始视频文件常见为 WebM、MP4上传把文件可靠传到服务端分片、断点续传、校验、签名存储在对象存储中的源文件转码统一编码格式并生成多码率切片ffmpeg、H.264、AAC、HLS多码率 m3u8 与 ts 切片播放在端上流畅播放HLS/DASH、CDN、播放器兼容学员看到的可拖动视频管理课程、章节、权限、统计数据库模型、权限系统、数据报表课程列表、学习进度、观看统计这张表后面每一节都会对应展开。理解这条链路之后遇到视频课程类项目时就不会只盯着“能不能录”这一个点而是能从全链路判断瓶颈在哪里。1.3 课程视频与普通短视频的技术差异写代码之前先想清楚一个区别课程视频不是短视频。短视频通常几十秒到几分钟上传后平台统一处理课程视频动辄十几分钟到数小时播放场景也更复杂。这带来三个技术差异录制时长长源文件体积大上传不能指望一次 POST 完成。内容含代码、PPT、板书等细节编码质量要求高码率太低会看不清文字。学习行为决定了学员会反复拖动、暂停、回看HLS 切片的关键帧间隔和 CDN 预热策略都需要专门设计。这个对比不是要区分“谁更复杂”而是提醒技术方案要按课程场景的实际约束来设计。2. 搭一个最小可运行闭环录制、上传、转码、播放这一节的目标是在本地环境跑通“浏览器录制 - 上传到 Node 服务 - ffmpeg 转码 - 浏览器播放”的完整流程。代码以逻辑演示为主生产环境要替换成更健壮的实现。2.1 前置环境工具版本建议用途Node.js18运行上传与转码编排接口FFmpeg5.x 或 6.x视频转码与切片浏览器Chrome / Edge 最新版测试 MediaRecorder 录制本地对象存储模拟minio 或 s3rver模拟对象存储注意实际项目落地前要先确认 Node、FFmpeg、浏览器版本满足需求。不同版本对 HLS 切片命名和 WebM 编码行为有差异。2.2 浏览器端录制MediaRecorder 的最小实现浏览器录制课程画面最简单的方式是先封装一个录制会话内部处理getUserMedia和MediaRecorder的协作。先定义返回对象后续可以一直持有它。function createRecordingSession(videoElement) { let recorder null; let stream null; const chunks []; return { async start() { stream await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 } }, audio: true }); videoElement.srcObject stream; await videoElement.play(); recorder new MediaRecorder(stream, { mimeType: video/webm;codecsvp8,opus }); recorder.ondataavailable (event) { if (event.data event.data.size 0) { chunks.push(event.data); } }; recorder.start(1000); }, async stop() { if (!recorder) throw new Error(not started); recorder.stop(); await new Promise((resolve) { recorder.onstop () resolve(); }); stream.getTracks().forEach((track) track.stop()); return new Blob(chunks, { type: video/webm }); } }; }这里要解释几个点mimeType在多数 Chrome 里是video/webm;codecsvp8,opus如果直接保存成 MP4 容易失败。iPhone Safari 对MediaRecorder的支持有限移动端通常会改用原生摄像头录制或后端合流方案。recorder.start(1000)表示每秒触发一次dataavailable。这个参数影响内存占用和异常恢复粒度太大会导致长时间录制时数据恢复成本高太小会增加事件频率。stop()时先停止录制再停止摄像头和麦克风轨道避免页面右上角一直显示录音图标。2.3 服务端上传接口接收文件并返回标识上传接口先用 Express 配合multer做一个可运行版本文件落盘到uploads/目录。const express require(express); const multer require(multer); const crypto require(crypto); const fs require(fs); const app express(); fs.mkdirSync(uploads, { recursive: true }); const upload multer({ storage: multer.diskStorage({ destination: uploads/, filename: (req, file, cb) { const ext file.originalname.split(.).pop() || webm; cb(null, ${Date.now()}_${crypto.randomUUID()}.${ext}); } }), limits: { fileSize: 2 * 1024 * 1024 * 1024 } }); app.post(/api/upload, upload.single(video), (req, res) { if (!req.file) { return res.status(400).json({ error: no file }); } res.json({ videoId: req.file.filename, size: req.file.size, url: /files/${req.file.filename} }); }); app.use(/files, express.static(uploads)); app.listen(3000, () console.log(server ready on 3000));这个版本适合本地验证但要注意直接用originalname做路径拼接存在路径穿越风险上面用随机 UUID 重命名可以避免。2GB 的fileSize只是单请求上限生产环境应当使用分片上传避免大文件在弱网下反复重传。上传完成后应立即计算md5或sha256并在响应中返回方便后续转码任务校验文件完整性。2.4 服务端转码把 WebM 转成 HLS浏览器产出的 WebM 不适合直接用于多端播放。统一转成 H.264 AAC 的 HLS 切片是课程类应用最常用的做法。原因有三兼容性好、支持拖动、便于 CDN 分发。const { execFile } require(child_process); const path require(path); const fs require(fs); function transcodeToHls(videoId, inputPath, outputDir) { fs.mkdirSync(outputDir, { recursive: true }); const args [ -i, inputPath, -vf, scale1280:720, -pix_fmt, yuv420p, -c:v, libx264, -preset, veryfast, -g, 60, -sc_threshold, 0, -c:a, aac, -b:a, 128k, -ar, 44100, -hls_time, 6, -hls_playlist_type, vod, -hls_segment_filename, path.join(outputDir, segment_%03d.ts), path.join(outputDir, playlist.m3u8) ]; execFile(ffmpeg, args, (err, stdout, stderr) { if (err) { console.error(transcode failed, stderr); return; } console.log(transcode done, videoId); }); }关键参数解释参数含义错误配置表现-pix_fmt yuv420p输出标准像素格式不设置可能输出 yuv444部分播放器无法解码-preset veryfast编码速度和压缩率的取舍太慢则任务积压太快则码率偏高-g 60关键帧间隔 60 帧按 30fps 约 2 秒设置过大会导致拖动卡顿、切片切点不准-sc_threshold 0禁用场景切换自动插入关键帧不关闭会破坏固定 keyframe 间隔-hls_time 6每个切片约 6 秒过短则切片文件多、请求频繁过长则拖动延迟高-hls_playlist_type vod生成 VOD 点播列表不设置可能生成 event 类型列表行为不同2.5 播放端用 HLS.js 兼容现代浏览器原生video在 iOS Safari 上可以直接播放 HLS但 Chrome、Firefox 不行。前端一般引入hls.js做兼容。video idcourseVideo controls/video script srchttps://cdn.jsdelivr.net/npm/hls.js1/script script const video document.getElementById(courseVideo); const videoId your_video_id; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(/stream/${videoId}/playlist.m3u8); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () { video.play(); }); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src /stream/${videoId}/playlist.m3u8; } /script播放端最容易忽略的是 CORS。如果 HLS 切片和playlist.m3u8存放在 CDN 或对象存储且前端页面在不同域名下必须在响应头里配置Access-Control-Allow-Origin否则hls.js的 XHR 请求会被浏览器拦截。3. 决定视频质量和分发效率的关键选型最小闭环跑通之后问题会从“能不能跑”变成“好不好用”。视频质量、上传速度、播放清晰度、防盗链每一个都对应一组参数和选型决策。3.1 编码参数与多
返回列表