Add image caching for playlist items

This commit is contained in:
zv0n 2025-05-01 19:06:28 +02:00
parent 81f721099c
commit 6bfd9a2d8b
6 changed files with 173 additions and 85 deletions

View file

@ -0,0 +1,81 @@
<?php
namespace App\Jobs;
use App\Models\Device;
use App\Models\PlaylistItem;
use Illuminate\Support\Facades\Storage;
use Ramsey\Uuid\Uuid;
use Spatie\Browsershot\Browsershot;
use Wnx\SidecarBrowsershot\BrowsershotLambda;
class CommonFunctions
{
public static function generateImage(string $markup): string {
$uuid = Uuid::uuid4()->toString();
$pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png');
$bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp');
// Generate PNG
if (config('app.puppeteer_mode') === 'sidecar-aws') {
try {
BrowsershotLambda::html($markup)
->windowSize($device->width ?? 800, $device->height ?? 480)
->save($pngPath);
} catch (\Exception $e) {
throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e);
}
} else {
try {
Browsershot::html($markup)
->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : [])
->windowSize($device->width ?? 800, $device->height ?? 480)
->save($pngPath);
} catch (\Exception $e) {
throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e);
}
}
try {
CommonFunctions::convertToBmpImageMagick($pngPath, $bmpPath);
} catch (\ImagickException $e) {
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
}
return $uuid;
}
/**
* @throws \ImagickException
*/
private static function convertToBmpImageMagick(string $pngPath, string $bmpPath): void
{
$imagick = new \Imagick($pngPath);
$imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
$imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false);
$imagick->setImageDepth(1);
$imagick->stripImage();
$imagick->setFormat('BMP3');
$imagick->writeImage($bmpPath);
$imagick->clear();
}
public static function cleanupFolder(): void
{
$activeDeviceImageUuids = Device::pluck('current_screen_image')->filter()->toArray();
$activePlaylistImageUuids = PlaylistItem::pluck('current_image')->filter()->toArray();
$activeImageUuids = array_merge($activeDeviceImageUuids, $activePlaylistImageUuids);
$files = Storage::disk('public')->files('/images/generated/');
foreach ($files as $file) {
if (basename($file) === '.gitignore') {
continue;
}
// Get filename without path and extension
$fileUuid = pathinfo($file, PATHINFO_FILENAME);
// If the UUID is not in use by any device, move it to archive
if (! in_array($fileUuid, $activeImageUuids)) {
Storage::disk('public')->delete($file);
}
}
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Jobs;
use App\Models\PlaylistItem;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GeneratePlaylistItemJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
private readonly int $playlistItemId,
private readonly string $markup
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$newImageUuid = CommonFunctions::generateImage($this->markup);
PlaylistItem::find($this->playlistItemId)->update(['current_image' => $newImageUuid]);
\Log::info("Playlist item $this->playlistItemId: updated with new image: $newImageUuid");
CommonFunctions::cleanupFolder();
}
}

View file

