Paket bauen und veroeffentlichen / paket (push) Successful in 42s
- ingest wuerfelt beim ersten Start einen Schluessel und meldet sich beim Hub; der antwortet 202, bis im Pult jemand auf Freigeben tippt - nach dem Verbinden schickt der Hub die Adresse von /sounds, ingest legt Szene "abdulive Ton" mit der Browser-Source an und haengt sie in jede Szene - fern:verbinden bleibt als Weg von Hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
6.8 KiB
TypeScript
152 lines
6.8 KiB
TypeScript
// 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<string, (d: Record<string, unknown>) => unknown>): ObsClient {
|
|
return {
|
|
run: async () => {},
|
|
isConnected: () => true,
|
|
request: async <T>(type: string, data?: unknown): Promise<T> => {
|
|
const f = antworten[type];
|
|
if (!f) throw new Error(`OBS lehnt ab (604): ${type}`);
|
|
return f((data ?? {}) as Record<string, unknown>) 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<string, string[]>(szenen.map((s) => [s, []]));
|
|
const quellen = new Map<string, Record<string, unknown>>();
|
|
const aufrufe: string[] = [];
|
|
const obs: ObsClient = {
|
|
run: async () => {},
|
|
isConnected: () => true,
|
|
request: async <T>(type: string, data?: unknown): Promise<T> => {
|
|
const d = (data ?? {}) as Record<string, unknown>;
|
|
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<string, unknown>);
|
|
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']);
|
|
});
|