Express + MongoDB 实现 VOD 视频点播
一、安装依赖
npm install express mongoose multer ffmpeg-static fluent-ffmpeg
express
:用于构建 Web 服务器。mongoose
:用于与 MongoDB 进行交互。multer
:用于处理文件上传。ffmpeg-static
:提供 FFmpeg 的静态二进制文件。fluent-ffmpeg
:用于视频处理。
二、数据库连接与模型定义
创建 models
目录并在其中创建 Video.js
文件
// models/Video.js
const mongoose = require("mongoose");
const videoSchema = new mongoose.Schema({
title: String,
description: String,
filePath: String,
thumbnailPath: String,
});
module.exports = mongoose.model("Video", videoSchema);
在项目根目录创建 app.js
文件并连接到 MongoDB
// app.js
const express = require("express");
const mongoose = require("mongoose");
const app = express();
mongoose.connect("mongodb://localhost:27017/vod_db", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on("error", console.error.bind(console, "MongoDB 连接错误:"));
db.once("open", () => {
console.log("MongoDB 连接成功");
});
const Video = require("./models/Video");
// 其他代码...
三、视频上传功能
使用 multer
处理视频上传,并使用 fluent-ffmpeg
生成视频缩略图:
// app.js
const multer = require("multer");
const ffmpegPath = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
const path = require("path");
const fs = require("fs");
// 配置 multer
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = "uploads/videos";
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname));
},
});
const upload = multer({ storage: storage });
// 处理视频上传
app.post("/upload", upload.single("video"), async (req, res) => {
try {
const { title, description } = req.body;
const filePath = req.file.path;
const thumbnailPath = `uploads/thumbnails/${Date.now()}.jpg`;
// 生成缩略图
await new Promise((resolve, reject) => {
ffmpeg()
.setFfmpegPath(ffmpegPath)
.input(filePath)
.on("end", resolve)
.on("error", reject)
.screenshots({
count: 1,
folder: "uploads/thumbnails",
size: "320x240",
filename: `${Date.now()}.jpg`,
});
});
// 保存视频信息到数据库
const newVideo = new Video({
title,
description,
filePath,
thumbnailPath,
});
await newVideo.save();
res.status(200).json({ message: "视频上传成功", video: newVideo });
} catch (error) {
res.status(500).json({ message: "视频上传失败", error: error.message });
}
});
四、视频列表展示
创建一个路由来获取视频列表
// app.js
app.get("/videos", async (req, res) => {
try {
const videos = await Video.find();
res.status(200).json(videos);
} catch (error) {
res.status(500).json({ message: "获取视频列表失败", error: error.message });
}
});
五、视频播放
创建一个路由来流式传输视频文件
// app.js
app.get("/videos/:id", async (req, res) => {
try {
const video = await Video.findById(req.params.id);
if (!video) {
return res.status(404).json({ message: "视频未找到" });
}
const filePath = video.filePath;
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = end - start + 1;
const file = fs.createReadStream(filePath, { start, end });
const head = {
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": "video/mp4",
};
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
"Content-Length": fileSize,
"Content-Type": "video/mp4",
};
res.writeHead(200, head);
fs.createReadStream(filePath).pipe(res);
}
} catch (error) {
res.status(500).json({ message: "视频播放失败", error: error.message });
}
});