// Die Leitung zum Hub, Seite des Stream-PCs. import { test } from 'node:test'; import assert from 'node:assert/strict'; import type { ObsClient } from '#obs/client.ts'; import { befehlAus, einrichtenAus, SOUND_QUELLE, soundQuelleEinrichten, sseZerleger, standLesen, TON_SZENE, } from './fern.ts'; test('SSE: Nachrichten, die zerteilt oder zu zweit ankommen', () => { const z = sseZerleger(); assert.deepEqual(z('retry: 2000\n\n'), [], 'retry allein ist keine Nachricht'); assert.deepEqual(z('data: {"a"'), []); assert.deepEqual(z(':1}\n\ndata: 2\n\nda'), ['{"a":1}', '2']); assert.deepEqual(z('ta: 3\r\n\r\n'), ['3'], 'auch mit Windows-Zeilenenden'); }); test('nur bekannte Befehle mit ID werden ausgefuehrt', () => { assert.deepEqual(befehlAus({ id: 'x', art: 'szene', szene: 'Pause' }), { id: 'x', art: 'szene', szene: 'Pause' }); assert.deepEqual(befehlAus({ id: 'x', art: 'stumm', quelle: 'Mikro', stumm: true }), { id: 'x', art: 'stumm', quelle: 'Mikro', stumm: true }); assert.equal(befehlAus({ art: 'clip' }), null, 'ohne ID gibt es niemanden, dem man antwortet'); assert.equal(befehlAus({ id: 'x', art: 'beat' }), null); assert.equal(befehlAus({ id: 'x', art: 'StopStream' }), null); }); function falschesObs(antworten: Record) => unknown>): ObsClient { return { run: async () => {}, isConnected: () => true, request: async (type: string, data?: unknown): Promise => { const f = antworten[type]; if (!f) throw new Error(`OBS lehnt ab (604): ${type}`); return f((data ?? {}) as Record) as T; }, }; } test('Stand: Szenen in OBS-Reihenfolge, nur Eingaenge mit Ton', async () => { const obs = falschesObs({ GetSceneList: () => ({ currentProgramSceneName: 'Gameplay', // OBS zaehlt von unten: Index 0 steht im Fenster ganz unten. scenes: [{ sceneName: 'Pause', sceneIndex: 0 }, { sceneName: 'Gameplay', sceneIndex: 2 }, { sceneName: 'Just Chatting', sceneIndex: 1 }], }), GetInputList: () => ({ inputs: [{ inputName: 'Mikro' }, { inputName: 'Facecam' }, { inputName: 'Desktop' }] }), GetInputMute: (d) => { if (d['inputName'] === 'Facecam') throw new Error('kein Ton'); return { inputMuted: d['inputName'] === 'Desktop' }; }, GetReplayBufferStatus: () => ({ outputActive: true }), }); assert.deepEqual(await standLesen(obs), { obs: true, szenen: ['Gameplay', 'Just Chatting', 'Pause'], aktuell: 'Gameplay', ton: [{ name: 'Mikro', stumm: false }, { name: 'Desktop', stumm: true }], puffer: true, }); }); test('Stand ohne Wiedergabepuffer: kein Clip-Knopf, aber Szenen', async () => { const obs = falschesObs({ GetSceneList: () => ({ currentProgramSceneName: 'A', scenes: [{ sceneName: 'A', sceneIndex: 0 }] }), GetInputList: () => ({ inputs: [] }), }); const s = await standLesen(obs); assert.equal(s.puffer, false); assert.deepEqual(s.szenen, ['A']); }); test('nur eine Adresse auf die Sound-Seite wird als Browser-Source angelegt', () => { assert.deepEqual(einrichtenAus({ art: 'einrichten', soundsUrl: 'https://abdu.dimension47.de/sounds?key=4OB0-x_y' }), { art: 'einrichten', soundsUrl: 'https://abdu.dimension47.de/sounds?key=4OB0-x_y' }); assert.equal(einrichtenAus({ art: 'einrichten', soundsUrl: 'https://boese.example/irgendwas' }), null); assert.equal(einrichtenAus({ art: 'einrichten', soundsUrl: 'javascript:alert(1)//sounds?key=a' }), null); assert.equal(einrichtenAus({ art: 'szene', soundsUrl: 'https://a/sounds?key=a' }), null); }); /** OBS zum Mitschreiben: Szenen, Quellen und was darin steckt. */ function obsMitSzenen(szenen: string[]) { const inhalt = new Map(szenen.map((s) => [s, []])); const quellen = new Map>(); const aufrufe: string[] = []; const obs: ObsClient = { run: async () => {}, isConnected: () => true, request: async (type: string, data?: unknown): Promise => { const d = (data ?? {}) as Record; aufrufe.push(type); switch (type) { case 'GetSceneList': return { scenes: [...inhalt.keys()].map((sceneName) => ({ sceneName })) } as T; case 'CreateScene': inhalt.set(String(d['sceneName']), []); return {} as T; case 'GetInputSettings': { const q = quellen.get(String(d['inputName'])); if (!q) throw new Error('OBS lehnt ab (600): keine Quelle'); return { inputSettings: q } as T; } case 'CreateInput': quellen.set(String(d['inputName']), d['inputSettings'] as Record); inhalt.get(String(d['sceneName']))!.push(String(d['inputName'])); return {} as T; case 'SetInputSettings': Object.assign(quellen.get(String(d['inputName']))!, d['inputSettings']); return {} as T; case 'GetSceneItemList': return { sceneItems: inhalt.get(String(d['sceneName']))!.map((sourceName) => ({ sourceName })) } as T; case 'CreateSceneItem': inhalt.get(String(d['sceneName']))!.push(String(d['sourceName'])); return {} as T; } throw new Error(`unerwartet: ${type}`); }, }; return { obs, inhalt, quellen, aufrufe }; } test('Sound-Quelle: einmal angelegt, in jeder Szene, ein zweiter Lauf aendert nichts', async () => { const o = obsMitSzenen(['Gameplay', 'Pause']); const url = 'https://hub/sounds?key=a'; const getan = await soundQuelleEinrichten(o.obs, url); assert.equal(getan.length, 4); assert.deepEqual(o.inhalt.get(TON_SZENE), [SOUND_QUELLE]); assert.deepEqual(o.inhalt.get('Gameplay'), [TON_SZENE]); assert.deepEqual(o.inhalt.get('Pause'), [TON_SZENE]); const q = o.quellen.get(SOUND_QUELLE)!; assert.equal(q['reroute_audio'], true, 'sonst geht der Ton am Stream vorbei'); assert.equal(q['shutdown'], false); assert.deepEqual(await soundQuelleEinrichten(o.obs, url), []); assert.equal(o.aufrufe.filter((a) => a.startsWith('Create')).length, 4); }); test('Sound-Quelle: neue Szene und neuer Schluessel werden nachgezogen', async () => { const o = obsMitSzenen(['Gameplay']); await soundQuelleEinrichten(o.obs, 'https://hub/sounds?key=alt'); o.inhalt.set('Just Chatting', []); const getan = await soundQuelleEinrichten(o.obs, 'https://hub/sounds?key=neu'); assert.deepEqual(getan, [`Adresse von "${SOUND_QUELLE}" erneuert`, 'in "Just Chatting" eingehaengt']); assert.equal(o.quellen.get(SOUND_QUELLE)!['url'], 'https://hub/sounds?key=neu'); }); test('die eigene Ton-Szene steht nicht in der Szenenauswahl', async () => { const obs = falschesObs({ GetSceneList: () => ({ currentProgramSceneName: 'A', scenes: [ { sceneName: 'A', sceneIndex: 1 }, { sceneName: TON_SZENE, sceneIndex: 0 }] }), GetInputList: () => ({ inputs: [] }), }); assert.deepEqual((await standLesen(obs)).szenen, ['A']); });