Pult: Stream-PC per Tipp freigeben, Sound-Quelle legt er selbst in OBS an
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>
This commit is contained in:
ZeasyAlexandClaude Opus 5.5 committed 2026-10-26 13:03:06 +01:00
1 parent 32416d1916
commit 54dbe1265b
15 files changed
+675 -50

No files matched your search

+83 -1
View File
@@ -3,7 +3,9 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import type { ObsClient } from '#obs/client.ts';
import { befehlAus, sseZerleger, standLesen } from './fern.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();
@@ -67,3 +69,83 @@ test('Stand ohne Wiedergabepuffer: kein Clip-Knopf, aber Szenen', async () => {
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']);
});