File size: 7,900 Bytes
51d5cd5 f8d8200 8cfcabf f8d8200 51d5cd5 8cfcabf 878cc6e 51d5cd5 4e7eecc 51d5cd5 f8d8200 51d5cd5 8cfcabf 51d5cd5 8cfcabf 51d5cd5 878cc6e 51d5cd5 8cfcabf 878cc6e 9d9d550 82169fb 51d5cd5 9d9d550 878cc6e 51d5cd5 f8d8200 3356f01 c5fa5ac d72929d 3356f01 d72929d f8d8200 89721cc 51d5cd5 3356f01 8ad67b4 51d5cd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | import express from 'express';
import { createServer as createViteServer } from 'vite';
import { createProxyMiddleware } from 'http-proxy-middleware';
import path from 'path';
import crypto from 'crypto';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { spawn, execSync } from 'child_process';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const NodeMediaServer = require('node-media-server');
let constTunnelUrl: string | null = null;
let startingBore = false;
async function startBoreTunnel() {
if (startingBore) return;
startingBore = true;
const borePath = path.join(process.cwd(), 'bore');
if (!fs.existsSync(borePath)) {
console.log('[SYSTEM] Downloading bore TCP proxy...');
try {
const res = await fetch("https://github.com/ekzhang/bore/releases/download/v0.5.1/bore-v0.5.1-x86_64-unknown-linux-musl.tar.gz");
const buffer = await res.arrayBuffer();
fs.writeFileSync("bore.tar.gz", Buffer.from(buffer));
execSync("tar -xzf bore.tar.gz");
execSync("chmod +x bore");
fs.unlinkSync("bore.tar.gz");
} catch(e) {
console.error('[SYSTEM] Failed to download bore tunnel:', e);
return;
}
}
console.log('[SYSTEM] Starting bore TCP tunnel on port 1935...');
const cp = spawn(borePath, ['local', '1935', '--to', 'bore.pub']);
const handleData = (data: Buffer) => {
const text = data.toString();
const match = text.match(/listening at (bore\.pub:\d+)/);
if (match) {
constTunnelUrl = `rtmp://${match[1]}/live`;
console.log(`[SYSTEM] TCP Tunnel active at: ${constTunnelUrl}`);
}
};
cp.stdout.on('data', handleData);
cp.stderr.on('data', handleData);
cp.on('close', (code) => {
console.log(`[SYSTEM] Bore tunnel closed with code ${code}`);
constTunnelUrl = null;
setTimeout(() => { startingBore = false; startBoreTunnel(); }, 5000);
});
}
async function startServer() {
const app = express();
const PORT = process.env.PORT ? parseInt(process.env.PORT as string, 10) : 3000;
// Logging middleware
app.use((req, res, next) => {
console.log(`[REQ] ${req.method} ${req.url}`);
next();
});
// Start Node Media Server for RTMP Ingest (1935) and HTTP FLV (8123)
const nmsConfig = {
rtmp: {
port: 1935,
chunk_size: 60000,
gop_cache: true,
ping: 30,
ping_timeout: 60
},
http: {
port: 8123,
allow_origin: '*',
}
};
const nms = new NodeMediaServer(nmsConfig);
nms.run();
const activeStreams = new Set<string>();
nms.on('prePublish', (...args: any[]) => {
const session = args[0];
let streamPath = (typeof session === 'string') ? args[1] : (session?.streamPath || session?.StreamPath || session?.publishStreamPath);
if (args.length > 1 && typeof args[1] === 'string') {
streamPath = args[1];
}
console.log('[SYSTEM] RTMP stream prePublish payload:', streamPath);
if (streamPath) activeStreams.add(streamPath.toLowerCase());
});
nms.on('postPublish', (...args: any[]) => {
const session = args[0];
let streamPath = (typeof session === 'string') ? args[1] : (session?.streamPath || session?.StreamPath || session?.publishStreamPath);
if (args.length > 1 && typeof args[1] === 'string') {
streamPath = args[1];
}
console.log('[SYSTEM] RTMP stream started:', streamPath);
if (streamPath) activeStreams.add(streamPath.toLowerCase());
});
nms.on('donePublish', (...args: any[]) => {
const session = args[0];
let streamPath = (typeof session === 'string') ? args[1] : (session?.streamPath || session?.StreamPath || session?.publishStreamPath);
if (args.length > 1 && typeof args[1] === 'string') {
streamPath = args[1];
}
console.log('[SYSTEM] RTMP stream ended:', streamPath);
if (streamPath) activeStreams.delete(streamPath.toLowerCase());
});
// Start the TCP Tunnel proxy
startBoreTunnel();
// Proxy HTTP FLV requests from port 3000 to port 8123 where NMS is listening
// This allows the frontend web player to access the stream using HTTP-FLV over port 3000
app.use('/live', createProxyMiddleware({
target: 'http://127.0.0.1:8123',
changeOrigin: true,
ws: true
}));
// Our own stream checker API
app.get('/api/streams/:app/:key', (req, res) => {
const streamPath = `/${req.params.app}/${req.params.key}`.toLowerCase();
const isActive = activeStreams.has(streamPath);
console.log(`[REQ] GET /api/streams${streamPath} => ${isActive}`);
res.json({ active: isActive });
});
// API Route to get connection info
app.get('/api/config', async (req, res) => {
let rtmpUrl = '';
// Check if bore TCP tunnel successfully issued a public proxy address
if (constTunnelUrl) {
return res.json({ rtmpUrl: constTunnelUrl, isNgrok: true }); // using isNgrok flag to keep UI happy and colored emerald!
}
// Hugging Face Spaces injects SPACE_HOST
if (process.env.SPACE_HOST) {
rtmpUrl = `rtmp://${process.env.SPACE_HOST}:1935/live`;
} else {
const appUrl = process.env.APP_URL || 'localhost';
// Determine the external URL. Note: AI Studio only exposes Port 3000 (HTTP).
if (appUrl.startsWith('https://')) {
const hostname = new URL(appUrl).hostname;
rtmpUrl = `rtmp://${hostname}:1935/live`;
} else {
rtmpUrl = `rtmp://${appUrl.split(':')[0]}:1935/live`;
}
}
res.json({ rtmpUrl });
});
// Vite middleware for development
if (process.env.NODE_ENV !== 'production') {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'spa',
});
app.use(vite.middlewares);
} else {
// production mode
// __dirname is the directory where server.js is located (which should be 'dist' thanks to esbuild)
const distPath = __dirname;
console.log('[SYSTEM] Static dist path:', distPath);
// explicitly serve assets directory
app.use('/assets', express.static(path.join(distPath, 'assets'), {
setHeaders: (res, filePath) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}));
// serve the rest of dist
app.use(express.static(distPath, {
setHeaders: (res, filePath) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
}
}));
// Add health diagnostic endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', dir: __dirname, time: new Date().toISOString() });
});
app.get('/assets/*', (req, res) => {
res.status(404).send('Not Found');
});
app.get('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.sendFile(path.join(distPath, 'index.html'));
});
}
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`Express server running on http://localhost:${PORT}`);
});
// Graceful shutdown to release ports
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down...');
nms.stop();
server.close();
process.exit(0);
});
}
startServer();
|