bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

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