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>
69 lines
2.5 KiB
JavaScript
69 lines
2.5 KiB
JavaScript
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
function parseRate(rate) {
|
|
const [numerator, denominator] = String(rate || '0/1').split('/').map(Number);
|
|
return denominator ? numerator / denominator : 0;
|
|
}
|
|
|
|
// Handyvideos liegen oft quer im Container und tragen die Drehung als Metadatum.
|
|
function readRotation(stream) {
|
|
const side = stream.side_data_list?.find((entry) => entry.rotation !== undefined);
|
|
if (side) return Number(side.rotation);
|
|
if (stream.tags?.rotate) return Number(stream.tags.rotate);
|
|
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,
|
|
// wie das Video angezeigt wird.
|
|
export async function probe(file) {
|
|
const { stdout } = await execFileAsync(
|
|
'ffprobe',
|
|
['-v', 'error', '-print_format', 'json', '-show_streams', '-show_format', file],
|
|
{ maxBuffer: 10 * 1024 * 1024 },
|
|
);
|
|
const info = JSON.parse(stdout);
|
|
const video = info.streams.find(
|
|
(stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic,
|
|
);
|
|
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.');
|
|
|
|
// 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) {
|
|
return { duration, hasVideo: false, width: null, height: null, rotation: 0, fps: 0, hasAudio };
|
|
}
|
|
const rotation = readRotation(video);
|
|
const sideways = Math.abs(rotation) % 180 === 90;
|
|
return {
|
|
duration,
|
|
hasVideo: true,
|
|
width: sideways ? video.height : video.width,
|
|
height: sideways ? video.width : video.height,
|
|
rotation,
|
|
fps: Math.round(parseRate(video.avg_frame_rate || video.r_frame_rate) * 100) / 100,
|
|
hasAudio,
|
|
};
|
|
}
|