Länge von Browser-Aufnahmen ohne Kopfangabe ausmessen

WebM aus dem MediaRecorder nennt oft keine Gesamtlänge; ffprobe liest
sie dann aus dem letzten Paket. Videos ohne lesbare Länge werden als
Fehler markiert, weil sich ihr Tempo sonst nicht berechnen lässt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Opus 5.5 committed 2026-09-29 11:00:59 +00:00
1 parent 6e497ffc45
commit 18d5b80959
3 files changed
+32 -2

No files matched your search

+21 -2
View File
@@ -16,6 +16,21 @@ function readRotation(stream) {
return 0; return 0;
} }
// Länge aus dem Ende des letzten Pakets, wenn der Dateikopf keine nennt.
async function measureDuration(file) {
const { stdout } = await execFileAsync(
'ffprobe',
['-v', 'error', '-show_entries', 'packet=pts_time,duration_time', '-of', 'csv=p=0', file],
{ maxBuffer: 64 * 1024 * 1024 },
);
let end = 0;
for (const line of stdout.split('\n')) {
const [pts, duration] = line.split(',').map(Number);
if (Number.isFinite(pts)) end = Math.max(end, pts + (Number.isFinite(duration) ? duration : 0));
}
return end > 0 ? end : null;
}
// Liefert die Eckdaten einer Video- oder Audiodatei; Breite und Höhe so, // Liefert die Eckdaten einer Video- oder Audiodatei; Breite und Höhe so,
// wie das Video angezeigt wird. // wie das Video angezeigt wird.
export async function probe(file) { export async function probe(file) {
@@ -28,10 +43,14 @@ export async function probe(file) {
const video = info.streams.find( const video = info.streams.find(
(stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic, (stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic,
); );
const hasAudio = info.streams.some((stream) => stream.codec_type === 'audio'); const audio = info.streams.find((stream) => stream.codec_type === 'audio');
const hasAudio = Boolean(audio);
if (!video && !hasAudio) throw new Error('Weder Bild noch Ton gefunden.'); if (!video && !hasAudio) throw new Error('Weder Bild noch Ton gefunden.');
const duration = Number(info.format?.duration ?? video?.duration ?? 0); // Browser-Aufnahmen (WebM) tragen oft keine Gesamtlänge im Kopf.
const duration =
[info.format?.duration, video?.duration, audio?.duration].map(Number).find(Number.isFinite) ??
(await measureDuration(file));
if (!video) { if (!video) {
return { duration, hasVideo: false, width: null, height: null, rotation: 0, fps: 0, hasAudio }; return { duration, hasVideo: false, width: null, height: null, rotation: 0, fps: 0, hasAudio };
} }
+1
View File
@@ -320,6 +320,7 @@ export function createProjectStore({ dataDir, probe }) {
try { try {
media = await probe(final); media = await probe(final);
if (needs === 'video' && !media.hasVideo) problem = 'Keine Videospur gefunden.'; if (needs === 'video' && !media.hasVideo) problem = 'Keine Videospur gefunden.';
else if (needs === 'video' && !(media.duration > 0)) problem = 'Länge des Videos nicht lesbar.';
if (needs === 'audio' && !media.hasAudio) problem = 'Keine Tonspur gefunden.'; if (needs === 'audio' && !media.hasAudio) problem = 'Keine Tonspur gefunden.';
} catch (error) { } catch (error) {
problem = error.message; problem = error.message;
+10
View File
@@ -26,6 +26,9 @@ before(() => {
// Wie ein Hochkant-Handyvideo: quer gespeichert, mit Drehung im Dateikopf // Wie ein Hochkant-Handyvideo: quer gespeichert, mit Drehung im Dateikopf
ffmpeg('-i', path.join(dir, 'quer.mp4'), '-an', '-c', 'copy', '-metadata:s:v:0', 'rotate=90', ffmpeg('-i', path.join(dir, 'quer.mp4'), '-an', '-c', 'copy', '-metadata:s:v:0', 'rotate=90',
path.join(dir, 'hochkant.mp4')); path.join(dir, 'hochkant.mp4'));
// Wie eine Browser-Aufnahme: WebM ohne Gesamtlänge im Kopf
ffmpeg('-f', 'lavfi', '-i', 'sine', '-t', '2', '-c:a', 'libopus', '-f', 'webm', '-live', '1',
path.join(dir, 'aufnahme.webm'));
}); });
after(() => { after(() => {
@@ -54,3 +57,10 @@ test('Datei ohne Video wird abgelehnt', { skip: !hasFfmpeg && 'ffmpeg fehlt' },
fs.writeFileSync(file, 'kein Video'); fs.writeFileSync(file, 'kein Video');
await assert.rejects(probe(file)); await assert.rejects(probe(file));
}); });
test('Browser-Aufnahme ohne Längenangabe wird ausgemessen', { skip: !hasFfmpeg && 'ffmpeg fehlt' }, async () => {
const media = await probe(path.join(dir, 'aufnahme.webm'));
assert.equal(media.hasVideo, false);
assert.equal(media.hasAudio, true);
assert.ok(Math.abs(media.duration - 2) < 0.1, `Länge ${media.duration}`);
});