From a4032546545c6cd6b6f0028a8bdf5af594f412d1 Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:00:07 +0100 Subject: [PATCH 1/7] wip --- config/assets.php | 5 +- .../js/components/assets/Browser/Grid.vue | 1 + .../components/assets/Browser/Thumbnail.vue | 1 + src/Console/Processes/Ffmpeg.php | 29 +++++- .../CP/Assets/ThumbnailController.php | 22 ++++- .../Resources/CP/Assets/HasThumbnails.php | 3 +- src/Imaging/ImageGenerator.php | 6 +- src/Imaging/ThumbnailExtractor.php | 5 + tests/Feature/Assets/VideoThumbnailTest.php | 94 +++++++++++++++++++ 9 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/Assets/VideoThumbnailTest.php diff --git a/config/assets.php b/config/assets.php index b91ecb9104e..240fe27475a 100644 --- a/config/assets.php +++ b/config/assets.php @@ -148,8 +148,9 @@ | Control Panel Video Thumbnails |-------------------------------------------------------------------------- | - | When enabled, Statamic will generate thumbnails for videos. - | Generated thumbnails are displayed in the Control Panel. + | When enabled, Statamic will generate thumbnails for videos when FFmpeg + | is available. Generated thumbnails are displayed in the Control Panel. + | Without FFmpeg, videos fall back to a filetype icon. | */ diff --git a/resources/js/components/assets/Browser/Grid.vue b/resources/js/components/assets/Browser/Grid.vue index f832956c56a..63aa8f79e8a 100644 --- a/resources/js/components/assets/Browser/Grid.vue +++ b/resources/js/components/assets/Browser/Grid.vue @@ -128,6 +128,7 @@ 'w-full p-4': asset.extension === 'svg', 'rounded-lg p-1': asset.orientation === 'square', }" + @error="asset.thumbnail = null" /> diff --git a/resources/js/components/assets/Browser/Thumbnail.vue b/resources/js/components/assets/Browser/Thumbnail.vue index 50c49093e1b..345f3003327 100644 --- a/resources/js/components/assets/Browser/Thumbnail.vue +++ b/resources/js/components/assets/Browser/Thumbnail.vue @@ -7,6 +7,7 @@ loading="lazy" :draggable="false" :class="{ 'h-8 w-8 object-cover': square }" + @error="asset.thumbnail = null" /> startTimestamp = $startTimestamp; @@ -49,10 +53,21 @@ private function buildCommand(string $ffmpegBinary, string $path, string $output ])->join(' '); } + public function available(): bool + { + return filled($this->ffmpegBinary()); + } + public function ffmpegBinary(): ?string { + if (static::$binaryResolved) { + return static::$resolvedBinary; + } + + static::$binaryResolved = true; + if ($binary = config('statamic.assets.ffmpeg.binary')) { - return $binary; + return static::$resolvedBinary = $binary; } $output = $this->run($this->isWindows() ? 'where ffmpeg' : 'which ffmpeg'); @@ -63,11 +78,19 @@ public function ffmpegBinary(): ?string } if (str($output)->lower()->contains('could not find files for the given')) { - return null; + return static::$resolvedBinary = null; } - return str(StringUtilities::normalizeLineEndings(trim($output))) + $resolved = str(StringUtilities::normalizeLineEndings(trim($output))) ->explode("\n") ->first(); + + return static::$resolvedBinary = filled($resolved) ? $resolved : null; + } + + public static function clearBinaryCache(): void + { + static::$binaryResolved = false; + static::$resolvedBinary = null; } } diff --git a/src/Http/Controllers/CP/Assets/ThumbnailController.php b/src/Http/Controllers/CP/Assets/ThumbnailController.php index 6e529e1bd88..f05812d56c4 100644 --- a/src/Http/Controllers/CP/Assets/ThumbnailController.php +++ b/src/Http/Controllers/CP/Assets/ThumbnailController.php @@ -71,9 +71,15 @@ public function show($asset, $size = null, $orientation = null) return $placeholder; } + $path = $this->generate(); + + if (! $path) { + return $this->getUnavailableThumbnailResponse(); + } + return $this->server->getResponseFactory()->create( $this->server->getCache(), - $this->generate() + $path ); } @@ -189,4 +195,18 @@ private function getPlaceholderResponse() return response(Statamic::svg('filetypes/picture'))->header('Content-Type', 'image/svg+xml'); } + + /** + * When thumbnail generation fails (e.g. FFmpeg missing for videos), show a filetype icon. + * + * @return \Illuminate\Http\Response + */ + private function getUnavailableThumbnailResponse() + { + $svg = $this->asset->isVideo() + ? Statamic::svg('filetypes/video') + : Statamic::svg('filetypes/picture'); + + return response($svg)->header('Content-Type', 'image/svg+xml'); + } } diff --git a/src/Http/Resources/CP/Assets/HasThumbnails.php b/src/Http/Resources/CP/Assets/HasThumbnails.php index 76a08f3d33d..a0ad4cfe4b2 100644 --- a/src/Http/Resources/CP/Assets/HasThumbnails.php +++ b/src/Http/Resources/CP/Assets/HasThumbnails.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Resources\CP\Assets; use Illuminate\Support\Fluent; +use Statamic\Imaging\ThumbnailExtractor; use Statamic\Support\Traits\Hookable; trait HasThumbnails @@ -13,7 +14,7 @@ private function thumbnails(): array { $data = match (true) { $this->isImage() || $this->isSvg() => $this->getImageThumbnail(), - $this->isVideo() && config('statamic.assets.video_thumbnails', true) => $this->getVideoThumbnail(), + $this->isVideo() && ThumbnailExtractor::available() => $this->getVideoThumbnail(), default => ['thumbnail' => null], }; diff --git a/src/Imaging/ImageGenerator.php b/src/Imaging/ImageGenerator.php index c419010fc51..17727960c62 100644 --- a/src/Imaging/ImageGenerator.php +++ b/src/Imaging/ImageGenerator.php @@ -155,10 +155,14 @@ public function generateVideoThumbnail($asset, array $params) */ public function generateByAsset($asset, array $params) { - if (ThumbnailExtractor::enabled() && $asset->isVideo()) { + if (ThumbnailExtractor::available() && $asset->isVideo()) { return $this->generateVideoThumbnail($asset, $params); } + if ($asset->isVideo()) { + return ''; + } + $manipulationCacheKey = 'asset::'.$asset->id().'::'.md5(json_encode($params)); $manifestCacheKey = static::assetCacheManifestKey($asset); diff --git a/src/Imaging/ThumbnailExtractor.php b/src/Imaging/ThumbnailExtractor.php index 06cf028bb2d..62e748b199d 100644 --- a/src/Imaging/ThumbnailExtractor.php +++ b/src/Imaging/ThumbnailExtractor.php @@ -20,6 +20,11 @@ public static function enabled() ); } + public static function available() + { + return static::enabled() && app(Ffmpeg::class)->available(); + } + public static function cachePath() { return config( diff --git a/tests/Feature/Assets/VideoThumbnailTest.php b/tests/Feature/Assets/VideoThumbnailTest.php new file mode 100644 index 00000000000..0cefb556467 --- /dev/null +++ b/tests/Feature/Assets/VideoThumbnailTest.php @@ -0,0 +1,94 @@ + [ + 'driver' => 'local', + 'root' => $this->tempDir = __DIR__.'/tmp', + ]]); + } + + public function tearDown(): void + { + Ffmpeg::clearBinaryCache(); + + app('files')->deleteDirectory($this->tempDir); + + parent::tearDown(); + } + + #[Test] + public function it_omits_thumbnail_url_from_asset_payload_when_ffmpeg_is_unavailable() + { + $this->withoutFfmpeg(); + $this->actingAs(tap(User::make()->makeSuper())->save()); + + $asset = $this->createVideoAsset(); + + $payload = (new AssetsFieldtypeAsset($asset))->resolve()['data']; + + $this->assertNull($payload['thumbnail']); + } + + #[Test] + public function it_returns_a_filetype_icon_when_video_thumbnail_cannot_be_generated() + { + $this->withoutFfmpeg(); + + $asset = $this->createVideoAsset(); + + $this->setTestRoles(['test' => ['access cp', 'view test assets']]); + $user = User::make()->assignRole('test')->save(); + + $this + ->actingAs($user) + ->get('/cp/thumbnails/'.base64_encode($asset->id()).'/small') + ->assertSuccessful() + ->assertHeader('Content-Type', 'image/svg+xml'); + } + + private function createVideoAsset() + { + $container = AssetContainer::make('test')->disk('test')->save(); + + return $container + ->makeAsset('clip.mp4') + ->upload(UploadedFile::fake()->create('clip.mp4', 100, 'video/mp4')); + } + + private function withoutFfmpeg() + { + config(['statamic.assets.ffmpeg.binary' => null]); + + $this->mock(Ffmpeg::class, function ($mock) { + $mock->shouldReceive('available')->andReturn(false); + $mock->shouldReceive('ffmpegBinary')->andReturn(null); + $mock->shouldReceive('extractThumbnail')->andReturn(null); + }); + + Ffmpeg::clearBinaryCache(); + } +} From e878eddfed6c9e16c1496fbd881db0efa2b9a7fd Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:01:38 +0100 Subject: [PATCH 2/7] Require configured FFmpeg binary to be executable A stale or mistyped ffmpeg.binary path was treated as available, so video thumbnail URLs were still emitted and the CP hit failing requests. --- src/Console/Processes/Ffmpeg.php | 2 +- tests/Console/FfmpegTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Console/Processes/Ffmpeg.php b/src/Console/Processes/Ffmpeg.php index b15edbd77ab..57f4f4a3480 100644 --- a/src/Console/Processes/Ffmpeg.php +++ b/src/Console/Processes/Ffmpeg.php @@ -67,7 +67,7 @@ public function ffmpegBinary(): ?string static::$binaryResolved = true; if ($binary = config('statamic.assets.ffmpeg.binary')) { - return static::$resolvedBinary = $binary; + return static::$resolvedBinary = is_executable($binary) ? $binary : null; } $output = $this->run($this->isWindows() ? 'where ffmpeg' : 'which ffmpeg'); diff --git a/tests/Console/FfmpegTest.php b/tests/Console/FfmpegTest.php index 46bddcf5668..aa6de7db372 100644 --- a/tests/Console/FfmpegTest.php +++ b/tests/Console/FfmpegTest.php @@ -8,6 +8,13 @@ class FfmpegTest extends TestCase { + public function tearDown(): void + { + Ffmpeg::clearBinaryCache(); + + parent::tearDown(); + } + #[Test] public function it_builds_a_thumbnail_command_that_only_writes_errors_to_stderr() { @@ -20,6 +27,16 @@ public function it_builds_a_thumbnail_command_that_only_writes_errors_to_stderr( $this->assertStringNotContainsString('-vframes', $command); } + #[Test] + public function it_ignores_a_configured_binary_that_is_not_executable() + { + Ffmpeg::clearBinaryCache(); + config(['statamic.assets.ffmpeg.binary' => storage_path('missing-ffmpeg-binary')]); + + $this->assertNull((new Ffmpeg)->ffmpegBinary()); + $this->assertFalse((new Ffmpeg)->available()); + } + private function buildCommand(...$arguments) { $method = (new \ReflectionClass(Ffmpeg::class))->getMethod('buildCommand'); From 1065be79ecba0da08bf73d228a128cd265c94c97 Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:02:04 +0100 Subject: [PATCH 3/7] Resolve FFmpeg availability per request with once() A process-static cache could leave long-lived workers (e.g. Octane) stuck with a stale missing-binary result across requests. --- config/assets.php | 1 + src/Console/Processes/Ffmpeg.php | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/config/assets.php b/config/assets.php index 240fe27475a..4e94f608a81 100644 --- a/config/assets.php +++ b/config/assets.php @@ -273,6 +273,7 @@ | | Statamic uses FFmpeg to extract thumbnails from videos to be shown in the | Control Panel. You may adjust the binary location and cache path here. + | The configured binary must exist and be executable. | */ diff --git a/src/Console/Processes/Ffmpeg.php b/src/Console/Processes/Ffmpeg.php index 57f4f4a3480..c8dc3a99de9 100644 --- a/src/Console/Processes/Ffmpeg.php +++ b/src/Console/Processes/Ffmpeg.php @@ -2,16 +2,13 @@ namespace Statamic\Console\Processes; +use Illuminate\Support\Once; use Statamic\View\Antlers\Language\Utilities\StringUtilities; class Ffmpeg extends Process { protected string $startTimestamp = '00:00:00'; - private static bool $binaryResolved = false; - - private static ?string $resolvedBinary = null; - public function startTimestamp(string $startTimestamp): self { $this->startTimestamp = $startTimestamp; @@ -60,14 +57,13 @@ public function available(): bool public function ffmpegBinary(): ?string { - if (static::$binaryResolved) { - return static::$resolvedBinary; - } - - static::$binaryResolved = true; + return once(fn () => $this->resolveFfmpegBinary()); + } + private function resolveFfmpegBinary(): ?string + { if ($binary = config('statamic.assets.ffmpeg.binary')) { - return static::$resolvedBinary = is_executable($binary) ? $binary : null; + return is_executable($binary) ? $binary : null; } $output = $this->run($this->isWindows() ? 'where ffmpeg' : 'which ffmpeg'); @@ -78,19 +74,18 @@ public function ffmpegBinary(): ?string } if (str($output)->lower()->contains('could not find files for the given')) { - return static::$resolvedBinary = null; + return null; } $resolved = str(StringUtilities::normalizeLineEndings(trim($output))) ->explode("\n") ->first(); - return static::$resolvedBinary = filled($resolved) ? $resolved : null; + return filled($resolved) ? $resolved : null; } public static function clearBinaryCache(): void { - static::$binaryResolved = false; - static::$resolvedBinary = null; + Once::flush(); } } From 4acd3c9c9e6c118b2f020bb9e030146324089d49 Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:03:01 +0100 Subject: [PATCH 4/7] Strengthen video thumbnail fallback tests Assert the video filetype SVG body, and cover video_thumbnails being disabled so Glide never tries to manipulate an MP4. --- tests/Feature/Assets/VideoThumbnailTest.php | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Assets/VideoThumbnailTest.php b/tests/Feature/Assets/VideoThumbnailTest.php index 0cefb556467..9cc2c1f83e1 100644 --- a/tests/Feature/Assets/VideoThumbnailTest.php +++ b/tests/Feature/Assets/VideoThumbnailTest.php @@ -8,6 +8,7 @@ use Statamic\Facades\AssetContainer; use Statamic\Facades\User; use Statamic\Http\Resources\CP\Assets\AssetsFieldtypeAsset; +use Statamic\Statamic; use Tests\FakesRoles; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -54,7 +55,7 @@ public function it_omits_thumbnail_url_from_asset_payload_when_ffmpeg_is_unavail } #[Test] - public function it_returns_a_filetype_icon_when_video_thumbnail_cannot_be_generated() + public function it_returns_a_video_filetype_icon_when_video_thumbnail_cannot_be_generated() { $this->withoutFfmpeg(); @@ -67,7 +68,27 @@ public function it_returns_a_filetype_icon_when_video_thumbnail_cannot_be_genera ->actingAs($user) ->get('/cp/thumbnails/'.base64_encode($asset->id()).'/small') ->assertSuccessful() - ->assertHeader('Content-Type', 'image/svg+xml'); + ->assertHeader('Content-Type', 'image/svg+xml') + ->assertSee(Statamic::svg('filetypes/video'), false); + } + + #[Test] + public function it_returns_a_video_filetype_icon_when_video_thumbnails_are_disabled() + { + config(['statamic.assets.video_thumbnails' => false]); + Ffmpeg::clearBinaryCache(); + + $asset = $this->createVideoAsset(); + + $this->setTestRoles(['test' => ['access cp', 'view test assets']]); + $user = User::make()->assignRole('test')->save(); + + $this + ->actingAs($user) + ->get('/cp/thumbnails/'.base64_encode($asset->id()).'/small') + ->assertSuccessful() + ->assertHeader('Content-Type', 'image/svg+xml') + ->assertSee(Statamic::svg('filetypes/video'), false); } private function createVideoAsset() From 3c39a7bfd3b073e88f4bf2d092177e31d1c004f4 Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:56:55 +0100 Subject: [PATCH 5/7] Memoize FFmpeg binary across instances for the request Laravel once() is instance-scoped and Ffmpeg is not a singleton, so each asset re-resolved the binary. Use a static cache cleared on Octane RequestReceived (and via clearBinaryCache) instead of Once::flush(). --- src/Console/Processes/Ffmpeg.php | 16 +++++++++++++--- src/Providers/AppServiceProvider.php | 6 ++++++ tests/Console/FfmpegTest.php | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Console/Processes/Ffmpeg.php b/src/Console/Processes/Ffmpeg.php index c8dc3a99de9..345933ca8db 100644 --- a/src/Console/Processes/Ffmpeg.php +++ b/src/Console/Processes/Ffmpeg.php @@ -2,13 +2,16 @@ namespace Statamic\Console\Processes; -use Illuminate\Support\Once; use Statamic\View\Antlers\Language\Utilities\StringUtilities; class Ffmpeg extends Process { protected string $startTimestamp = '00:00:00'; + private static bool $binaryResolved = false; + + private static ?string $resolvedBinary = null; + public function startTimestamp(string $startTimestamp): self { $this->startTimestamp = $startTimestamp; @@ -57,7 +60,13 @@ public function available(): bool public function ffmpegBinary(): ?string { - return once(fn () => $this->resolveFfmpegBinary()); + if (static::$binaryResolved) { + return static::$resolvedBinary; + } + + static::$binaryResolved = true; + + return static::$resolvedBinary = $this->resolveFfmpegBinary(); } private function resolveFfmpegBinary(): ?string @@ -86,6 +95,7 @@ private function resolveFfmpegBinary(): ?string public static function clearBinaryCache(): void { - Once::flush(); + static::$binaryResolved = false; + static::$resolvedBinary = null; } } diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index 507d81d19cc..05bb28b501f 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -8,9 +8,11 @@ use Illuminate\Foundation\Http\Middleware\TrimStrings; use Illuminate\Http\Request; use Illuminate\Routing\Router; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Session; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; +use Statamic\Console\Processes\Ffmpeg; use Statamic\CP\CarbonAsVueComponent; use Statamic\Facades; use Statamic\Facades\Addon; @@ -49,6 +51,10 @@ public function boot() $this->loadRoutesFrom("{$this->root}/routes/routes.php"); }); + if (class_exists(\Laravel\Octane\Events\RequestReceived::class)) { + Event::listen(\Laravel\Octane\Events\RequestReceived::class, fn () => Ffmpeg::clearBinaryCache()); + } + $this->app[\Illuminate\Contracts\Http\Kernel::class] ->pushMiddleware(\Statamic\Http\Middleware\PoweredByHeader::class) ->pushMiddleware(\Statamic\Http\Middleware\CheckComposerJsonScripts::class) diff --git a/tests/Console/FfmpegTest.php b/tests/Console/FfmpegTest.php index aa6de7db372..afce7bdd5cd 100644 --- a/tests/Console/FfmpegTest.php +++ b/tests/Console/FfmpegTest.php @@ -37,6 +37,23 @@ public function it_ignores_a_configured_binary_that_is_not_executable() $this->assertFalse((new Ffmpeg)->available()); } + #[Test] + public function it_memoizes_binary_resolution_across_instances() + { + Ffmpeg::clearBinaryCache(); + config(['statamic.assets.ffmpeg.binary' => PHP_BINARY]); + + $resolved = (new Ffmpeg)->ffmpegBinary(); + + config(['statamic.assets.ffmpeg.binary' => storage_path('missing-ffmpeg-binary')]); + + $this->assertSame($resolved, (new Ffmpeg)->ffmpegBinary()); + + Ffmpeg::clearBinaryCache(); + + $this->assertNull((new Ffmpeg)->ffmpegBinary()); + } + private function buildCommand(...$arguments) { $method = (new \ReflectionClass(Ffmpeg::class))->getMethod('buildCommand'); From 23db8db7dec21952dd9133d96960679080fab501 Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:57:07 +0100 Subject: [PATCH 6/7] Fall back to file icon when asset fieldtype thumbnails fail Browser Grid/Thumbnail already cleared a broken thumbnail URL on img error; Assets fieldtype tiles and rows did not, so failed video thumbs still showed as broken images there. --- resources/js/components/fieldtypes/assets/AssetRow.vue | 1 + resources/js/components/fieldtypes/assets/AssetTile.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/assets/AssetRow.vue b/resources/js/components/fieldtypes/assets/AssetRow.vue index 3f9fef16e0d..f23072790f6 100644 --- a/resources/js/components/fieldtypes/assets/AssetRow.vue +++ b/resources/js/components/fieldtypes/assets/AssetRow.vue @@ -18,6 +18,7 @@ :src="thumbnail" :alt="asset.basename" v-if="thumbnail" + @error="asset.thumbnail = null" /> diff --git a/resources/js/components/fieldtypes/assets/AssetTile.vue b/resources/js/components/fieldtypes/assets/AssetTile.vue index bfab7cdd54d..e8cf4cc7c99 100644 --- a/resources/js/components/fieldtypes/assets/AssetTile.vue +++ b/resources/js/components/fieldtypes/assets/AssetTile.vue @@ -36,7 +36,7 @@ From 1787ceab29b8dab3b07241a16ca1c9a7ed802662 Mon Sep 17 00:00:00 2001 From: Jay George Date: Thu, 27 Aug 2026 15:57:30 +0100 Subject: [PATCH 7/7] Require PATH-discovered FFmpeg binary to be executable which/where can return a stale non-executable path; treat that the same as a missing binary so video thumbnail URLs are not emitted. --- src/Console/Processes/Ffmpeg.php | 6 +++++- tests/Console/FfmpegTest.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Console/Processes/Ffmpeg.php b/src/Console/Processes/Ffmpeg.php index 345933ca8db..aefbd2c1c9b 100644 --- a/src/Console/Processes/Ffmpeg.php +++ b/src/Console/Processes/Ffmpeg.php @@ -90,7 +90,11 @@ private function resolveFfmpegBinary(): ?string ->explode("\n") ->first(); - return filled($resolved) ? $resolved : null; + if (! filled($resolved) || ! is_executable($resolved)) { + return null; + } + + return $resolved; } public static function clearBinaryCache(): void diff --git a/tests/Console/FfmpegTest.php b/tests/Console/FfmpegTest.php index afce7bdd5cd..4ea37cae9cf 100644 --- a/tests/Console/FfmpegTest.php +++ b/tests/Console/FfmpegTest.php @@ -54,6 +54,35 @@ public function it_memoizes_binary_resolution_across_instances() $this->assertNull((new Ffmpeg)->ffmpegBinary()); } + #[Test] + public function it_ignores_a_path_discovered_binary_that_is_not_executable() + { + Ffmpeg::clearBinaryCache(); + config(['statamic.assets.ffmpeg.binary' => null]); + + $path = storage_path('non-executable-ffmpeg'); + file_put_contents($path, ''); + chmod($path, 0644); + + $ffmpeg = new class($path) extends Ffmpeg + { + public function __construct(private string $discoveredPath) + { + parent::__construct(); + } + + public function run($command, $cacheKey = null) + { + return $this->discoveredPath; + } + }; + + $this->assertNull($ffmpeg->ffmpegBinary()); + $this->assertFalse($ffmpeg->available()); + + @unlink($path); + } + private function buildCommand(...$arguments) { $method = (new \ReflectionClass(Ffmpeg::class))->getMethod('buildCommand');