const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = process.env.PORT || 24710; const HOST = process.env.HOST || '0.0.0.0'; const ROOT = __dirname; const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream', '.css': 'text/css', '.png': 'image/png', '.ico': 'image/x-icon', '.svg': 'image/svg+xml', '.json': 'application/json', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.mp4': 'video/mp4', '.m4v': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', }; function extType(p) { return MIME[path.extname(p).toLowerCase()] || 'application/octet-stream'; } function originalCompressedPath(fp) { if (fp.endsWith('.unityweb')) return fp.slice(0, -'.unityweb'.length); if (fp.endsWith('.gz')) return fp.slice(0, -'.gz'.length); if (fp.endsWith('.br')) return fp.slice(0, -'.br'.length); return fp; } function headersForFile(fp, size, encoding) { const headers = { 'Content-Type': extType(originalCompressedPath(fp)), 'Content-Length': size, 'X-Content-Type-Options': 'nosniff', 'Accept-Ranges': 'bytes', }; if (encoding != null) { headers['Content-Encoding'] = encoding; headers['Cache-Control'] = 'public, max-age=604800'; } else if (/\.(mp4|m4v|webm|ogg)$/i.test(fp)) { headers['Cache-Control'] = 'public, max-age=604800'; } else if (fp.endsWith('.loader.js')) { headers['Cache-Control'] = 'no-cache'; } else { headers['Cache-Control'] = 'public, max-age=3600'; } return headers; } function detectUnityEncoding(fp, callback) { if (fp.endsWith('.gz')) { callback(null, 'gzip'); return; } if (fp.endsWith('.br')) { callback(null, 'br'); return; } if (!fp.endsWith('.unityweb')) { callback(null, null); return; } fs.open(fp, 'r', (openErr, fd) => { if (openErr) { callback(openErr); return; } const buf = Buffer.alloc(2); fs.read(fd, buf, 0, 2, 0, (readErr) => { fs.close(fd, () => {}); if (readErr) { callback(readErr); return; } callback(null, buf[0] === 0x1f && buf[1] === 0x8b ? 'gzip' : 'br'); }); }); } // 首页:基于原始 index.html,注入启动封面(不修改任何 Unity 加载逻辑) function serveIndex(res) { try { let html = fs.readFileSync(path.join(ROOT, 'index.html'), 'utf-8'); // 注入到 之前的 CSS const css = ` `; // 注入到 最前面的 overlay const overlay = `

运动伤害现场急救虚拟仿真实验

VIRTUAL SIMULATION EXPERIMENT

点击按钮进入全屏沉浸式实验环境

`; // 注入到 之前的 JS(不修改原始 Unity 加载逻辑) const js = ` `; html = html.replace('', css + '\n'); html = html.replace('', '\n' + overlay); html = html.replace('', js + '\n'); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); } catch (err) { res.writeHead(500); res.end('Internal Server Error'); console.error(err); } } // 静态文件 function serveStatic(req, res) { let requestPath; try { requestPath = decodeURIComponent(req.url.split('?')[0]); } catch (err) { res.writeHead(400); res.end('Bad Request'); return; } let fp = path.join(ROOT, requestPath); if (!fp.startsWith(ROOT)) { res.writeHead(403); res.end('Forbidden'); return; } fs.stat(fp, (err, stat) => { if (err || !stat.isFile()) { res.writeHead(404); res.end('Not Found'); return; } detectUnityEncoding(fp, (encodingErr, encoding) => { if (encodingErr) { res.writeHead(500); res.end('Internal Server Error'); return; } const range = req.headers.range; if (range) { const match = /^bytes=(\d*)-(\d*)$/.exec(range); if (!match) { res.writeHead(416, { 'Content-Range': `bytes */${stat.size}` }); res.end(); return; } let start; let end; if (match[1] === '' && match[2] !== '') { const suffixLength = parseInt(match[2], 10); start = Math.max(stat.size - suffixLength, 0); end = stat.size - 1; } else { start = match[1] === '' ? 0 : parseInt(match[1], 10); end = match[2] === '' ? stat.size - 1 : parseInt(match[2], 10); } if (Number.isNaN(start) || Number.isNaN(end) || start > end || end >= stat.size) { res.writeHead(416, { 'Content-Range': `bytes */${stat.size}` }); res.end(); return; } const headers = headersForFile(fp, end - start + 1, encoding); headers['Content-Range'] = `bytes ${start}-${end}/${stat.size}`; res.writeHead(206, headers); fs.createReadStream(fp, { start, end }).pipe(res); return; } res.writeHead(200, headersForFile(fp, stat.size, encoding)); fs.createReadStream(fp).pipe(res); }); }); } http.createServer((req, res) => { if (req.url === '/' || req.url === '/index.html') return serveIndex(res); serveStatic(req, res); }).listen(PORT, HOST, () => { console.log(''); console.log(' ╔══════════════════════════════════════════════════╗'); console.log(' ║ 运动伤害现场急救虚拟仿真实验 - WebGL Server ║'); console.log(' ║ ║'); console.log(' ║ Server running at: http://' + HOST + ':' + PORT + ' ║'); console.log(' ║ Press Ctrl+C to stop ║'); console.log(' ╚══════════════════════════════════════════════════╝'); console.log(''); });