宝塔面板下让 video-thumb.php 跑通 ffmpeg

宝塔面板下让 video-thumb.php 跑通 ffmpeg

把 maer.win 的随拍页面搬到 ai.seerking.com 时,视频帖子封面显示不出来。
查下来一环套一环,记一下。

现象

maer.win 的随拍分类里 49 张图 + 3 个视频。视频帖正文里只有 ,没有 poster 属性。
maer.win 上能正常显示视频封面(ffmpeg 提真实帧)。ai.seerking.com 上同样的三个视频帖,封面是空的。

第一处:变量没定义

最开始以为问题在视频封面生成。看了 page-gallery.php

if ($isVideoOnly && !empty($firstVideo)) {
    $thumbSrc = '/video-thumb.php?src=' . urlencode($firstVideo) . '&w=400';
}

$firstVideo 这个变量从来没被定义过,前面视频数组存的是 $vm[1]
所以 $thumbSrc 永远是空, 不输出,播放按钮孤零零飘在卡片里。

补上:

preg_match_all('/]+src=["\']([^"\']+)["\']/', $text, $vm);
$hasVideo = !empty($vm[1]);
$firstVideo = $hasVideo ? $vm[1][0] : '';

第二处:exec 被禁

修了第一处,刷新还是没图。点开浏览器 devtools 看 video-thumb.php 的响应:

Fatal error: Uncaught Error: Call to undefined function exec()

宝塔默认的 php.ini 把 exec / shell_exec / passthru / popen / proc_open / system 全禁了。
这个安全配置在 PHP-FPM 进程下生效,命令行 php 不受影响。

我写了个 video-thumb.php 三层降级,这样即使没解封也能跑:

// ── 模式A: ffmpeg (需 exec + ffmpeg 都可用) ──
$hasExec = function_exists('exec') && function_exists('shell_exec')
    && function_exists('escapeshellarg') && function_exists('escapeshellcmd');
$hasFfmpeg = false;
if ($hasExec) {
    $which = @shell_exec('command -v ffmpeg 2>/dev/null');
    $hasFfmpeg = !empty(trim((string)$which));
}

if ($hasExec && $hasFfmpeg) {
    $tmpFile = $cacheDir . $hash . '_tmp.jpg';
    $cmd = sprintf(
        '%s -ss 0.5 -i %s -vframes 1 -q:v 2 -vf scale=%d:-1 -y %s 2>&1',
        escapeshellcmd('ffmpeg'), escapeshellarg($src), $width, escapeshellarg($tmpFile)
    );
    @exec($cmd, $output, $rc);
    if ($rc === 0 && file_exists($tmpFile) && filesize($tmpFile) > 500) {
        @rename($tmpFile, $cacheFile);
        header('Content-Type: image/jpeg');
        readfile($cacheFile);
        exit;
    }
    @unlink($tmpFile);
}

// ── 模式B: GD 渐变封面 ──
if (function_exists('imagecreatetruecolor') && function_exists('imagejpeg')) {
    $height = max(80, round($width * 9 / 16));
    $img = @imagecreatetruecolor($width, $height);
    if ($img) {
        for ($y = 0; $y < $height; $y++) {
            $ratio = $y / max(1, $height);
            $r = round(20 + $ratio * 10);
            $g = round(25 + $ratio * 5);
            $b = round(50 + $ratio * 30);
            $color = imagecolorallocate($img, $r, $g, $b);
            imageline($img, 0, $y, $width, $y, $color);
        }
        // 中心白色三角 + 圆形光晕 + 底栏暗条
        // (省略... 跟 maer.win 一样)
        imagejpeg($img, $cacheFile, 85);
        imagedestroy($img);
        header('Content-Type: image/jpeg');
        readfile($cacheFile);
        exit;
    }
}

// ── 模式C: 兜底 SVG ──
header('Content-Type: image/svg+xml');
echo '...';

降到 GD 模式以后,封面确实出来了——但是一眼假。深蓝渐变 + 白色三角,明显是占位图。
跟 maer.win 的真实视频帧对比差太多,还是得解封 exec 装 ffmpeg。

第三处:装 ffmpeg

apt-get install -y ffmpeg

这一步炸出第二个坑:

dpkg: error processing package nginx-common (--configure):
*** nginx (Y/I/N/O/D/Z) [default=N] ? 

宝塔的 nginx 用的是 /www/server/nginx/sbin/nginx,但 Ubuntu 的 apt-get install ffmpeg
nginx-common 也列为依赖(间接依赖链:ffmpeg → libavcodec-extra → libnginx-mod-*)。
nginx-common 配置时会弹 conffile 提示保持还是替换当前版本,默认 N 然后 dpkg 报错。

加上 --force-confdef --force-confnew 让 apt 自动选:

DEBIAN_FRONTEND=noninteractive \
  apt-get -o Dpkg::Options::="--force-confdef" \
           -o Dpkg::Options::="--force-confnew" \
  install -y ffmpeg

装完 Ubuntu 自带的 nginx.service 会注册但不会启动(80 端口被宝塔占着)。
为了以后不被 apt upgrade 拉起来抢端口,hold 住:

apt-mark hold nginx nginx-common nginx-core

第四处:PHP 版本

解封 exec 后第一波没生效:

$ grep '^disable_functions' /www/server/php/80/etc/php.ini
disable_functions = system,putenv,chroot,...

改了,但刷新页面 video-thumb.php 还在走 GD 模式(5KB 渐变,不是 ffmpeg 出的 38KB 真实帧)。

宝塔默认装了两套 PHP(80 和 82),页面跑的是 8.2 不是 8.0:

$ pgrep -a php-fpm
179957 php-fpm: master process (/www/server/php/82/etc/php-fpm.conf)

重新改 82,重启:

cp /www/server/php/82/etc/php.ini /www/server/php/82/etc/php.ini.bak.$(date +%Y%m%d_%H%M%S)
sed -i 's/^disable_functions = .*/disable_functions = system,putenv,chroot,chgrp,chown,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv/' /www/server/php/82/etc/php.ini
/etc/init.d/php-fpm-82 restart

然后清掉旧的 GD 缓存(不删的话,video-thumb.php 会命中 7 天缓存继续返回渐变图):

rm -f /www/wwwroot/ai.seerking.com/usr/uploads/video-thumbs/*.jpg

验证

三个视频帖都拿到了 ffmpeg 真实帧:

| 视频 | 大小 | 尺寸 |
|---|---|---|
| 与好友骑行 | 38.6 KB | 400×844 |
| 厂房花飞 | 19.8 KB | 400×225 |
| 青海湖视频 | 39.3 KB | 400×844 |

跟 maer.win 的 Lavc58.134.100 mjpeg 输出是同一个编码器,视觉一致。

几个值得记一下的点:

  • page-gallery.php 里用了 $firstVideo 但没定义——这种 bug PHP 不报错,$thumbSrc 静默为空,是从 HTML 输出里看 不见了才反应过来
  • 宝塔默认装了两套 PHP(80 和 82),改 ini 前必须先 pgrep -a php-fpm 看清楚跑的是哪套,不然改了 80 没动 82 整个下午都在 debug
  • apt install ffmpeg 会顺带拉 nginx-common,装完一定 apt-mark hold 三件套(nginx nginx-common nginx-core),否则下次 apt upgrade 会把宝塔 nginx 挤掉
  • video-thumb.php 顶上的 7 天缓存判断会先于 ffmpeg 逻辑生效,GD 占位缓存如果不删,新逻辑永远跑不到

添加新评论