lthn.io/app/Core/Media/Conversions/MediaVideoThumbConversion.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

139 lines
3.9 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Media\Conversions;
use Core\Media\Abstracts\Image;
use Core\Media\Abstracts\MediaConversion;
use Core\Media\Support\ImageResizer;
use Core\Media\Support\MediaConversionData;
use Core\Media\Support\TemporaryFile;
use FFMpeg\Coordinate\TimeCode;
use FFMpeg\FFMpeg;
use Illuminate\Support\Facades\File;
/**
* Video thumbnail generation conversion.
*
* Extracts a frame from a video at a specified timestamp and generates
* a resized thumbnail image. Requires FFmpeg to be installed.
*/
class MediaVideoThumbConversion extends MediaConversion
{
protected float $atSecond = 0;
/**
* Get the engine name for this conversion.
*/
public function getEngineName(): string
{
return 'VideoThumb';
}
/**
* Check if this conversion can be performed.
*
* Only processes video files and requires FFmpeg installation.
*/
public function canPerform(): bool
{
return $this->isVideo();
}
/**
* Get the output file path.
*
* Video thumbnails are always saved as JPG.
*/
public function getPath(): string
{
return $this->getFilePathWithSuffix('jpg');
}
/**
* Set the timestamp (in seconds) to extract the frame from.
*/
public function atSecond(float $value = 0): static
{
$this->atSecond = $value;
return $this;
}
/**
* Perform the video thumbnail generation.
*
* Returns null if FFmpeg is not installed.
*/
public function handle(): ?MediaConversionData
{
if (! $this->isFFmpegInstalled()) {
return null;
}
// Copy video to temporary location for processing
$temporaryFile = TemporaryFile::make()->fromDisk(
sourceDisk: $this->getFromDisk(),
sourceFilepath: $this->getFilepath()
);
$thumbFilepath = $this->getFilePathWithSuffix('jpg', $temporaryFile->path());
// Extract frame using FFmpeg
$ffmpeg = FFMpeg::create([
'ffmpeg.binaries' => config('media.ffmpeg_path', '/usr/bin/ffmpeg'),
'ffprobe.binaries' => config('media.ffprobe_path', '/usr/bin/ffprobe'),
]);
$video = $ffmpeg->open($temporaryFile->path());
$duration = $ffmpeg->getFFProbe()->format($temporaryFile->path())->get('duration');
// Ensure seconds is within valid bounds
$seconds = ($duration > 0 && $this->atSecond > 0)
? min($this->atSecond, floor($duration))
: 0;
$frame = $video->frame(TimeCode::fromSeconds($seconds));
$frame->save($thumbFilepath);
// Sometimes the frame is not saved, so we retry with the first frame
// This is a workaround for edge cases in FFmpeg
if ($this->atSecond !== 0.0 && ! File::exists($thumbFilepath)) {
$frame = $video->frame(TimeCode::fromSeconds(0));
$frame->save($thumbFilepath);
}
// Resize the thumbnail and save it to the destination disk
ImageResizer::make($thumbFilepath)
->disk($this->getToDisk())
->path($this->getPath())
->resize(Image::MEDIUM_WIDTH, Image::MEDIUM_HEIGHT);
// Clean up temporary files
$temporaryFile->directory()->delete();
return MediaConversionData::conversion($this);
}
/**
* Check if FFmpeg is installed and accessible.
*/
private function isFFmpegInstalled(): bool
{
$ffmpegPath = config('media.ffmpeg_path', '/usr/bin/ffmpeg');
$ffprobePath = config('media.ffprobe_path', '/usr/bin/ffprobe');
return file_exists($ffmpegPath) &&
file_exists($ffprobePath) &&
basename($ffmpegPath) === 'ffmpeg' &&
basename($ffprobePath) === 'ffprobe';
}
}