130 lines
4.1 KiB
PHP
130 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\Video;
|
|
use FFMpeg\FFMpeg;
|
|
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\Config;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class GenerateHlsJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public $video;
|
|
|
|
public function __construct(Video $video)
|
|
{
|
|
$this->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);
|
|
}
|
|
}
|
|
}
|
|
|