&1', $out, $code); $ok = $code === 0; return $ok; } /** * 探测视频的编码信息。 * * @return array{codec:string,pix_fmt:string,width:int,height:int,duration:float} */ public static function probe(string $path): array { $empty = ['codec' => '', 'pix_fmt' => '', 'width' => 0, 'height' => 0, 'duration' => 0.0]; $cmd = self::FFPROBE . ' -v error -select_streams v:0' . ' -show_entries stream=codec_name,pix_fmt,width,height' . ' -show_entries format=duration' . ' -of default=noprint_wrappers=1 ' . escapeshellarg($path) . ' 2>&1'; $out = []; $code = 1; @exec($cmd, $out, $code); if ($code !== 0) { return $empty; } $kv = []; foreach ($out as $line) { $p = strpos($line, '='); if ($p > 0) { $kv[substr($line, 0, $p)] = substr($line, $p + 1); } } return [ 'codec' => strtolower((string)($kv['codec_name'] ?? '')), 'pix_fmt' => strtolower((string)($kv['pix_fmt'] ?? '')), 'width' => (int)($kv['width'] ?? 0), 'height' => (int)($kv['height'] ?? 0), 'duration' => (float)($kv['duration'] ?? 0), ]; } /** * 这个视频需要转码吗。 * * 判据(任一成立就要转): * ① 编码不是 h264 —— HEVC/VP9/AV1 在部分浏览器播不了 * ② 像素格式不是 yuv420p/yuvj420p —— 10bit / 4:2:2 浏览器不认 * * ⚠️ 已经符合的**不重复转**:转码要 CPU 也要时间, * 用户传 H.264 视频时不该白等。 */ public static function needsTranscode(string $path): bool { $p = self::probe($path); if ($p['codec'] === '') { /* 探测失败(文件损坏/ffprobe 缺失)⇒ 不转,原样存, 至少还能下载。硬转可能把好文件弄坏。 */ return false; } if ($p['codec'] !== 'h264') { return true; } $okPix = ['yuv420p', 'yuvj420p', 'nv12']; return !in_array($p['pix_fmt'], $okPix, true); } /** * 转成 H.264。 * * @return array{ok:bool,path?:string,error?:string,note?:string} * ok=true 时 `path` 是**新文件**的临时路径(调用方负责清理), * `note` 是给用户看的一句话(如「已从 H.265 转成 H.264」)。 */ public static function toH264(string $src): array { if (!self::available()) { return ['ok' => false, 'error' => '服务器没有 ffmpeg,视频无法转码']; } $before = self::probe($src); $dst = $src . '.h264.mp4'; /* 缩放:宽超过 1920 才缩;-2 保证高度偶数(yuv420 要求) */ $scale = "scale='min(1920,iw)':-2"; $cmd = self::FFMPEG . ' -hide_banner -loglevel error -y' . ' -i ' . escapeshellarg($src) . ' -c:v libx264 -preset veryfast -crf 23' . ' -pix_fmt yuv420p' . ' -vf ' . escapeshellarg($scale) . ' -c:a aac -b:a 128k' . ' -movflags +faststart' . ' ' . escapeshellarg($dst) . ' 2>&1'; $out = []; $code = 1; @exec('timeout ' . self::TIMEOUT . ' ' . $cmd, $out, $code); if ($code !== 0 || !is_file($dst) || (int)@filesize($dst) === 0) { @unlink($dst); $msg = trim(implode(' ', array_slice($out, 0, 3))); return ['ok' => false, 'error' => '转码失败' . ($msg !== '' ? ':' . $msg : '')]; } $after = self::probe($dst); /* 转完自检:必须真的是 h264 + yuv420p,否则别拿去用 */ if ($after['codec'] !== 'h264' || !in_array($after['pix_fmt'], ['yuv420p', 'yuvj420p'], true)) { @unlink($dst); return ['ok' => false, 'error' => '转码结果异常(' . $after['codec'] . '/' . $after['pix_fmt'] . ')']; } $fromName = $before['codec'] !== '' ? strtoupper($before['codec']) : '原格式'; return [ 'ok' => true, 'path' => $dst, 'note' => '已从 ' . $fromName . ' 转成 H.264(' . $before['width'] . 'x' . $before['height'] . ' → ' . $after['width'] . 'x' . $after['height'] . ')', ]; } }