

上传页面.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');
});