video = $video; $this->onQueue('video-processing'); } public function handle() { $video = $this->video->fresh(); if ($video->status !== 'ready') { Log::warning('GenerateHlsJob: Video not ready', ['id' => $video->id]); return; } $sourcePath = storage_path('app/' . $video->path); if (!file_exists($sourcePath)) { Log::error('GenerateHlsJob: Source file missing', ['path' => $sourcePath]); return; } $hlsDir = 'public/hls/' . $video->id; $hlsPath = storage_path('app/' . $hlsDir); // Clean existing HLS if (is_dir($hlsPath)) { Storage::deleteDirectory($hlsDir); } Storage::makeDirectory($hlsDir); 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, ]); $videoMedia = $ffmpeg->open($sourcePath); // HLS variants: 480p, 720p, 1080p $variants = [ [ 'height' => 480, 'name' => '480p', 'bitrate' => 1000, ], [ 'height' => 720, 'name' => '720p', 'bitrate' => 2500, ], [ 'height' => 1080, 'name' => '1080p', 'bitrate' => 5000, ], ]; $hlsOptions = [ '-c:v h264_nvenc', '-preset p4', '-g 48', // GOP size 2s @25fps '-sc_threshold 0', '-c:a aac', '-ar 48000', '-f hls', '-hls_time 6', '-hls_list_size 0', '-hls_segment_filename', '%v/%03d.ts', '-hls_flags', 'delete_segments+append_list', '-master_pl_name', 'playlist.m3u8', ]; $videoMedia->save(new \FFMpeg\Format\Video\X264(), $hlsPath, function ($filters) use ($variants) { foreach ($variants as $variant) { $filters->custom('-map 0:v:0 -map 0:a:0?') ->size("trunc(oh*a/oh/{$variant['height']})*{$variant['height']}") // Scale ->resize(new \FFMpeg\Coordinate\Dimension($variant['height'] * 16 / 9, $variant['height'])) ->videoCodec($variant['bitrate'] . 'k') ->addLegacyOption('-var_stream_map v:0,name:' . $variant['name'] . ' v:1,name:720p v:2,name:1080p'); } }); // Mark HLS ready $video->update([ 'has_hls' => true, 'hls_path' => $hlsDir, ]); Log::info('GenerateHlsJob: HLS generated successfully', [ 'video_id' => $video->id, 'variants' => array_column($variants, 'name'), 'hls_url' => asset('storage/' . $hlsDir . '/playlist.m3u8'), ]); } catch (\Exception $e) { Log::error('GenerateHlsJob failed: ' . $e->getMessage(), ['video_id' => $video->id]); Storage::deleteDirectory($hlsDir); } } }