stream-pack-init.js (5653B)
1 const fs = require("fs"); 2 const path = require("path"); 3 const crypto = require("crypto"); 4 5 const ROOT = path.join(__dirname, ".."); 6 const PACK_DIR = path.join(ROOT, "stream_pack"); 7 8 function log(msg) { 9 console.log(`[stream-pack] ${msg}`); 10 } 11 12 function fail(msg) { 13 console.error(`[stream-pack] ERROR: ${msg}`); 14 process.exit(1); 15 } 16 17 function parseArgs() { 18 const args = process.argv.slice(2); 19 const out = {}; 20 for (const a of args) { 21 const m = String(a || "").match(/^--([^=]+)=(.*)$/); 22 if (m) out[m[1]] = m[2]; 23 } 24 return out; 25 } 26 27 function randToken(bytes) { 28 return crypto.randomBytes(bytes).toString("base64url"); 29 } 30 31 function normalizeDomain(s) { 32 const v = String(s || "").trim().toLowerCase(); 33 if (!v) return ""; 34 if (!/^[a-z0-9.-]+$/.test(v)) return ""; 35 if (!v.includes(".")) return ""; 36 return v; 37 } 38 39 function writeFileAlways(filePath, content) { 40 fs.writeFileSync(filePath, content, "utf8"); 41 } 42 43 function readEnvFile(envPath) { 44 try { 45 const raw = fs.readFileSync(envPath, "utf8"); 46 const out = {}; 47 for (const line of raw.split(/\r?\n/)) { 48 const s = line.trim(); 49 if (!s || s.startsWith("#")) continue; 50 const m = s.match(/^([A-Z0-9_]+)=(.*)$/); 51 if (!m) continue; 52 let v = m[2] || ""; 53 v = v.trim(); 54 if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); 55 out[m[1]] = v; 56 } 57 return out; 58 } catch { 59 return {}; 60 } 61 } 62 63 function main() { 64 const args = parseArgs(); 65 const domain = normalizeDomain(args.domain); 66 const email = String(args.email || "").trim(); 67 68 if (!domain) { 69 fail('Missing/invalid `--domain=...` (example: --domain=stream.example.com)'); 70 } 71 if (!email || !email.includes("@")) { 72 fail('Missing/invalid `--email=...` (needed for HTTPS certs; example: --email=you@example.com)'); 73 } 74 75 fs.mkdirSync(PACK_DIR, { recursive: true }); 76 77 const envPath = path.join(PACK_DIR, ".env"); 78 const existing = readEnvFile(envPath); 79 80 const API_KEY = existing.LIVEKIT_API_KEY || randToken(8); 81 const API_SECRET = existing.LIVEKIT_API_SECRET || randToken(24); 82 const TURN_USER = existing.TURN_USER || "bzl"; 83 const TURN_PASS = existing.TURN_PASS || randToken(18); 84 const TURN_REALM = existing.TURN_REALM || domain; 85 const TURN_EXTERNAL_IP = existing.TURN_EXTERNAL_IP || ""; 86 const RTC_UDP_START = existing.RTC_UDP_START || "50000"; 87 const RTC_UDP_END = existing.RTC_UDP_END || "50100"; 88 const TURN_RELAY_START = existing.TURN_RELAY_START || "49160"; 89 const TURN_RELAY_END = existing.TURN_RELAY_END || "49200"; 90 91 const lines = [ 92 "# Stream Pack (optional)", 93 "#", 94 "# This stack is separate from core Bzl. Core stays runnable without any domain.", 95 "# Streaming requires HTTPS and a public server.", 96 "", 97 `STREAM_DOMAIN=${domain}`, 98 `STREAM_EMAIL=${email}`, 99 "", 100 "# LiveKit auth (Bzl will mint JWTs with these)", 101 `LIVEKIT_API_KEY=${API_KEY}`, 102 `LIVEKIT_API_SECRET=${API_SECRET}`, 103 "", 104 "# TURN (coturn) credentials for LiveKit clients", 105 `TURN_USER=${TURN_USER}`, 106 `TURN_PASS=${TURN_PASS}`, 107 `TURN_REALM=${TURN_REALM}`, 108 `TURN_EXTERNAL_IP=${TURN_EXTERNAL_IP}`, 109 "", 110 "# WebRTC UDP port range exposed by livekit-server (keep small for firewall sanity)", 111 `RTC_UDP_START=${RTC_UDP_START}`, 112 `RTC_UDP_END=${RTC_UDP_END}`, 113 "", 114 "# TURN relay UDP ports (coturn will allocate from this range)", 115 `TURN_RELAY_START=${TURN_RELAY_START}`, 116 `TURN_RELAY_END=${TURN_RELAY_END}`, 117 "" 118 ]; 119 writeFileAlways(envPath, lines.join("\n")); 120 log(`Wrote ${path.relative(ROOT, envPath)}`); 121 122 const livekitYamlPath = path.join(PACK_DIR, "livekit.yaml"); 123 const livekitYaml = `port: 7880 124 log_level: info 125 126 rtc: 127 tcp_port: 7881 128 port_range_start: ${RTC_UDP_START} 129 port_range_end: ${RTC_UDP_END} 130 use_external_ip: true 131 turn_servers: 132 - host: ${domain} 133 port: 3478 134 protocol: udp 135 username: ${TURN_USER} 136 credential: ${TURN_PASS} 137 138 keys: 139 ${API_KEY}: ${API_SECRET} 140 `; 141 writeFileAlways(livekitYamlPath, livekitYaml); 142 log(`Wrote ${path.relative(ROOT, livekitYamlPath)}`); 143 144 const composePath = path.join(PACK_DIR, "docker-compose.yml"); 145 const composeBody = `services: 146 livekit: 147 image: livekit/livekit-server:latest 148 container_name: bzl_livekit 149 restart: unless-stopped 150 command: --config /etc/livekit.yaml 151 volumes: 152 - ./livekit.yaml:/etc/livekit.yaml:ro 153 ports: 154 - "127.0.0.1:7880:7880" 155 - "7881:7881" 156 - "${RTC_UDP_START}-${RTC_UDP_END}:${RTC_UDP_START}-${RTC_UDP_END}/udp" 157 158 turn: 159 image: coturn/coturn:latest 160 container_name: bzl_turn 161 restart: unless-stopped 162 network_mode: host 163 command: 164 - -n 165 - --log-file=stdout 166 - --realm=${TURN_REALM} 167 - --fingerprint 168 - --lt-cred-mech 169 - --no-tls 170 - --no-dtls 171 - --no-cli 172 - --no-multicast-peers 173 - --no-loopback-peers 174 - --listening-port=3478 175 - --min-port=${TURN_RELAY_START} 176 - --max-port=${TURN_RELAY_END} 177 - --user=${TURN_USER}:${TURN_PASS} 178 - --external-ip=${TURN_EXTERNAL_IP} 179 `; 180 writeFileAlways(composePath, composeBody); 181 log(`Wrote ${path.relative(ROOT, composePath)}`); 182 183 const caddySnippetPath = path.join(PACK_DIR, "Caddyfile.snippet"); 184 const caddySnippet = `${domain} { 185 encode zstd gzip 186 reverse_proxy 127.0.0.1:7880 187 } 188 `; 189 writeFileAlways(caddySnippetPath, caddySnippet); 190 log(`Wrote ${path.relative(ROOT, caddySnippetPath)}`); 191 192 log("Done."); 193 log("Next: edit stream_pack/.env (TURN_EXTERNAL_IP), add the Caddy snippet, open firewall ports, then run `docker compose up -d`."); 194 } 195 196 main(); 197