Pult: Handy als Stream Deck mit Soundboard und Szenenwechsel
Paket bauen und veroeffentlichen / paket (push) Failing after 1m21s
Paket bauen und veroeffentlichen / paket (push) Failing after 1m21s
- /pult als installierbare Web-App (Manifest, Service Worker, Symbole) - Soundboard: Sounds vom Handy hochladen, bearbeiten, im Stream spielen ueber die neue Browser-Source /sounds - Szenen, Clip und Stummschalter: der Stream-PC waehlt sich mit eigenem Schluessel beim Hub ein (fern:verbinden), Befehle warten auf OBS - Browser-Sources melden, welche Seite sie sind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70448967f5
commit
8be195d938
29 files changed
+2935
-28
No files matched your search
@@ -0,0 +1,69 @@
|
||||
// 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, sseZerleger, standLesen } 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']);
|
||||
});
|
||||
Reference in new issue
Block a user