You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 04:00:52