Node/Express+React单页多视频流播放实现技术求助
解决方案:React+Vite + Node+Express 实现本地视频文件夹流式播放
后端实现(Node+Express)
核心思路
不需要在单个接口里处理多文件流,而是拆分两个核心接口:一个返回视频文件列表,一个根据文件名单独流式传输视频。这种设计符合HTTP规范,也更易维护和处理并发。
完整代码示例(server.cjs)
const express = require('express'); const fs = require('fs/promises'); const path = require('path'); const app = express(); const PORT = 3001; // 目标视频文件夹路径,自行修改为你的本地路径 const VIDEO_DIR = path.resolve(__dirname, './local-videos'); // 跨域配置(适配Vite默认前端端口3000) app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); res.header('Access-Control-Allow-Methods', 'GET'); next(); }); // 接口1:获取视频文件列表 app.get('/api/video-list', async (req, res) => { try { const files = await fs.readdir(VIDEO_DIR); // 过滤常见视频格式,可按需扩展 const videoFiles = files.filter(file => { const ext = path.extname(file).toLowerCase(); return ['.mp4', '.mov', '.avi', '.webm'].includes(ext); }); res.json(videoFiles); } catch (err) { console.error(err); res.status(500).json({ error: '读取视频目录失败' }); } }); // 接口2:单个视频流式传输 app.get('/api/video/:filename', async (req, res) => { const filename = req.params.filename; const videoPath = path.join(VIDEO_DIR, filename); try { // 检查文件是否存在 await fs.access(videoPath); const stats = await fs.stat(videoPath); // 设置视频响应头,确保浏览器正确识别 res.setHeader('Content-Type', `video/${path.extname(filename).slice(1)}`); res.setHeader('Content-Length', stats.size); res.setHeader('Accept-Ranges', 'bytes'); // 创建读取流并管道到响应,实现流式传输 const readStream = fs.createReadStream(videoPath); readStream.pipe(res); // 处理流错误 readStream.on('error', (err) => { console.error(err); res.status(500).end('视频流传输失败'); }); } catch (err) { if (err.code === 'ENOENT') { res.status(404).json({ error: '视频文件不存在' }); } else { console.error(err); res.status(500).json({ error: '获取视频文件失败' }); } } }); app.listen(PORT, () => { console.log(`服务运行在 http://localhost:${PORT}`); });
前端实现(React+Vite)
核心思路
通过请求后端文件列表接口,拿到文件名后动态拼接视频流接口URL,循环渲染<video>组件即可。
完整代码示例(Viddz.jsx)
import { useState, useEffect } from 'react'; const Viddz = () => { const [videoList, setVideoList] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchVideos = async () => { try { const res = await fetch('http://localhost:3001/api/video-list'); if (!res.ok) throw new Error('获取视频列表失败'); const videos = await res.json(); setVideoList(videos); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchVideos(); }, []); if (loading) return <div>加载视频中...</div>; if (error) return <div>错误:{error}</div>; return ( <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem', padding: '1rem' }}> {videoList.map(filename => ( <video key={filename} controls style={{ width: '300px', height: 'auto' }} src={`http://localhost:3001/api/video/${encodeURIComponent(filename)}`} > 你的浏览器不支持视频播放 </video> ))} </div> ); }; export default Viddz;
关键知识点说明
后端流式传输
- 单个文件流接口是最优方案:每个视频请求独立处理,Node.js的流机制会自动处理并发,避免内存溢出。
fs.createReadStream会分块读取文件,配合res.pipe()直接将数据发送给浏览器,无需加载整个文件到内存。- 正确设置响应头是关键:
Content-Type告诉浏览器视频格式,Content-Length支持进度条拖拽,Accept-Ranges支持断点续传。
前端动态设置src
- 使用
encodeURIComponent处理文件名中的特殊字符(空格、中文等),避免URL解析错误。 - 循环渲染时用文件名作为
key,比index更稳定,避免React渲染异常。
内容的提问来源于stack exchange,提问作者schizoSan
相关产品推荐
相关产品推荐

