const assert = require("assert"); const crypto = require("crypto"); const fs = require("fs"); const os = require("os"); const path = require("path"); const { EventEmitter } = require("events"); const { spawn } = require("child_process"); const AdmZip = require("adm-zip"); const root = path.join(__dirname, ".."); const { ArtifactManager, safeArchivePath, validateManifestEntry } = require("../src/services/artifact-manager"); const { MediaMtxRuntime, parseMetrics, readResponseText, renderConfig, sha256Credential, validateHlsAsset } = require("../src/services/mediamtx-runtime"); const { StreamTestingService, INACTIVITY_MS, isPrivateAddress, resolveIngestConfiguration, rewriteManifest, sourceFor, validateHostname } = require("../src/services/stream-testing"); const protocol = require("../plugins/lumi_transcription/backend/companion/protocol"); class FakeRuntime extends EventEmitter { constructor() { super(); this.session = null; this.running = false; this.cleared = 0; this.pathReady = true; this.inboundBytes = 4096; } status() { return { available: true, installed: true, valid: true, version: "v1.18.2", tested_version: "v1.18.2", source: "managed by Lumi", managed_install_supported: true, detail: "MediaMTX v1.18.2 is ready.", process: { state: this.running ? "ready" : "stopped", running: this.running, restart_count: 0, log_tail: "" } }; } async configureSession(session) { this.session = session; this.running = true; } async clearSession() { this.session = null; this.cleared += 1; } async diagnostics(sessionPath) { return { path: { name: sessionPath, ready: this.pathReady, tracks: ["H264", "MPEG4Audio"], bytes_received: this.inboundBytes, bytes_sent: 2048, readers: 1 }, metrics: { paths_inbound_bytes: this.inboundBytes, paths_outbound_bytes: 2048, hls_sessions: 1 }, process: { state: "ready", running: true } }; } async install() { return this.status(); } async repair() { return this.status(); } async healthCheck() { return { ok: true, version: "v1.18.2", api_status: 200, metrics_status: 200, hls_status: 404 }; } async close() { this.running = false; } } async function verifyArtifactManager(tempRoot) { assert.throws(() => validateManifestEntry({ id: "bad", url: "http://example.invalid/a.zip", sha256: "0".repeat(64) }), /invalid/); assert.throws(() => safeArchivePath("../escape.exe"), /unsafe path/); const zip = new AdmZip(); zip.addFile("LICENSE", Buffer.from("MIT")); zip.addFile("mediamtx.exe", Buffer.from("fake executable")); zip.addFile("mediamtx.yml", Buffer.from("paths: {}")); const archive = zip.toBuffer(); const entry = { id: "fake-mediamtx", install_id: "mediamtx", backend: "mediamtx", version: "v1.18.2", url: "https://example.invalid/mediamtx.zip", filename: "mediamtx.zip", sha256: crypto.createHash("sha256").update(archive).digest("hex"), bytes: archive.length, max_bytes: archive.length + 1, max_extracted_bytes: 1024, expected_entries: ["LICENSE", "mediamtx.exe", "mediamtx.yml"] }; const manager = new ArtifactManager(path.join(tempRoot, "artifacts"), { fetch: async () => new Response(archive, { status: 200, headers: { "Content-Length": String(archive.length) } }) }); await assert.rejects(() => manager.download(entry), /explicit setup confirmation/); const downloaded = await manager.download(entry, { confirmed: true }); assert(downloaded.valid, "downloaded artifact must match its pinned SHA-256"); const installed = manager.installZip(entry, downloaded.path, { verify: (staged) => assert(fs.existsSync(path.join(staged, "mediamtx.exe"))) }); assert(fs.existsSync(path.join(installed.path, "mediamtx.exe")), "validated archive must install atomically"); const originalExecutable = fs.readFileSync(path.join(installed.path, "mediamtx.exe"), "utf8"); const replacementZip = new AdmZip(); replacementZip.addFile("LICENSE", Buffer.from("MIT replacement")); replacementZip.addFile("mediamtx.exe", Buffer.from("broken replacement")); replacementZip.addFile("mediamtx.yml", Buffer.from("paths: {}")); const replacementArchive = replacementZip.toBuffer(); const replacementPath = path.join(tempRoot, "replacement.zip"); fs.writeFileSync(replacementPath, replacementArchive); const replacementEntry = { ...entry, sha256: crypto.createHash("sha256").update(replacementArchive).digest("hex"), bytes: replacementArchive.length }; assert.throws( () => manager.installZip(replacementEntry, replacementPath, { verifyInstalled: () => { throw new Error("replacement probe failed"); } }), /replacement probe failed/ ); assert.equal( fs.readFileSync(path.join(installed.path, "mediamtx.exe"), "utf8"), originalExecutable, "a failed post-install probe must restore the last known-good runtime" ); assert.throws( () => manager.installZip({ ...entry, sha256: "0".repeat(64) }, downloaded.path), /Checksum mismatch/ ); } async function verifyManagedRuntime(tempRoot) { const runtimeRoot = path.join(tempRoot, "runtime"); const executable = path.join(runtimeRoot, "mediamtx", "mediamtx.exe"); fs.mkdirSync(path.dirname(executable), { recursive: true }); fs.writeFileSync(executable, "fake"); const fixture = path.join(root, "scripts", "fixtures", "fake-mediamtx.js"); const runtime = new MediaMtxRuntime({ runtimeRoot, configRoot: path.join(tempRoot, "config"), platform: "win32", arch: "x64", spawn: (_executable, args, options) => spawn(process.execPath, [fixture, args[0]], options), spawnSync: () => ({ status: 0, stdout: "v1.18.2\n", stderr: "" }) }); await assert.rejects(() => runtime.install(), /administrator confirmation/); await assert.rejects(() => runtime.repair(), /administrator confirmation/); assert.equal(runtime.probeExecutable(executable).version, "v1.18.2", "manual executable probes must remain bounded and versioned"); const health = await runtime.healthCheck(); assert(health.ok && health.restart_verified && health.api_status === 200 && health.metrics_status === 200, "fake MediaMTX listeners and supervised restart must pass the runtime health check"); const session = { path: "lumi-test/00000000-0000-4000-8000-000000000001", username: "private-user", password: "private-password", transport: "rtmp", ingestPort: 29350 }; await runtime.configureSession(session); const generated = fs.readFileSync(runtime.configPath, "utf8"); assert(!generated.includes(session.username) && !generated.includes(session.password), "generated MediaMTX config must not persist plaintext publisher credentials"); assert(generated.includes(sha256Credential(session.username)) && generated.includes(sha256Credential(session.password))); assert.match(generated, /rtsp: false/); assert.match(generated, /webrtc: false/); assert.match(generated, /srt: false/); assert.match(generated, /hlsVariant: lowLatency/); assert.match(generated, /maxReaders: 4/); assert.match(generated, /rtmpAddress: "127\.0\.0\.1:29350"/); const diagnostic = await runtime.diagnostics(session.path); assert.equal(diagnostic.path.name, session.path); const playlist = await runtime.fetchHls(session.path, "index.m3u8"); assert.equal(playlist.status, 200); assert.match(await playlist.text(), /segment0\.mp4/); assert.throws(() => validateHlsAsset("../secret"), /Invalid media path/); await runtime.clearSession(); const denyConfig = fs.readFileSync(runtime.configPath, "utf8"); assert.match(denyConfig, /paths: \{\}/); assert(!denyConfig.includes("private-user")); await runtime.close(); } async function verifyStreamService() { const old = { host: process.env.LUMI_STREAM_TEST_INGEST_HOST, transport: process.env.LUMI_STREAM_TEST_TRANSPORT, insecure: process.env.LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE }; delete process.env.LUMI_STREAM_TEST_INGEST_HOST; process.env.LUMI_STREAM_TEST_TRANSPORT = "rtmp"; delete process.env.LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE; const runtime = new FakeRuntime(); const service = new StreamTestingService({ runtime, timer: false }); const sent = []; try { const created = await service.create( { id: "device-1", name: "Studio PC", pairing_host: "http://localhost:3000" }, { source: { width: 2560, height: 1440, fps: 60 } }, (type, payload) => sent.push({ type, payload }) ); assert.match(created.ingest.server, /^rtmp:\/\/localhost:19350\/lumi-test$/); assert.match(created.ingest.key, /^[0-9a-f-]{36}\?user=/); assert.equal(created.source.variants.length, 1, "MediaMTX handoff must preserve source quality without a synthetic ladder"); assert.equal(runtime.session.path, `lumi-test/${created.id}`); assert(runtime.session.username && runtime.session.password); assert.equal(runtime.session.bindHost, "127.0.0.1", "LAN-only RTMP must bind to the resolved private interface"); await assert.rejects( () => service.create({ id: "device-2", pairing_host: "http://localhost:3000" }, {}), (error) => error.code === "STREAM_TEST_ACTIVE" ); await service.sweep(); const status = service.publicStatus(); assert.equal(status.session.state, "receiving"); assert.equal(status.session.metrics.receiver.path_ready, true); assert(Object.hasOwn(status.receiver.process, "log_tail"), "admin diagnostics must include a bounded sanitized MediaMTX log tail"); assert(!JSON.stringify(status).includes("127.0.0.1:"), "browser status must not expose loopback MediaMTX origins"); service.updateObs("device-1", { session_id: created.id, bitrate_kbps: 6000, dropped_frames: 2, total_frames: 10000, congestion: 0 }); service.addCaption("device-1", { session_id: created.id, text: "Private caption", delay_ms: 120 }); assert.throws( () => service.addCaption("device-2", { session_id: created.id, text: "Wrong device" }), /not active for this Companion/ ); service.reportPlayer({ session_id: created.id, latency_seconds: 1.2, buffer_seconds: 1.5, stalls: 0, errors: 0 }); assert.match(service.captionFile(created.id), /Private caption/); const endedSession = service.active; await service.stop({ session_id: created.id, reason: "Verification complete." }); assert.equal(runtime.cleared, 1); assert.equal(endedSession.username, null); assert.equal(endedSession.password, null); assert.equal(sent.at(-1).type, "stream_test_ended"); await assert.rejects( () => service.create({ id: "device-1", pairing_host: "http://localhost:3000" }, {}), (error) => error.code === "STREAM_TEST_RATE_LIMIT" ); const expiring = await service.create( { id: "device-2", pairing_host: "http://localhost:3000" }, {} ); service.active.expiresAt = Date.now() - 1; await service.sweep(); assert.equal(service.active, null, "session expiry must revoke the MediaMTX path and end the session"); const inactive = await service.create( { id: "device-3", pairing_host: "http://localhost:3000" }, {} ); service.active.lastInboundBytes = runtime.inboundBytes; service.active.lastMediaAt = Date.now() - INACTIVITY_MS - 1; await service.sweep(); assert.equal(service.active, null, "stalled inbound bytes must trigger the inactivity recovery path"); const failed = await service.create( { id: "device-4", pairing_host: "http://localhost:3000" }, {} ); runtime.emit("failed", new Error("fixture process exited")); await new Promise((resolve) => setTimeout(resolve, 10)); assert.equal(service.active, null, "an unexpected MediaMTX exit must end the session and request OBS recovery"); assert([created.id, expiring.id, inactive.id, failed.id].every(Boolean)); } finally { await service.close(); if (old.host === undefined) delete process.env.LUMI_STREAM_TEST_INGEST_HOST; else process.env.LUMI_STREAM_TEST_INGEST_HOST = old.host; if (old.transport === undefined) delete process.env.LUMI_STREAM_TEST_TRANSPORT; else process.env.LUMI_STREAM_TEST_TRANSPORT = old.transport; if (old.insecure === undefined) delete process.env.LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE; else process.env.LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE = old.insecure; } } async function main() { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-stream-testing-")); try { assert.equal(validateHostname("stream.example.com"), "stream.example.com"); assert.equal(validateHostname("https://stream.example.com"), ""); assert.equal(validateHostname("[::1]"), "[::1]"); assert(isPrivateAddress("127.0.0.1") && isPrivateAddress("192.168.1.20") && !isPrivateAddress("8.8.8.8")); assert.deepStrictEqual(sourceFor({ width: 1920, height: 1080, fps: 60 }).variants.map((item) => item.name), ["source"]); assert.match(rewriteManifest( "#EXTM3U\n#EXT-X-MAP:URI=\"init.mp4\"\nsegment0.mp4\n", "/admin/stream-testing/media/test/", "lumi-test/test" ), /URI="\/admin\/stream-testing\/media\/test\/init\.mp4"[\s\S]*\/admin\/stream-testing\/media\/test\/segment0\.mp4/); assert.throws(() => rewriteManifest("../secret", "/proxy/", "lumi-test/test"), /unsafe playlist URI/); assert.throws(() => rewriteManifest("https://foreign.invalid/segment.mp4", "/proxy/", "lumi-test/test"), /foreign playlist URI/); assert.deepStrictEqual( parseMetrics('paths_inbound_bytes{name="lumi-test/id"} 42\nunapproved_metric 9000\n', "lumi-test/id"), { paths_inbound_bytes: 42 } ); await assert.rejects( () => readResponseText(new Response("x".repeat(32), { headers: { "Content-Length": "32" } }), 16), /oversized diagnostic response/ ); const config = renderConfig({ apiPort: 10001, metricsPort: 10002, hlsPort: 10003, session: { path: "lumi-test/id", username: "user", password: "pass", transport: "rtmp", ingestPort: 19350 } }); assert(!config.includes('\n pass: "pass"') && config.includes("sha256:")); assert(!config.includes("ffmpeg")); assert.match(config, /record: false/); assert.match(config, /maxReaders: 4/); assert.match(config, /paths:\n "lumi-test\/id":/); assert(!/action: publish[\s\S]*path: all/.test(config)); const oldNetwork = { host: process.env.LUMI_STREAM_TEST_INGEST_HOST, transport: process.env.LUMI_STREAM_TEST_TRANSPORT, cert: process.env.LUMI_STREAM_TEST_TLS_CERT, key: process.env.LUMI_STREAM_TEST_TLS_KEY, insecure: process.env.LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE }; try { delete process.env.LUMI_STREAM_TEST_INGEST_HOST; process.env.LUMI_STREAM_TEST_TRANSPORT = "rtmp"; delete process.env.LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE; await assert.rejects( () => resolveIngestConfiguration({ pairing_host: "https://8.8.8.8" }), (error) => error.code === "STREAM_TEST_INSECURE_REMOTE" ); const cert = path.join(tempRoot, "cert.pem"); const key = path.join(tempRoot, "key.pem"); fs.writeFileSync(cert, "test certificate"); fs.writeFileSync(key, "test key"); process.env.LUMI_STREAM_TEST_TRANSPORT = "rtmps"; process.env.LUMI_STREAM_TEST_TLS_CERT = cert; process.env.LUMI_STREAM_TEST_TLS_KEY = key; const secure = await resolveIngestConfiguration({ pairing_host: "https://lumi.example.test" }); assert.equal(secure.transport, "rtmps"); assert.equal(secure.host, "lumi.example.test"); } finally { for (const [key, value] of Object.entries({ LUMI_STREAM_TEST_INGEST_HOST: oldNetwork.host, LUMI_STREAM_TEST_TRANSPORT: oldNetwork.transport, LUMI_STREAM_TEST_TLS_CERT: oldNetwork.cert, LUMI_STREAM_TEST_TLS_KEY: oldNetwork.key, LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE: oldNetwork.insecure })) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } } for (const type of ["stream_test_create", "stream_test_obs_metrics", "stream_test_caption", "stream_test_stop"]) { const parsed = protocol.parseEnvelope(Buffer.from(JSON.stringify(protocol.envelope(type, {}, null)))); assert.strictEqual(parsed.type, type); } await verifyArtifactManager(tempRoot); await verifyManagedRuntime(tempRoot); await verifyStreamService(); const server = fs.readFileSync(path.join(root, "src/web/server.js"), "utf8"); const service = fs.readFileSync(path.join(root, "src/services/stream-testing.js"), "utf8"); const runtime = fs.readFileSync(path.join(root, "src/services/mediamtx-runtime.js"), "utf8"); const gateway = fs.readFileSync(path.join(root, "plugins/lumi_transcription/backend/companion/gateway.js"), "utf8"); const nativeBridge = fs.readFileSync(path.join(root, "companion/native/obs-bridge/src/plugin.cpp"), "utf8"); const companion = fs.readFileSync(path.join(root, "companion/src/Lumi.Companion.App/CompanionRuntime.cs"), "utf8"); const webUi = fs.readFileSync(path.join(root, "src/web/views/admin-stream-testing.ejs"), "utf8"); assert.match(server, /admin\/stream-testing\/runtime\/install/); assert.match(server, /admin\/stream-testing\/media\/:id\/\*/); assert.match(server, /admin\/stream-testing\/media\/:id\/\*[\s\S]{0,500}requireRole\("admin"\)/); assert.match(server, /streamTestingService\.proxyMedia/); assert.match(service, /INACTIVITY_MS/); assert.match(service, /caption_delay/); assert.match(service, /LUMI_STREAM_TEST_ALLOW_INSECURE_REMOTE/); assert(!/FFMPEG|ffmpegArgs|h264_nvenc|libx264/.test(service)); assert(!/shell:\s*true/.test(runtime)); assert.match(gateway, /await service\.create/); assert.match(gateway, /Companion disconnected; the private receiver was stopped/); assert.match(nativeBridge, /stream_test_snapshot/); assert.match(nativeBridge, /stream_test_restore/); assert.match(nativeBridge, /obs_process_id/); assert.match(nativeBridge, /obs_output_start\(output\)/); assert(!nativeBridge.includes("obs_frontend_streaming_start();")); assert(!nativeBridge.includes("obs_encoder_release(encoder);")); assert.match(companion, /StreamTestRecoveryStore/); assert.match(companion, /RestoreObsAfterStreamTestAsync/); assert.match(companion, /StreamTestObsRestartRequired/); assert.match(companion, /LastObsProcessId/); assert.match(companion, /Restart OBS before trying again/); assert.match(webUi, /data-runtime-install/); assert.match(webUi, /data-runtime-health/); assert.match(webUi, /layout-bottom[\s\S]*admin\/stream-testing\/hls\.js[\s\S]*stream-testing\.js/); assert(!webUi.includes("Run test pattern")); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); const { streamTestingService } = require("../src/services/stream-testing"); await streamTestingService.close(); } console.log("Managed MediaMTX Stream Testing verification passed."); } main().catch((error) => { console.error(error); process.exitCode = 1; });