观影照片支持视频:转 H.264 + 抽首帧封面 (#84)
用户:确认一下观影照片是不是允许上传视频,如果不允许需要修改, 要求能上传视频、视频需要转码,参考旅程中的行程做法。 事实核查:原来不允许 —— POST /movies/<id>/photos 走 Upload::image(), 那条管线只收 JPG/PNG/GIF/WebP(要裁剪缩转运 JPEG),传视频直接拒。 旅程走 Attachment::save(),内部对 video/* 调 Video::toH264()。 Schema VERSION 35→36: - event_photos 加 kind / thumb_key / mime 三列; kind 默认 image —— 老数据全是照片,默认值即正确,不需要回填 UPDATE; - 建表语句同步改(新库直接是新结构)。 Video.php: - 新增 poster() 抽首帧。取第 1 秒(手机视频开头常是黑场/对焦), 宽 480 足够列表缩略;失败返回空 path 且**不阻断上传**。 ApiHandler.php 上传分支按 MIME 分流: - 视频 → toH264 转码 + poster 抽封面 + Storage 落盘 videos/ 与 thumbs/; 转码失败不阻断(存原视频,至少能下载后用本地播放器看); - 图片 → 仍走 Upload::image(),裁剪压缩行为完全不变。 - 返回值加了 kind / thumb / mime,端上靠它画播放按钮。 Movie.php: - photos() 带出 kind/thumb_key/mime; - addPhotos() 兼容裸键数组与结构化数组,只有一条 INSERT 路径; - deletePhoto() / destroy() 补删 thumb_key,否则删视频留孤儿封面。 验证(服务器实测):造 H.265 视频 → needsTranscode=true,toH264 成功 转成 h264/yuv420p,poster 抽出 13KB 封面;完整上传后接口返回 5 条 (4 image + 1 video 带 thumb)。迁移已跑(35→36),字段与数据核实无误, 测试视频及封面已清理,videos/ 与 thumbs/ 目录确认为空。
This commit is contained in:
@@ -195,4 +195,48 @@ final class Video
|
||||
. ' → ' . $after['width'] . 'x' . $after['height'] . ')',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽一帧做封面(2026-11-06,观影照片支持视频)。
|
||||
*
|
||||
* 为什么要抽首帧:列表/网格里不可能为每个视频实时解码 —— 一屏 9 个视频,
|
||||
* 端上要同时挂 9 个解码器,必然卡死。必须服务端抽一帧存成**普通图片**,
|
||||
* 列表只显示这张图,点开才真播。
|
||||
*
|
||||
* ⚠️ 取第 **1 秒**而不是第 0 帧:
|
||||
* 手机视频开头经常是黑场/对焦过程(`-ss 0` 抽出来是一片黑),
|
||||
* 第 1 秒通常已经有内容了。
|
||||
* ⚠️ **失败不阻断**(返回空 path):
|
||||
* 抽帧失败不影响视频本身能用;端上对没有封面的视频显示一个通用图标即可。
|
||||
* 让"没有封面"变成上传失败是最不能接受的取舍。
|
||||
*
|
||||
* @return array{ok:bool, path:string, error:string}
|
||||
*/
|
||||
public static function poster(string $src): array
|
||||
{
|
||||
if (!self::available()) {
|
||||
return ['ok' => false, 'path' => '', 'error' => '服务器没有 ffmpeg'];
|
||||
}
|
||||
$dst = $src . '.poster.jpg';
|
||||
/* 宽度 480 足够列表缩略(端上最大也就显示 100vp 左右)。
|
||||
等比、-2 保证偶数。`-frames:v 1` 只出这一帧。 */
|
||||
$cmd = self::FFMPEG . ' -hide_banner -loglevel error -y'
|
||||
. ' -ss 1 -i ' . escapeshellarg($src)
|
||||
. ' -frames:v 1 -vf ' . escapeshellarg("scale=480:-2")
|
||||
. ' -q:v 4'
|
||||
. ' ' . escapeshellarg($dst)
|
||||
. ' 2>&1';
|
||||
|
||||
$out = [];
|
||||
$code = 1;
|
||||
/* 抽帧很快,给 30 秒足够;超时按失败处理,不拖住上传 */
|
||||
@exec('timeout 30 ' . $cmd, $out, $code);
|
||||
|
||||
if ($code !== 0 || !is_file($dst) || (int)@filesize($dst) === 0) {
|
||||
@unlink($dst);
|
||||
$msg = trim(implode(' ', array_slice($out, 0, 2)));
|
||||
return ['ok' => false, 'path' => '', 'error' => '抽帧失败' . ($msg !== '' ? ':' . $msg : '')];
|
||||
}
|
||||
return ['ok' => true, 'path' => $dst, 'error' => ''];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user