Skip to content

Commit 04d2802

Browse files
authored
[6.x] Fix video thumbnails showing as a broken image (#15291)
1 parent fd1e874 commit 04d2802

13 files changed

Lines changed: 259 additions & 8 deletions

File tree

config/assets.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,9 @@
148148
| Control Panel Video Thumbnails
149149
|--------------------------------------------------------------------------
150150
|
151-
| When enabled, Statamic will generate thumbnails for videos.
152-
| Generated thumbnails are displayed in the Control Panel.
151+
| When enabled, Statamic will generate thumbnails for videos when FFmpeg
152+
| is available. Generated thumbnails are displayed in the Control Panel.
153+
| Without FFmpeg, videos fall back to a filetype icon.
153154
|
154155
*/
155156

@@ -272,6 +273,7 @@
272273
|
273274
| Statamic uses FFmpeg to extract thumbnails from videos to be shown in the
274275
| Control Panel. You may adjust the binary location and cache path here.
276+
| The configured binary must exist and be executable.
275277
|
276278
*/
277279

resources/js/components/assets/Browser/Grid.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
'w-full p-4': asset.extension === 'svg',
129129
'rounded-lg p-1': asset.orientation === 'square',
130130
}"
131+
@error="asset.thumbnail = null"
131132
/>
132133
<file-icon v-else :extension="asset.extension" class="size-1/2" />
133134
</div>

resources/js/components/assets/Browser/Thumbnail.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
loading="lazy"
88
:draggable="false"
99
:class="{ 'h-8 w-8 object-cover': square }"
10+
@error="asset.thumbnail = null"
1011
/>
1112
<img
1213
v-else-if="asset.is_svg"

resources/js/components/fieldtypes/assets/AssetRow.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
:src="thumbnail"
1919
:alt="asset.basename"
2020
v-if="thumbnail"
21+
@error="asset.thumbnail = null"
2122
/>
2223
<file-icon :extension="asset.extension ?? 'generic'" v-else class="size-7" />
2324
</button>

resources/js/components/fieldtypes/assets/AssetTile.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
<img v-if="canShowSvg" :src="asset.url" :title="label" class="p-4 w-full relative" />
3737

3838
<template v-else>
39-
<img :src="thumbnail" v-if="thumbnail" :title="label" class="rounded-md relative" />
39+
<img :src="thumbnail" v-if="thumbnail" :title="label" class="rounded-md relative" @error="asset.thumbnail = null" />
4040

4141
<file-icon v-else :extension="asset.extension ?? 'generic'" class="h-full w-full p-4 relative" />
4242
</template>

src/Console/Processes/Ffmpeg.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class Ffmpeg extends Process
88
{
99
protected string $startTimestamp = '00:00:00';
1010

11+
private static bool $binaryResolved = false;
12+
13+
private static ?string $resolvedBinary = null;
14+
1115
public function startTimestamp(string $startTimestamp): self
1216
{
1317
$this->startTimestamp = $startTimestamp;
@@ -49,10 +53,26 @@ private function buildCommand(string $ffmpegBinary, string $path, string $output
4953
])->join(' ');
5054
}
5155

56+
public function available(): bool
57+
{
58+
return filled($this->ffmpegBinary());
59+
}
60+
5261
public function ffmpegBinary(): ?string
62+
{
63+
if (static::$binaryResolved) {
64+
return static::$resolvedBinary;
65+
}
66+
67+
static::$binaryResolved = true;
68+
69+
return static::$resolvedBinary = $this->resolveFfmpegBinary();
70+
}
71+
72+
private function resolveFfmpegBinary(): ?string
5373
{
5474
if ($binary = config('statamic.assets.ffmpeg.binary')) {
55-
return $binary;
75+
return is_executable($binary) ? $binary : null;
5676
}
5777

5878
$output = $this->run($this->isWindows() ? 'where ffmpeg' : 'which ffmpeg');
@@ -66,8 +86,20 @@ public function ffmpegBinary(): ?string
6686
return null;
6787
}
6888

69-
return str(StringUtilities::normalizeLineEndings(trim($output)))
89+
$resolved = str(StringUtilities::normalizeLineEndings(trim($output)))
7090
->explode("\n")
7191
->first();
92+
93+
if (! filled($resolved) || ! is_executable($resolved)) {
94+
return null;
95+
}
96+
97+
return $resolved;
98+
}
99+
100+
public static function clearBinaryCache(): void
101+
{
102+
static::$binaryResolved = false;
103+
static::$resolvedBinary = null;
72104
}
73105
}

src/Http/Controllers/CP/Assets/ThumbnailController.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,15 @@ public function show($asset, $size = null, $orientation = null)
7171
return $placeholder;
7272
}
7373

74+
$path = $this->generate();
75+
76+
if (! $path) {
77+
return $this->getUnavailableThumbnailResponse();
78+
}
79+
7480
return $this->server->getResponseFactory()->create(
7581
$this->server->getCache(),
76-
$this->generate()
82+
$path
7783
);
7884
}
7985

@@ -189,4 +195,18 @@ private function getPlaceholderResponse()
189195

190196
return response(Statamic::svg('filetypes/picture'))->header('Content-Type', 'image/svg+xml');
191197
}
198+
199+
/**
200+
* When thumbnail generation fails (e.g. FFmpeg missing for videos), show a filetype icon.
201+
*
202+
* @return \Illuminate\Http\Response
203+
*/
204+
private function getUnavailableThumbnailResponse()
205+
{
206+
$svg = $this->asset->isVideo()
207+
? Statamic::svg('filetypes/video')
208+
: Statamic::svg('filetypes/picture');
209+
210+
return response($svg)->header('Content-Type', 'image/svg+xml');
211+
}
192212
}

src/Http/Resources/CP/Assets/HasThumbnails.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Statamic\Http\Resources\CP\Assets;
44

55
use Illuminate\Support\Fluent;
6+
use Statamic\Imaging\ThumbnailExtractor;
67
use Statamic\Support\Traits\Hookable;
78

89
trait HasThumbnails
@@ -13,7 +14,7 @@ private function thumbnails(): array
1314
{
1415
$data = match (true) {
1516
$this->isImage() || $this->isSvg() => $this->getImageThumbnail(),
16-
$this->isVideo() && config('statamic.assets.video_thumbnails', true) => $this->getVideoThumbnail(),
17+
$this->isVideo() && ThumbnailExtractor::available() => $this->getVideoThumbnail(),
1718
default => ['thumbnail' => null],
1819
};
1920

src/Imaging/ImageGenerator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,14 @@ public function generateVideoThumbnail($asset, array $params)
155155
*/
156156
public function generateByAsset($asset, array $params)
157157
{
158-
if (ThumbnailExtractor::enabled() && $asset->isVideo()) {
158+
if (ThumbnailExtractor::available() && $asset->isVideo()) {
159159
return $this->generateVideoThumbnail($asset, $params);
160160
}
161161

162+
if ($asset->isVideo()) {
163+
return '';
164+
}
165+
162166
$manipulationCacheKey = 'asset::'.$asset->id().'::'.md5(json_encode($params));
163167
$manifestCacheKey = static::assetCacheManifestKey($asset);
164168

src/Imaging/ThumbnailExtractor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public static function enabled()
2020
);
2121
}
2222

23+
public static function available()
24+
{
25+
return static::enabled() && app(Ffmpeg::class)->available();
26+
}
27+
2328
public static function cachePath()
2429
{
2530
return config(

0 commit comments

Comments
 (0)