S3: 9:16-Render mit Warteschlange, nachrangig zum Stream

Aus der Rohdatei entsteht das fertige Hochformat. Der Filtergraph liegt jetzt
in packages/media und wird von calibrate und vom Render-Schritt gemeinsam
benutzt - die Zahlen darin sind eingemessen, sie gehoeren an genau eine Stelle.

Eine Aufgabe nach der anderen, und waehrend gesendet wird standardmaessig
gar nicht (renderWhileLive: false). ffmpeg laeuft unter nice, mit Zeitlimit,
und die letzten Zeilen von stderr landen in der Fehlermeldung statt im Nichts.
Aus dem 90-Sekunden-Puffer werden die letzten 45 Sekunden genommen (-sseof vor
-i, sonst wird die ganze Datei dekodiert und danach verworfen).

Dabei einen eigenen Fehler aus S2 korrigiert: die Rohdatei wurde in den
Windows-Bind-Mount kopiert und dort verarbeitet - genau das, was ADR-0004
Punkt 2 verbietet. Jetzt liegt sie im Arbeits-Volume, auf die Windows-Seite
kommt nur das fertige, kleine Ergebnis.

Verifiziert an einer kuenstlichen Aufnahme mit drei getrennten Tonspuren:
1920x1080/70 s hinein, 1080x1920/45 s heraus, Naht an der richtigen Stelle,
27,9 s Rechenzeit unter nice 15. 39 Tests gruen.

Neuer Befehl `render <datei>` zum Nachrendern nach Layout-Aenderungen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-09 10:21:58 +02:00
1 parent 684f10eb02
commit 0e020f3073
11 files changed
+597 -27

No files matched your search

+26 -1
View File
@@ -5,7 +5,7 @@
// EventSub kommt spaeter - fuer den Clip-Weg reicht IRC plus Polling.
// Siehe docs/projects/00-ingest.md.
import { loadConfig, paths } from '#core/config.ts';
import { loadConfig, loadLayout, paths } from '#core/config.ts';
import { logger } from '#core/logger.ts';
import { sleep } from '#core/retry.ts';
import { openDb, closeDb } from '#db/index.ts';
@@ -15,6 +15,7 @@ import { helix } from '#twitch/helix.ts';
import { createIrc } from '#twitch/irc.ts';
import { readSecrets } from '#twitch/secrets.ts';
import { createClipper } from './clips.ts';
import { createRenderer } from './render.ts';
import { createSpikeDetector } from './spike.ts';
import { startServer } from './server.ts';
@@ -66,6 +67,26 @@ export async function runIngest(): Promise<void> {
}
);
const layout = loadLayout();
const profile = layout.profiles['default'];
if (!profile) throw new Error('config/layout.yaml hat kein Profil "default"');
const renderer = createRenderer({
cfg: {
profile,
tailSeconds: cfg.clipper.tailSeconds,
outputTrack: cfg.clipper.audio.outputTrack,
encoder: cfg.clipper.ffmpeg.encoder,
preset: cfg.clipper.ffmpeg.preset,
crf: cfg.clipper.ffmpeg.crf,
niceness: cfg.clipper.ffmpeg.niceness,
renderWhileLive: cfg.clipper.renderWhileLive,
},
repo,
workDir: paths.work,
isLive: () => state.streamId !== null,
});
const clipper = createClipper({
cfg: {
enabled: cfg.clipper.enabled,
@@ -76,12 +97,14 @@ export async function runIngest(): Promise<void> {
...(process.env.OBS_REPLAY_DIR ? { replayDirHost: process.env.OBS_REPLAY_DIR } : {}),
replayDirContainer: paths.replay,
clipsDir: paths.clips,
workDir: paths.work,
},
repo,
obs,
say: (text) => irc.say(text),
currentStreamId: () => state.streamId,
broadcasterId: () => state.broadcasterId,
onRawReady: (job) => renderer.enqueue(job),
});
startServer({
@@ -92,6 +115,8 @@ export async function runIngest(): Promise<void> {
db: repo.stats(),
spikeArmed: spikes.isArmed(),
clipsWaitingForFile: clipper.pendingCount(),
renderQueue: renderer.pending(),
rendering: renderer.currentJob(),
note: state.chatAnonymous
? 'Chat wird anonym mitgelesen. Zum Senden und fuer Helix: Einrichtung unter /setup.'
: undefined,