@ -8,10 +8,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Ramsey\Uuid\Uuid;
use Spatie\Browsershot\Browsershot;
use Wnx\SidecarBrowsershot\BrowsershotLambda;
class GenerateScreenJob implements ShouldQueue
{
@ -30,72 +26,11 @@ class GenerateScreenJob implements ShouldQueue
*/
public function handle(): void
{
$device = Device::find($this->deviceId);
$uuid = Uuid::uuid4()->toString();
$pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png');
$bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp');
$newImageUuid = CommonFunctions::generateImage($this->markup);
// Generate PNG
if (config('app.puppeteer_mode') === 'sidecar-aws') {
try {
BrowsershotLambda::html($this->markup)
->windowSize($device->width ?? 800, $device->height ?? 480)
->save($pngPath);
} catch (\Exception $e) {
throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e);
}
} else {
try {
Browsershot::html($this->markup)
->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : [])
->windowSize($device->width ?? 800, $device->height ?? 480)
->save($pngPath);
} catch (\Exception $e) {
throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e);
}
}
Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]);
\Log::info("Device $this->deviceId: updated with new image: $newImageUuid");
try {
$this->convertToBmpImageMagick($pngPath, $bmpPath);
} catch (\ImagickException $e) {
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
}
$device->update(['current_screen_image' => $uuid]);
\Log::info("Device $this->deviceId: updated with new image: $uuid");
$this->cleanupFolder();
}
/**
* @throws \ImagickException
*/
private function convertToBmpImageMagick(string $pngPath, string $bmpPath): void
{
$imagick = new \Imagick($pngPath);
$imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
$imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false);
$imagick->setImageDepth(1);
$imagick->stripImage();
$imagick->setFormat('BMP3');
$imagick->writeImage($bmpPath);
$imagick->clear();
}
private function cleanupFolder(): void
{
$activeImageUuids = Device::pluck('current_screen_image')->filter()->toArray();
$files = Storage::disk('public')->files('/images/generated/');
foreach ($files as $file) {
if (basename($file) === '.gitignore') {
continue;
}
// Get filename without path and extension
$fileUuid = pathinfo($file, PATHINFO_FILENAME);
// If the UUID is not in use by any device, move it to archive
if (! in_array($fileUuid, $activeImageUuids)) {
Storage::disk('public')->delete($file);
}
}
CommonFunctions::cleanupFolder();
}
}

View file

@ -15,6 +15,7 @@ class PlaylistItem extends Model
protected $casts = [
'is_active' => 'boolean',
'last_displayed_at' => 'datetime',
'current_image' => 'string',
];
public function playlist(): BelongsTo

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('playlist_items', function (Blueprint $table) {
$table->string('current_image')->nullable()->after('is_active');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('playlist_items', function (Blueprint $table) {
$table->dropColumn('current_image');
});
}
};

View file

@ -1,6 +1,7 @@
<?php
use App\Jobs\GenerateScreenJob;
use App\Jobs\GeneratePlaylistItemJob;
use App\Models\Device;
use App\Models\User;
use Illuminate\Http\Request;
@ -46,28 +47,33 @@ Route::get('/display', function (Request $request) {
// Get current screen image from mirror device or continue if not available
if (! $image_uuid = $device->mirrorDevice?->current_screen_image) {
$refreshTimeOverride = null;
$nextPlaylistItem = $device->getNextPlaylistItem();
// Skip if cloud proxy is enabled for the device
if (! $device->proxy_cloud || $device->getNextPlaylistItem()) {
$playlistItem = $device->getNextPlaylistItem();
if ($playlistItem) {
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
$plugin = $playlistItem->plugin;
if (! $device->proxy_cloud || $nextPlaylistItem) {
if ($nextPlaylistItem) {
$refreshTimeOverride = $nextPlaylistItem->playlist()->first()->refresh_time;
$plugin = $nextPlaylistItem->plugin;
// Check and update stale data if needed
if ($plugin->isDataStale()) {
if ($plugin->isDataStale() || $nextPlaylistItem->last_displayed_at == null) {
$plugin->updateDataPayload();
}
$playlistItem->update(['last_displayed_at' => now()]);
if ($plugin->render_markup) {
$markup = Blade::render($plugin->render_markup, ['data' => $plugin->data_payload]);
} elseif ($plugin->render_markup_view) {
$markup = view($plugin->render_markup_view, ['data' => $plugin->data_payload])->render();
}
if ($plugin->render_markup) {
$markup = Blade::render($plugin->render_markup, ['data' => $plugin->data_payload]);
} elseif ($plugin->render_markup_view) {
$markup = view($plugin->render_markup_view, ['data' => $plugin->data_payload])->render();
}
GenerateScreenJob::dispatchSync($device->id, $markup);
GeneratePlaylistItemJob::dispatchSync($nextPlaylistItem->id, $markup);
}
}
$nextPlaylistItem->refresh();
if ($nextPlaylistItem->current_image != null)
{
$nextPlaylistItem->update(['last_displayed_at' => now()]);
$device->update(['current_screen_image' => $nextPlaylistItem->current_image]);
}
}