阶段4:格式自动识别 + flv.js 替换为 mpegts.js + 移动端 5 项修复 + 自动化冒烟测试
- 按 URL 扩展名自动识别插件(.m3u8→hls.js,.flv/.ts/.m2ts→mpegts.js),iOS 上 m3u8 仍走原生 HLS - flv 播放改由 mpegts.js 承担,移除 flv.js 依赖,plug:'flv.js' 保留兼容别名 - 修复 plug:'dash.js' 调用不存在函数的死代码 bug - mpegts 能力判断由 mseLivePlayback 放宽为 isSupported()(修复部分浏览器点播被误杀) - 移动端:iOS 原生全屏状态同步、iPad/iPod UA 识别、X5 同层播放属性、进度/音量条触摸拖动、触摸坐标滚动偏移修正 - 新增 Playwright 冒烟测试(npm test)与支持 Range 的本地服务器(npm run serve) - 文档说明:测试禁用 python http.server(不支持 Range,mp4 无法 seek)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 构建脚本(第二步)
|
||||
* 功能:从 node_modules 拷贝运行时依赖(hls.js/flv.js/mpegts.js)到 moeplayer/ 对应目录
|
||||
* 功能:从 node_modules 拷贝运行时依赖(hls.js/mpegts.js)到 moeplayer/ 对应目录
|
||||
* 播放器运行时会按需动态加载这些文件,目录结构需与 getPath() 的约定保持一致
|
||||
* 第一步(rollup 打包压缩)见 rollup.config.js
|
||||
* 用法:npm run build(即 rollup -c && node scripts/copy-libs.js)
|
||||
@@ -13,7 +13,6 @@ const root = path.resolve(__dirname, '..');
|
||||
// 运行时依赖拷贝清单:[npm包名, 产物目录, 需要拷贝的文件]
|
||||
const libs = [
|
||||
['hls.js', 'hls.js', ['hls.min.js', 'hls.min.js.map', 'LICENSE']],
|
||||
['flv.js', 'flv.js', ['flv.min.js', 'flv.min.js.map', 'LICENSE']],
|
||||
['mpegts.js', 'mpegts.js', ['mpegts.js', 'mpegts.js.map', 'LICENSE']]
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 支持 Range 请求的静态文件服务器(测试用)
|
||||
* python3 -m http.server 不支持 Range,会导致 mp4 无法 seek,故用此替代
|
||||
* 用法:node scripts/serve.js [端口]
|
||||
*/
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const port = parseInt(process.argv[2] || '8931', 10);
|
||||
const mime = {
|
||||
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
|
||||
'.png': 'image/png', '.mp4': 'video/mp4', '.json': 'application/json',
|
||||
'.map': 'application/json', '.ico': 'image/x-icon'
|
||||
};
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const urlPath = decodeURIComponent(req.url.split('?')[0]);
|
||||
let file = path.join(root, urlPath === '/' ? 'index.html' : urlPath);
|
||||
if (!file.startsWith(root)) { res.writeHead(403); res.end(); return; }
|
||||
fs.stat(file, (err, stat) => {
|
||||
if (err || !stat.isFile()) { res.writeHead(404); res.end('404'); return; }
|
||||
const type = mime[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
||||
const range = req.headers.range;
|
||||
if (range) {
|
||||
const m = /bytes=(\d*)-(\d*)/.exec(range);
|
||||
let start = m && m[1] ? parseInt(m[1], 10) : 0;
|
||||
let end = m && m[2] ? parseInt(m[2], 10) : stat.size - 1;
|
||||
end = Math.min(end, stat.size - 1);
|
||||
res.writeHead(206, {
|
||||
'Content-Type': type,
|
||||
'Content-Range': `bytes ${start}-${end}/${stat.size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1
|
||||
});
|
||||
fs.createReadStream(file, { start, end }).pipe(res);
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': type, 'Accept-Ranges': 'bytes', 'Content-Length': stat.size });
|
||||
fs.createReadStream(file).pipe(res);
|
||||
}
|
||||
});
|
||||
}).listen(port, () => console.log(`静态服务器(支持 Range): http://localhost:${port}`));
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 冒烟测试:真实浏览器加载演示页,验证核心功能
|
||||
* 覆盖:播放器初始化、播放/暂停、seek 跳转、点击进度条、皮肤文件加载
|
||||
* 用法:npm test(需要先 npm run build)
|
||||
*/
|
||||
const { chromium } = require('playwright');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 8931;
|
||||
let failed = 0;
|
||||
|
||||
function check(name, cond, detail) {
|
||||
if (cond) {
|
||||
console.log(' ✓ ' + name);
|
||||
} else {
|
||||
failed++;
|
||||
console.error(' ✗ ' + name + (detail ? ' —— ' + detail : ''));
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const server = spawn('node', [path.join(__dirname, 'serve.js'), String(PORT)], { stdio: 'ignore' });
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
|
||||
await page.goto(`http://localhost:${PORT}/index.html`);
|
||||
await page.waitForTimeout(2500);
|
||||
const video = () => page.evaluate(() => {
|
||||
const v = document.querySelector('video');
|
||||
return v ? { duration: v.duration, currentTime: v.currentTime, paused: v.paused } : null;
|
||||
});
|
||||
|
||||
console.log('初始化');
|
||||
check('全局 MoePlayer 存在', await page.evaluate(() => typeof window.MoePlayer === 'function'));
|
||||
const v0 = await video();
|
||||
check('video 元素已创建', !!v0);
|
||||
check('元数据已加载(duration>0)', v0 && v0.duration > 0, JSON.stringify(v0));
|
||||
|
||||
console.log('播放控制');
|
||||
await page.evaluate(() => player.play());
|
||||
await page.waitForTimeout(1500);
|
||||
const v1 = await video();
|
||||
check('播放中', v1 && !v1.paused && v1.currentTime > 0.5, JSON.stringify(v1));
|
||||
await page.evaluate(() => player.pause());
|
||||
await page.waitForTimeout(300);
|
||||
check('暂停', (await video()).paused);
|
||||
|
||||
console.log('seek');
|
||||
await page.evaluate(() => player.seek(20));
|
||||
await page.waitForTimeout(800);
|
||||
const v2 = await video();
|
||||
check('seek(20) 生效', v2.currentTime >= 19 && v2.currentTime <= 22, 'currentTime=' + v2.currentTime);
|
||||
|
||||
console.log('进度条点击');
|
||||
const rect = await page.evaluate(() => {
|
||||
const r = document.querySelector('.moe-bar-progress-bg').getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
});
|
||||
await page.mouse.click(rect.x + rect.w * 0.7, rect.y + rect.h / 2);
|
||||
await page.waitForTimeout(800);
|
||||
const v3 = await video();
|
||||
const expect3 = v0.duration * 0.7;
|
||||
check('点击 70% 位置跳转', Math.abs(v3.currentTime - expect3) < 2.5, `currentTime=${v3.currentTime},期望≈${expect3}`);
|
||||
|
||||
console.log('皮肤');
|
||||
for (const css of ['moeplayer.css', 'moeplayer.red.css', 'moeplayer.ixigua.css']) {
|
||||
const resp = await page.request.get(`http://localhost:${PORT}/moeplayer/css/${css}`);
|
||||
check(css + ' 可加载', resp.status() === 200);
|
||||
}
|
||||
|
||||
check('无页面 JS 错误', errors.length === 0, errors.join(' | '));
|
||||
|
||||
await browser.close();
|
||||
server.kill();
|
||||
console.log(failed === 0 ? '\n全部通过' : `\n${failed} 项失败`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user