Video Upload

This commit is contained in:
2025-11-16 22:40:55 +01:00
parent a3b8900047
commit 1f981376f6
14 changed files with 926 additions and 196 deletions
+163 -30
View File
@@ -11,8 +11,9 @@
declare(strict_types=1);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('display_errors', '0'); // API: keine HTML-Fehler ausgeben (bricht JSON)
ini_set('display_startup_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);
// --- Load settings -----------------------------------------------------------
@@ -506,6 +507,56 @@ function igel_images_upload(PDO $pdo, int $uid, int $igId): void
}
$out = [];
// Hilfsfunktion: Extension/MIME erkennen (inkl. Videos)
$detectMedia = function (string $origName, string $fileType): array {
$lower = strtolower($origName);
$ext = '.bin';
$mime = 'application/octet-stream';
$isImage = false;
$extFromLower = function (array $map) use ($lower): ?array {
foreach ($map as $pattern => $info) {
if (preg_match($pattern, $lower)) {
return $info;
}
}
return null;
};
// bevorzugt Endung, fallback auf gelieferten Content-Type
$info = $extFromLower([
'/\.(jpg|jpeg)$/' => ['.jpg', 'image/jpeg', true],
'/\.png$/' => ['.png', 'image/png', true],
'/\.webp$/' => ['.webp', 'image/webp', true],
'/\.gif$/' => ['.gif', 'image/gif', true],
'/\.heic$/' => ['.heic', 'image/heic', true],
'/\.heif$/' => ['.heif', 'image/heif', true],
'/\.(mp4|m4v)$/' => ['.mp4', 'video/mp4', false],
'/\.mov$/' => ['.mov', 'video/quicktime', false],
'/\.avi$/' => ['.avi', 'video/x-msvideo', false],
'/\.mkv$/' => ['.mkv', 'video/x-matroska', false],
'/\.webm$/' => ['.webm', 'video/webm', false],
'/\.(3gp|3gpp)$/' => ['.3gp', 'video/3gpp', false],
]);
if ($info) {
[$ext, $mime, $isImage] = $info;
} elseif ($fileType) {
$t = strtolower($fileType);
if (str_starts_with($t, 'image/')) {
$mime = $t;
$ext = '.jpg';
$isImage = true;
} elseif (str_starts_with($t, 'video/')) {
$mime = $t;
$ext = '.mp4';
$isImage = false;
}
}
return [$ext, $mime, $isImage];
};
$count = is_array($files['name']) ? count($files['name']) : 0;
for ($i = 0; $i < $count; $i++) {
if ((int) $files['error'][$i] !== UPLOAD_ERR_OK)
@@ -514,26 +565,14 @@ function igel_images_upload(PDO $pdo, int $uid, int $igId): void
$orig = (string) $files['name'][$i];
$size = (int) $files['size'][$i];
if ($size <= 0 || $size > MAX_IMAGE_SIZE)
$maxSize = defined('MAX_MEDIA_SIZE') ? MAX_MEDIA_SIZE : MAX_IMAGE_SIZE;
if ($size <= 0 || $size > $maxSize)
continue;
// MIME grob anhand Endung
$lower = strtolower($orig);
$ext = '.bin';
$mime = 'application/octet-stream';
if (preg_match('/\.(jpg|jpeg)$/', $lower)) {
$ext = '.jpg';
$mime = 'image/jpeg';
} elseif (preg_match('/\.png$/', $lower)) {
$ext = '.png';
$mime = 'image/png';
} elseif (preg_match('/\.webp$/', $lower)) {
$ext = '.webp';
$mime = 'image/webp';
} elseif (preg_match('/\.gif$/', $lower)) {
$ext = '.gif';
$mime = 'image/gif';
}
// MIME/Extension erkennen
$fileType = is_array($files['type']) && isset($files['type'][$i]) ? (string) $files['type'][$i] : '';
[$ext, $mime, $isImage] = $detectMedia($orig, $fileType);
// sichere Dateinamen
$base = bin2hex(random_bytes(8));
@@ -547,18 +586,37 @@ function igel_images_upload(PDO $pdo, int $uid, int $igId): void
if (!move_uploaded_file($tmp, $dest))
continue;
// Thumb
// Thumb (für Bilder echtes Thumbnail, für Videos Frame/Placeholder)
$thumbUrl = null;
try {
$thumbDir = rtrim(UPLOAD_THUMB_DIR, '/');
if (!is_dir($thumbDir)) {
@mkdir($thumbDir, 0755, true);
$thumbBase = $base . ($isImage ? $ext : '.png');
if ($isImage) {
try {
$thumbDir = rtrim(UPLOAD_THUMB_DIR, '/');
if (!is_dir($thumbDir)) {
@mkdir($thumbDir, 0755, true);
}
$thumbPath = $thumbDir . '/' . $thumbBase;
create_thumbnail($dest, $thumbPath, 512, 512); // Quadrat-Box
$thumbUrl = rtrim(UPLOAD_BASE_URL, '/') . '/thumbs/' . $thumbBase;
} catch (Throwable $e) {
$thumbUrl = null; // ok
}
} else {
try {
$thumbDir = rtrim(UPLOAD_THUMB_DIR, '/');
if (!is_dir($thumbDir)) {
@mkdir($thumbDir, 0755, true);
}
$thumbPath = $thumbDir . '/' . $thumbBase;
try {
create_video_thumbnail($dest, $thumbPath, 512, 512);
} catch (Throwable $e) {
create_video_placeholder($thumbPath, 512, 288);
}
$thumbUrl = rtrim(UPLOAD_BASE_URL, '/') . '/thumbs/' . $thumbBase;
} catch (Throwable $e) {
$thumbUrl = null;
}
$thumbPath = $thumbDir . '/' . $fn;
create_thumbnail($dest, $thumbPath, 512, 512); // Quadrat-Box
$thumbUrl = rtrim(UPLOAD_BASE_URL, '/') . '/thumbs/' . $fn;
} catch (Throwable $e) {
$thumbUrl = null; // ok
}
$url = rtrim(UPLOAD_BASE_URL, '/') . '/' . $fn;
@@ -701,6 +759,81 @@ function create_thumbnail(string $src, string $dest, int $maxW, int $maxH): void
imagedestroy($thumb);
}
// Placeholder für Video-Thumbnails (ohne ffmpeg)
function create_video_placeholder(string $dest, int $w = 512, int $h = 288): void
{
if (!extension_loaded('gd'))
throw new Exception('GD not loaded');
$im = imagecreatetruecolor($w, $h);
$bg = imagecolorallocate($im, 34, 34, 34);
imagefilledrectangle($im, 0, 0, $w, $h, $bg);
$accent = imagecolorallocate($im, 240, 240, 240);
$txt = imagecolorallocate($im, 180, 180, 180);
// Play-Icon (Dreieck)
$size = (int) min($w, $h) * 0.3;
$cx = (int) ($w / 2);
$cy = (int) ($h / 2);
$half = (int) ($size / 2);
$points = [
$cx - (int) ($half * 0.7), $cy - $half,
$cx - (int) ($half * 0.7), $cy + $half,
$cx + $half, $cy,
];
imagefilledpolygon($im, $points, 3, $accent);
// Rand
$border = imagecolorallocatealpha($im, 255, 255, 255, 60);
imagerectangle($im, 0, 0, $w - 1, $h - 1, $border);
// Text "VIDEO"
$label = 'VIDEO';
$fontSize = 5; // built-in font
$tw = imagefontwidth($fontSize) * strlen($label);
$th = imagefontheight($fontSize);
imagestring($im, $fontSize, (int) (($w - $tw) / 2), $h - $th - 6, $label, $txt);
$ext = strtolower(pathinfo($dest, PATHINFO_EXTENSION));
if ($ext === 'jpg' || $ext === 'jpeg') {
imagejpeg($im, $dest, 85);
} else {
imagepng($im, $dest, 6);
}
imagedestroy($im);
}
// Video-Thumbnail ber ffmpeg (erstes Frame)
function create_video_thumbnail(string $src, string $dest, int $maxW, int $maxH): void
{
$ffmpeg = trim((string) @shell_exec('command -v ffmpeg'));
if ($ffmpeg === '') {
throw new Exception('ffmpeg not available');
}
$srcEsc = escapeshellarg($src);
$destEsc = escapeshellarg($dest);
// Skaliert proportional, dann schwarze Rnder auf Quadrat
$scale = sprintf(
'scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(%d-iw)/2:(%d-ih)/2',
$maxW,
$maxH,
$maxW,
$maxH,
$maxW,
$maxH
);
$cmd = "$ffmpeg -y -v error -i $srcEsc -frames:v 1 -vf \"$scale\" $destEsc";
$out = [];
$ret = 0;
@exec($cmd, $out, $ret);
if ($ret !== 0 || !is_file($dest)) {
throw new Exception('ffmpeg failed');
}
}
// =============================================================================
// MESSWERTE
// =============================================================================