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, }; }