创建视频流平台是一个令人兴奋而又充满挑战的项目。最近,我将 Node.js 与用于后端处理的 FFmpeg 和用于现代前端的 Next.js 整合在一起,开始了这一旅程。在本文中,我将向您介绍完整的设置,包括视频处理、前端集成以及我如何解决沿途的各种问题。
使用的技术
1. Node.js 和 Express:
- 目的:处理视频上传、转换为 HLS 格式并提供视频文件。
- 主要特点:快速、可扩展、无阻塞 I/O。
2. FFmpeg:
- 目的:将视频文件转换为HLS(HTTP Live Streaming)格式。
- 主要功能:处理多媒体数据的开源工具。它可以转码、转换和流式传输音频和视频。
- HLS 和 M3U8:
- HLS:一种通过互联网传输视频的协议。它将视频分成小片段并以播放列表的形式提供。
- M3U8:HLS 用于组织视频片段和元数据的播放列表文件格式。
3. Multer:
- 目的:处理
multipart/form-data
中间件,用于上传文件。 - 主要特点:轻松处理文件上传,包括设置文件大小限制和唯一命名文件。
4. Next.js:
- 目的:提供具有服务器端渲染、静态站点生成和 API 路由的现代前端框架。
- 主要特点:高效路由、内置 CSS 支持和轻松的 API 集成。
后端设置
1. 快速服务器设置:
- 端点:处理视频上传,使用 FFmpeg 将视频转换为 HLS 格式,并提供视频文件。
- 代码重点
- 使用Multer上传视频。
- 使用FFmpeg将视频转换为 HLS 格式。
- 提供
.m3u8
播放列表和.ts
片段。
Server.ts:
import express from "express";
import cors from "cors";
import httpErrors from "http-errors";
import path from "path";
import fs from "fs";
import { exec } from "child_process";
interface CustomError extends Error {
code?:string
}
const app = express();
app.use(
cors({
origin: ["http://localhost:3000", "http://localhost:8000"],
credentials: true,
})
);
app.options("*", cors());
// Serve static files with proper mime types
app.use(
"uploads",
express.static("uploads", {
setHeaders: (res, filePath) =>{
if(filePath.endsWith(".m3u8")){
res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
}else if(filePath.endsWith(".ts")) {
res.setHeader("Content-Type", "video/mp2t");
}else if(filePath.endsWith(".mov")) {
res.setHeader("Content-Type", "video/quicktime");
}
},
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const storage = multer.diskStorage({
destination: function(req, file, cb) {
const dirname = path.join(__dirname, "uploads");
fs.mkdirSync(dirname, { recursive: true });
cb(null, dirname);
},
filename: function(req, file, cb) {
cb(null, uuidv4() + path.extname(file.originalname));
},
});
const upload = multer({
storage: storage,
limits: { fileSize: 100 * 1024 * 1024 },
});
app.get("/upload", function(req, res) {
const courseDirectoryPath = path.resolve(__dirname, "uploads/course");
fs.readdir(courseDirectoryPath, (err, files) => {
if(err) return res.status(500).json({ message: "Unable to scan directory", error: err });
const lessonIds = files.filter(file => !file.startsWith('.'));
if(lessonIds.length === 0) {
return res.status(404).json({ message: "No lessons found" });
}
const urls = lessonIds.map((lessonId) =>
uploads/course/${lessonId}/index.m3u8
);
res.status(200).json(urls);
});
});
app.post("/upload", upload.single("file"), function(req, res) {
if(!req.file) {
return res.status(400).json({ message: "No file uploaded" });
}
const lessonId = uuidv4();
const videoPath = path.join(__dirname, req.file.path);
const outputPath = path.join(__dirname, "output");
console.log(`hls-path: /upload/course/${lessonId}/index.m3u8`);
if(fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
// ffmpeg command to copy video to HLS format
const ffmpegCommand = `-i ${videoPath} -codec:v libx264 -crf 24 \
-map 0 -flags +global_header -segment_format mpegts \
-output_path ${outputPath}/segment%03d.ts -start_number 0${hlsPath}`;
exec(ffmpegCommand, (error, stdout, stderr) => {
if(error) {
console.error('exec error: ${error}');
return res.status(500).json({ message: "Error converting video" });
}
console.log(stdout);
console.log(stderr);
const videoUrl = `${http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}/uploads/course/${lessonId}/index.m3u8`;
res.json({
videoUrl: videoUrl,
lessonId: lessonId,
});
});
});
app.get("/", function(req, res) {
res.json({ message: "Hello World!" });
});
let PORT = process.env.PORT || 3000;
const server = app.listen(PORT, function() {
console.log(`Server running on port ${PORT}`);
});
server.on('error', function(err) {
if(err.code === 'EADDRINUSE') {
console.log(`Port is already in use. Switching to a different port...`);
PORT = Number(PORT) + 1;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} else {
console.error(`Server error: ${err as any}.message`);
}
});
2. FFmpeg 用于 HLS 转换的命令:
ffmpeg -i "${videoPath}" -codec:v libx264 -codec:a aac -hls_time 10 -hls_playlist_type vod -hls_segment_filename "${outputPath}/segment%03d.ts" -start_number 0 "${hlsPath}"
前端集成
1. 视频播放器组件:
- 使用 Hls.js:一个 JavaScript 库,可在支持 HLS 的浏览器中启用 HLS 播放。
- 实现:加载 HLS 源并将其附加到视频元素以实现流畅播放。
hlsPlayer.tsx:
'use client'
import React, { useRef, useEffect } from 'react'
import Hls from 'hls.js'
interface VideoPlayerProps {
src: string;
poster?: string;
}
export function VideoPlayer({ src, poster }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
let hls: Hls | null = null
if (videoRef.current) {
const video = videoRef.current
if (Hls.isSupported()) {
hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
})
hls.loadSource(src)
hls.attachMedia(video)
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src
}
return () => {
if (hls) {
hls.destroy()
}
}
}
}, [src])
return (
<video ref={videoRef} controls className="mx-auto w-full max-w-3xl" style={{ maxHeight: '70vh' }} poster={poster} />
)
}
Page.tsx:
'use client'
import { VideoPlayer } from '@/components/HlsPlayer'
import React from 'react'
export default function VideoPage() {
const videoUrl = 'http://localhost:8000/uploads/course/d90ac28e-a40a-49c3-9add-271ed44bb88e/index.m3u8'
return (
<div className="mx-auto mt-20 max-w-4xl p-4">
<h1 className="mb-6 text-3xl font-bold">Video Player</h1>
<div className="mb-6">
<VideoPlayer src={videoUrl} poster="/static/img/batman.webp" />
</div>
<div className="mt-6">
<h2 className="mb-2 text-xl font-semibold">Video Information</h2>
<p className="text-sm text-gray-600">
Video URL: <code className="rounded bg-gray-100 p-1">{videoUrl}</code>
</p>
</div>
</div>
)
}
2. 错误处理:
a. CORS 错误:
- 问题:浏览器阻止向不同来源的请求。
- 解决方案:在 Express 服务器中配置 CORS 标头,并更新 next.config.js 以包含允许的来源。
b. 内容安全策略 (CSP) 错误:
- 问题:由于 CSP 设置,媒体资源被阻止。
- 解决方案:调整 CSP 标头,允许来自服务器的媒体源。
3. CSP配置示例代码:
next.config.js
(无随机数):
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
media-src 'self' blob: ${process.env.LOCAL_FRONTEND_URL};
connect-src 'self' ${process.env.LOCAL_API_URL};
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
]
},
}
middleware.ts
(使用随机数):
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
return response
}
挑战与解决方案
- CORS 配置:
- 问题:浏览器阻止对后端的请求。
- 解决方案:更新 Express 中的 CORS 设置以允许特定来源。
2. CSP 错误:
- 问题:由于 CSP 限制而阻止媒体资源。
- 解决方案:调整了
next.config.js
中CSP 标头;并在middleware.ts
中使用了nonce
。
3. 多文件处理:
- 问题:管理大文件上传和唯一文件命名。
- 解决方案:配置 Multer 的文件大小限制,并使用 UUID 作为唯一的文件名。
结论
构建视频流平台涉及多种技术,需要克服多个挑战。通过利用 Node.js、FFmpeg、Multer 和 Next.js,创建了一个强大且可扩展的解决方案。尽管遇到了与 CORS、CSP 和文件处理相关的错误,我还是解决了这些问题,并提供了无缝的流媒体体验。
作者:Paudelronish
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。