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:
1 parent
6e497ffc45
commit
18d5b80959
3 files changed
+32
-2
No files matched your search
+21
-2
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user