getClientOriginalName(); $extension = strtolower($file->getClientOriginalExtension()); $storageName = Str::uuid().'.'.$extension; $datePath = now()->format('Y/m'); $relativePath = "display-media/{$datePath}/{$storageName}"; Storage::disk('public')->putFileAs("display-media/{$datePath}", $file, $storageName); $type = in_array($extension, ['mp4', 'webm', 'mov']) ? 'video' : 'image'; $metadata = []; if ($type === 'image') { $dimensions = @getimagesize($file->getRealPath()); if ($dimensions) { $metadata['width'] = $dimensions[0]; $metadata['height'] = $dimensions[1]; } } $media = DisplayMedia::create([ 'filename' => $filename, 'disk' => 'public', 'path' => $relativePath, 'source_type' => 'upload', 'type' => $type, 'mime_type' => $file->getMimeType(), 'file_size' => $file->getSize(), 'collection' => $collection, 'metadata' => ! empty($metadata) ? $metadata : null, ]); if ($type === 'video') { $thumbnailPath = $this->generateVideoThumbnail($media); if ($thumbnailPath !== null) { $media->update(['thumbnail_path' => $thumbnailPath]); } } return $media; } /** * Generate a poster frame for an uploaded video using ffmpeg. * * Returns the relative thumbnail path on the media's disk, or null when * generation is not possible (e.g. ffmpeg missing or an unreadable file). */ public function generateVideoThumbnail(DisplayMedia $media): ?string { if (! $media->isVideo() || ! $media->isUpload() || ! $media->path) { return null; } $disk = Storage::disk($media->disk); $videoPath = $disk->path($media->path); if (! is_file($videoPath)) { return null; } $thumbnailRelativePath = preg_replace('/\.[^.\/]+$/', '', $media->path).'-poster.jpg'; $thumbnailPath = $disk->path($thumbnailRelativePath); if (! is_dir(dirname($thumbnailPath))) { @mkdir(dirname($thumbnailPath), 0755, true); } // Try a frame ~1s in first (avoids black intro frames); fall back to // the very first frame for clips shorter than one second. foreach (['1', '0'] as $seekSeconds) { if ($this->extractFrame($videoPath, $thumbnailPath, $seekSeconds) && filesize($thumbnailPath) > 0) { return $thumbnailRelativePath; } } if (is_file($thumbnailPath)) { @unlink($thumbnailPath); } return null; } private function extractFrame(string $videoPath, string $thumbnailPath, string $seekSeconds): bool { $process = new Process([ 'ffmpeg', '-y', '-ss', $seekSeconds, '-i', $videoPath, '-frames:v', '1', '-vf', 'scale=640:-2', '-q:v', '3', $thumbnailPath, ]); $process->setTimeout(60); try { $process->run(); } catch (\Throwable) { return false; } return $process->isSuccessful() && is_file($thumbnailPath); } /** * Create a DisplayMedia record from an external URL. */ public function createFromUrl(string $url, string $type = 'video', ?string $title = null, ?string $collection = null): DisplayMedia { $filename = $title ?: $this->extractFilenameFromUrl($url); return DisplayMedia::create([ 'filename' => $filename, 'disk' => 'public', 'path' => null, 'external_url' => $url, 'source_type' => 'external', 'type' => $type, 'mime_type' => null, 'file_size' => 0, 'title' => $title, 'collection' => $collection, ]); } /** * Validate that an external URL is accessible. */ public function validateExternalUrl(string $url): bool { try { $response = Http::timeout(10) ->withOptions(['allow_redirects' => true]) ->head($url); return $response->successful() || $response->status() === 302 || $response->status() === 301; } catch (\Throwable) { // Some services block HEAD requests, try GET with stream try { $response = Http::timeout(10) ->withOptions(['allow_redirects' => true, 'stream' => true]) ->get($url); return $response->successful(); } catch (\Throwable) { return false; } } } /** * Delete a DisplayMedia record and its associated files. */ public function delete(DisplayMedia $media): void { if ($media->isUpload() && $media->path) { Storage::disk($media->disk)->delete($media->path); } if ($media->thumbnail_path) { Storage::disk($media->disk)->delete($media->thumbnail_path); } $media->delete(); } private function extractFilenameFromUrl(string $url): string { $parsed = parse_url($url, PHP_URL_PATH); $basename = $parsed ? basename($parsed) : 'external-media'; return Str::limit($basename, 100); } }