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:
1 parent
684f10eb02
commit
0e020f3073
11 files changed
+597
-27
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import type { LayoutProfile } from '#core/config.ts';
|
||||
import { parseProbe, parseProgress } from './ffmpeg.ts';
|
||||
import { micTrackArgs, verticalArgs, verticalFilter } from './vertical.ts';
|
||||
|
||||
// Das eingemessene Profil aus config/layout.yaml
|
||||
const PROFILE: LayoutProfile = {
|
||||
cam: { crop: { x: 0, y: 100, w: 440, h: 208 }, band: { w: 1080, h: 512 } },
|
||||
game: { crop: { x: 440, y: 0, w: 828, h: 1080 }, band: { w: 1080, h: 1408 } },
|
||||
};
|
||||
|
||||
test('Filtergraph nutzt die eingemessenen Werte', () => {
|
||||
const f = verticalFilter(PROFILE);
|
||||
assert.match(f, /crop=440:208:0:100/);
|
||||
assert.match(f, /crop=828:1080:440:0/);
|
||||
assert.match(f, /scale=1080:512/);
|
||||
assert.match(f, /scale=1080:1408/);
|
||||
assert.match(f, /vstack=inputs=2/);
|
||||
// setsar verhindert, dass ein krummes Pixelseitenverhaeltnis durchschlaegt
|
||||
assert.equal((f.match(/setsar=1/g) ?? []).length, 2);
|
||||
});
|
||||
|
||||
test('Baender ergeben zusammen die Zielhoehe', () => {
|
||||
assert.equal(PROFILE.cam.band.h + PROFILE.game.band.h, 1920);
|
||||
});
|
||||
|
||||
test('-sseof steht vor -i, sonst wird die ganze Datei dekodiert', () => {
|
||||
const args = verticalArgs({
|
||||
input: '/work/raw.mkv', output: '/out/v.mp4', profile: PROFILE, tailSeconds: 45,
|
||||
});
|
||||
const sseof = args.indexOf('-sseof');
|
||||
const input = args.indexOf('-i');
|
||||
assert.ok(sseof !== -1, '-sseof fehlt');
|
||||
assert.ok(sseof < input, '-sseof muss vor -i stehen');
|
||||
assert.equal(args[sseof + 1], '-45');
|
||||
});
|
||||
|
||||
test('ohne tailSeconds kein -sseof', () => {
|
||||
const args = verticalArgs({ input: 'a.mkv', output: 'b.mp4', profile: PROFILE });
|
||||
assert.equal(args.includes('-sseof'), false);
|
||||
});
|
||||
|
||||
test('Tonspur wird gewaehlt und darf fehlen', () => {
|
||||
const args = verticalArgs({
|
||||
input: 'a.mkv', output: 'b.mp4', profile: PROFILE, audioTrack: 2,
|
||||
});
|
||||
// Das Fragezeichen macht die Zuordnung optional - ohne das bricht ffmpeg ab,
|
||||
// wenn die Spur nicht existiert
|
||||
assert.ok(args.includes('0:a:2?'), `-map fehlt: ${args.join(' ')}`);
|
||||
});
|
||||
|
||||
test('Ausgabe ist fuer Handys geeignet kodiert', () => {
|
||||
const args = verticalArgs({ input: 'a.mkv', output: 'b.mp4', profile: PROFILE });
|
||||
assert.ok(args.includes('yuv420p'), 'ohne yuv420p spielen viele Player nichts ab');
|
||||
assert.ok(args.includes('+faststart'), 'ohne faststart laedt das Video traege');
|
||||
});
|
||||
|
||||
test('Mikrofonspur wird als 16-kHz-Mono-WAV gezogen', () => {
|
||||
const args = micTrackArgs('/work/raw.mkv', '/work/mic.wav', 1);
|
||||
assert.ok(args.includes('0:a:1'));
|
||||
assert.ok(args.includes('16000'));
|
||||
assert.ok(args.includes('pcm_s16le'));
|
||||
assert.deepEqual(args.slice(-3), ['-c:a', 'pcm_s16le', '/work/mic.wav']);
|
||||
});
|
||||
|
||||
test('liest den Fortschritt aus der ffmpeg-Ausgabe', () => {
|
||||
assert.equal(parseProgress('frame=120\nout_time_ms=4500000\nspeed=1.2x'), 4.5);
|
||||
assert.equal(parseProgress('frame=120\nspeed=1.2x'), null);
|
||||
});
|
||||
|
||||
test('erkennt Aufloesung, Laenge und mehrere Tonspuren', () => {
|
||||
// So sieht eine OBS-Aufnahme mit getrennten Spuren aus
|
||||
const json = JSON.stringify({
|
||||
format: { duration: '92.416000' },
|
||||
streams: [
|
||||
{ index: 0, codec_type: 'video', codec_name: 'h264', width: 1920, height: 1080 },
|
||||
{ index: 1, codec_type: 'audio', codec_name: 'aac', channels: 2, tags: { title: 'Mischung' } },
|
||||
{ index: 2, codec_type: 'audio', codec_name: 'aac', channels: 1, tags: { title: 'Mikrofon' } },
|
||||
{ index: 3, codec_type: 'audio', codec_name: 'aac', channels: 2, tags: { title: 'Spiel' } },
|
||||
],
|
||||
});
|
||||
const r = parseProbe(json);
|
||||
assert.equal(r.width, 1920);
|
||||
assert.equal(r.height, 1080);
|
||||
assert.ok(Math.abs(r.durationSeconds - 92.416) < 0.001);
|
||||
assert.equal(r.audio.length, 3);
|
||||
// Wichtig: die Nummerierung fuer -map 0:a:N zaehlt nur die Audiospuren,
|
||||
// nicht die Streams insgesamt
|
||||
assert.deepEqual(r.audio.map((a) => a.index), [0, 1, 2]);
|
||||
assert.equal(r.audio[1]?.title, 'Mikrofon');
|
||||
});
|
||||
|
||||
test('kommt mit einer Datei ohne Ton klar', () => {
|
||||
const r = parseProbe(JSON.stringify({
|
||||
format: { duration: '10' },
|
||||
streams: [{ index: 0, codec_type: 'video', width: 1280, height: 720 }],
|
||||
}));
|
||||
assert.equal(r.audio.length, 0);
|
||||
assert.equal(r.width, 1280);
|
||||
});
|
||||
Reference in new issue
Block a user