takeone-youtube-clone/app/Jobs/CompressVideoJob.php
ghassan a4384113c2 Audio songs: one-folder storage, version-aware download/share, GPU-checked renders
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
  unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
  subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
  updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).

Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
  is carried through share links and auto-selects audio + title + flag + about +
  OG/meta on open.

GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
  256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
  frequency bars; loop-filter rebuild makes generation ~25x faster.

Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:03:43 +03:00

136 lines
4.8 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\Setting;
use App\Models\Video;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Video\X264;
use Illuminate\Support\Facades\Config;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class CompressVideoJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $video;
public function __construct(Video $video)
{
$this->video = $video;
}
public function handle()
{
$video = $this->video;
// Get original file path
$originalPath = storage_path('app/' . $video->path);
if (!file_exists($originalPath)) {
Log::error('CompressVideoJob: Original file not found: ' . $originalPath);
return;
}
// Create compressed file alongside the original
$compressedFilename = 'compressed_' . $video->filename;
$compressedPath = dirname($originalPath) . '/' . $compressedFilename;
try {
$ffmpegConfig = Config::get('ffmpeg');
$ffmpeg = FFMpeg::create([
'ffmpeg.binaries' => $ffmpegConfig['ffmpeg'] ?? '/usr/bin/ffmpeg',
'ffprobe.binaries' => $ffmpegConfig['ffprobe'] ?? '/usr/bin/ffprobe',
'timeout' => $ffmpegConfig['timeout'] ?? 3600,
]);
$ffmpegVideo = $ffmpeg->open($originalPath);
// Verify the GPU is actually reachable and able to encode before sending
// the file to it; otherwise fall back to CPU so the job never hangs.
$gpuEnabled = Setting::gpuUsable();
$encoder = Setting::gpuEncoder();
$preset = Setting::gpuPreset();
$device = Setting::gpuDevice();
if ($gpuEnabled) {
$videoPasses = [
"-c:v {$encoder}",
"-preset {$preset}",
'-rc vbr',
'-cq 23',
'-profile:v high',
'-pix_fmt yuv420p',
"-gpu {$device}",
];
} else {
$videoPasses = [
'-c:v libx264',
'-preset fast',
'-crf 23',
'-profile:v high',
'-pix_fmt yuv420p',
];
}
$audioPasses = ['-c:a aac', '-b:a 192k'];
$format = new X264('aac', $encoder);
foreach ($videoPasses as $pass) {
$format->addLegacyOption($pass);
}
foreach ($audioPasses as $pass) {
$format->addLegacyOption($pass);
}
$ffmpegVideo->save($format, $compressedPath);
// Check if compressed file was created and is smaller
if (file_exists($compressedPath)) {
$originalSize = filesize($originalPath);
$compressedSize = filesize($compressedPath);
// Only use compressed file if it's smaller
if ($compressedSize < $originalSize) {
// Delete original and rename compressed
unlink($originalPath);
rename($compressedPath, $originalPath);
// Update video record
$video->update([
'size' => $compressedSize,
'filename' => $video->filename, // Keep same filename
'mime_type' => 'video/mp4',
]);
Log::info('CompressVideoJob: Video compressed', [
'video_id' => $video->id,
'original_size' => $originalSize,
'compressed_size' => $compressedSize,
'saved' => round(($originalSize - $compressedSize) / $originalSize * 100) . '%',
'encoder' => $encoder,
'gpu' => $gpuEnabled,
]);
} else {
// Compressed file is larger, delete it
unlink($compressedPath);
Log::info('CompressVideoJob: Compression made file larger, keeping original');
}
}
$video->update(['status' => 'ready']);
// Chain to HLS generation for GPU-accelerated adaptive playback
\App\Jobs\GenerateHlsJob::dispatch($video);
} catch (\Exception $e) {
Log::error('CompressVideoJob failed: ' . $e->getMessage());
$video->update(['status' => 'ready']); // Mark as ready anyway
}
}
}