大文件分片案例html + nodejs + 视频上传案例

上传页面.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>流媒体视频播放</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { padding: 30px; font-family: "Microsoft Yahei", Arial, sans-serif; }
    .input-group { margin: 20px 0; display: flex; gap: 10px; align-items: center; }
    input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
    button { padding: 10px 20px; background: #2196F3; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
    video { width: 100%; max-height: 70vh; background: #000; border-radius: 4px; }
    .log { margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; font-size: 12px; color: #666; }
  </style>
</head>
<body>
  <h1>流媒体视频播放</h1>
  <div class="input-group">
    <input type="text" id="videoName" value="微信视频2026-04-09_102453_303.mp4">
    <button onclick="playVideo()">播放视频</button>
  </div>
  <video id="videoPlayer" controls></video>
  <div class="log" id="log"></div>

  <script>
    const logEl = document.getElementById('log');
    const videoPlayer = document.getElementById('videoPlayer');

    // 日志工具
    function log(msg) {
      logEl.innerHTML += `<p>${new Date().toLocaleTimeString()} - ${msg}</p>`;
      console.log(msg);
    }

    async function playVideo() {
      const videoName = document.getElementById('videoName').value.trim();

      if (!videoName) {
        alert('请输入视频文件名!');
        return;
      }

      // 🔴 核心修复:编码中文文件名,解决乱码404
      const encodedName = encodeURIComponent(videoName);
      const videoUrl = `http://localhost:3000/video/${encodedName}`;

      log(`请求视频地址:${videoUrl}`);

      // 先验证接口是否正常
      try {
        const res = await fetch(videoUrl, { method: 'HEAD' });
        if (!res.ok) {
          log(`接口请求失败,状态码:${res.status}`);
          alert(`播放失败!接口返回${res.status},请检查后端服务和文件名`);
          return;
        }
        log(`接口验证成功,状态码:${res.status}`);
      } catch (err) {
        log(`请求错误:${err.message}`);
        alert('网络错误,请检查后端服务是否开启');
        return;
      }

      // 给播放器赋值地址
      videoPlayer.src = videoUrl;

      // 监听加载成功
      videoPlayer.oncanplay = () => {
        log('视频加载成功,开始播放');
        videoPlayer.play();
      };

      // 监听加载错误
      videoPlayer.onerror = (err) => {
        log(`播放错误:${err.message}`);
        alert('播放失败!请检查:\n1. 后端服务是否开启\n2. 文件名是否正确\n3. 视频是否上传成功');
      };
    }
  </script>
</body>
</html>

播放页面.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>流媒体视频播放</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { padding: 30px; font-family: "Microsoft Yahei", Arial, sans-serif; }
    .input-group { margin: 20px 0; display: flex; gap: 10px; align-items: center; }
    input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
    button { padding: 10px 20px; background: #2196F3; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
    video { width: 100%; max-height: 70vh; background: #000; border-radius: 4px; }
    .log { margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; font-size: 12px; color: #666; }
  </style>
</head>
<body>
  <h1>流媒体视频播放</h1>
  <div class="input-group">
    <input type="text" id="videoName" value="微信视频2026-04-09_102453_303.mp4">
    <button onclick="playVideo()">播放视频</button>
  </div>
  <video id="videoPlayer" controls></video>
  <div class="log" id="log"></div>

  <script>
    const logEl = document.getElementById('log');
    const videoPlayer = document.getElementById('videoPlayer');

    // 日志工具
    function log(msg) {
      logEl.innerHTML += `<p>${new Date().toLocaleTimeString()} - ${msg}</p>`;
      console.log(msg);
    }

    async function playVideo() {
      const videoName = document.getElementById('videoName').value.trim();

      if (!videoName) {
        alert('请输入视频文件名!');
        return;
      }

      // 🔴 核心修复:编码中文文件名,解决乱码404
      const encodedName = encodeURIComponent(videoName);
      const videoUrl = `http://localhost:3000/video/${encodedName}`;

      log(`请求视频地址:${videoUrl}`);

      // 先验证接口是否正常
      try {
        const res = await fetch(videoUrl, { method: 'HEAD' });
        if (!res.ok) {
          log(`接口请求失败,状态码:${res.status}`);
          alert(`播放失败!接口返回${res.status},请检查后端服务和文件名`);
          return;
        }
        log(`接口验证成功,状态码:${res.status}`);
      } catch (err) {
        log(`请求错误:${err.message}`);
        alert('网络错误,请检查后端服务是否开启');
        return;
      }

      // 给播放器赋值地址
      videoPlayer.src = videoUrl;

      // 监听加载成功
      videoPlayer.oncanplay = () => {
        log('视频加载成功,开始播放');
        videoPlayer.play();
      };

      // 监听加载错误
      videoPlayer.onerror = (err) => {
        log(`播放错误:${err.message}`);
        alert('播放失败!请检查:\n1. 后端服务是否开启\n2. 文件名是否正确\n3. 视频是否上传成功');
      };
    }
  </script>
</body>
</html>

package.json

javascript 复制代码
{
  "name": "liumeit",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "cors": "^2.8.6",
    "express": "^5.2.1",
    "multer": "^2.1.1"
  }
}

server.js

javascript 复制代码
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const multer = require('multer');

const app = express();
app.use(cors());
app.use(express.json());

// 目录
const UPLOAD_DIR = path.join(__dirname, 'uploads');
const CHUNK_DIR = path.join(__dirname, 'chunks');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR);
if (!fs.existsSync(CHUNK_DIR)) fs.mkdirSync(CHUNK_DIR);

// 最简单的存储,不会出错
const upload = multer({ dest: CHUNK_DIR });

// 上传分片
app.post('/upload/chunk', upload.single('chunk'), (req, res) => {
  const { filename, index } = req.body;

  // 🔴 关键:直接改名!彻底解决 multer 解析问题
  const tempPath = req.file.path;
  const targetPath = path.join(CHUNK_DIR, `${filename}-${index}`);
  fs.renameSync(tempPath, targetPath);

  console.log(`✅ 分片 ${index} 保存成功:${filename}-${index}`);
  res.send('ok');
});

// 合并分片
app.post('/upload/merge', (req, res) => {
  const { filename, totalChunks } = req.body;
  const final = fs.createWriteStream(path.join(UPLOAD_DIR, filename));

  for (let i = 0; i < totalChunks; i++) {
    const chunkPath = path.join(CHUNK_DIR, `${filename}-${i}`);
    
    if (!fs.existsSync(chunkPath)) {
      console.log(`❌ 分片不存在:${chunkPath}`);
      return res.status(500).send('缺失分片');
    }

    final.write(fs.readFileSync(chunkPath));
    fs.unlinkSync(chunkPath);
  }

  final.end();
  console.log('✅ 合并完成!');
  res.send('ok');
});

// 3. 视频播放接口(终极修复版,路径100%正确)
app.get('/video/:filename', (req, res) => {
  try {
    const filename = req.params.filename;
    // 🔴 核心修复:用 path.join 绝对路径,彻底解决路径错误
    const filePath = path.join(__dirname, 'uploads', filename);

    console.log(`🔍 播放请求:${filename},完整路径:${filePath}`);
    console.log(`✅ 文件是否存在:${fs.existsSync(filePath)}`);

    if (!fs.existsSync(filePath)) {
      return res.status(404).json({ error: '视频不存在', path: filePath });
    }

    const stat = fs.statSync(filePath);
    const fileSize = stat.size;
    const range = req.headers.range;

    // 统一设置CORS头
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Accept-Ranges', 'bytes');
    res.setHeader('Content-Type', 'video/mp4');

    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 readStream = fs.createReadStream(filePath, { start, end });
      res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
      res.setHeader('Content-Length', chunkSize);
      res.writeHead(206);
      readStream.pipe(res);
    } else {
      res.setHeader('Content-Length', fileSize);
      res.writeHead(200);
      fs.createReadStream(filePath).pipe(res);
    }
  } catch (err) {
    console.error('❌ 播放接口错误:', err);
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, () => {
  console.log('🚀 服务启动:http://localhost:3000');
});
相关推荐
Cobyte1 天前
3.响应式系统基础:从发布订阅模式的角度理解 Vue2 的数据响应式原理
前端·javascript·vue.js
竹林8181 天前
从零到一:在React前端中集成The Graph查询Uniswap V3池数据实战
前端·javascript
军军君011 天前
Three.js基础功能学习十八:智能黑板实现实例五
前端·javascript·vue.js·3d·typescript·前端框架·threejs
Moment1 天前
AI全栈入门指南:一文搞清楚NestJs 中的 Controller 和路由
前端·javascript·后端
程序员马晓博1 天前
前端并发治理:从 Token 刷新聊起,一个 Promise 就够了
前端·javascript
ai产品老杨1 天前
异构计算时代的视频底座:基于 ZLMediaKit 与 Spring Boot 的 X86/ARM 跨平台架构解析
arm开发·spring boot·音视频
英俊潇洒美少年1 天前
Vue、React.lazy、React 19 异步组件核心区别
javascript·vue.js·react.js
快乐小土豆~~1 天前
echarts柱状图的X轴label过长被重叠覆盖
前端·javascript·vue.js·echarts
hhcccchh1 天前
1.1 HTML 语义化标签(header、nav、main、section、footer 等)
java·前端·html
小李子呢02111 天前
前端八股2---Proxy 代理
前端·javascript·vue.js