(filePath: string)
| 78 | // --------------------------------------------------------------------------- |
| 79 | |
| 80 | function probeVideo(filePath: string): VideoMeta | undefined { |
| 81 | try { |
| 82 | const ffprobePath = findFFprobe(); |
| 83 | if (!ffprobePath) return undefined; |
| 84 | const raw = execFileSync( |
| 85 | ffprobePath, |
| 86 | ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath], |
| 87 | { encoding: "utf-8", timeout: 15_000 }, |
| 88 | ); |
| 89 | |
| 90 | const parsed: { |
| 91 | streams?: { |
| 92 | codec_type?: string; |
| 93 | codec_name?: string; |
| 94 | width?: number; |
| 95 | height?: number; |
| 96 | r_frame_rate?: string; |
| 97 | avg_frame_rate?: string; |
| 98 | }[]; |
| 99 | format?: { duration?: string }; |
| 100 | } = JSON.parse(raw); |
| 101 | |
| 102 | const streams = parsed.streams ?? []; |
| 103 | const videoStream = streams.find((s) => s.codec_type === "video"); |
| 104 | if (!videoStream) return undefined; |
| 105 | |
| 106 | const hasAudio = streams.some((s) => s.codec_type === "audio"); |
| 107 | |
| 108 | let fps = 30; |
| 109 | const fpsStr = videoStream.avg_frame_rate ?? videoStream.r_frame_rate; |
| 110 | if (fpsStr) { |
| 111 | const parts = fpsStr.split("/"); |
| 112 | const num = parseFloat(parts[0] ?? ""); |
| 113 | const den = parseFloat(parts[1] ?? "1"); |
| 114 | if (den !== 0 && !Number.isNaN(num) && !Number.isNaN(den)) { |
| 115 | fps = Math.round((num / den) * 100) / 100; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | const durationStr = parsed.format?.duration; |
| 120 | const durationSeconds = durationStr !== undefined ? parseFloat(durationStr) : 5; |
| 121 | |
| 122 | return { |
| 123 | durationSeconds: Number.isNaN(durationSeconds) ? 5 : durationSeconds, |
| 124 | width: videoStream.width ?? 1920, |
| 125 | height: videoStream.height ?? 1080, |
| 126 | fps, |
| 127 | hasAudio, |
| 128 | videoCodec: videoStream.codec_name ?? "unknown", |
| 129 | }; |
| 130 | } catch { |
| 131 | return undefined; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | function isWebCompatible(codec: string): boolean { |
| 136 | return WEB_CODECS.has(codec.toLowerCase()); |
no test coverage detected