- Installed p7h/nas-file-manager package via private VCS repo - Published config/nas-file-manager.php with super_admin middleware restriction - Added NAS env vars to .env.example - Created admin/nas-storage page with connection info panel and file browser widget - Added NAS Storage link to admin sidebar (super_admin only) - Added SuperAdminController@nasStorage method and admin.nas-storage route - Includes all accumulated branch changes: profile wall, 2FA, audit logs, settings panel, country/phone/timezone components, posts, slideshow, playlist shares, video downloads/shares, comment likes, notifications, social links, and more Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
5.2 KiB
PHP
158 lines
5.2 KiB
PHP
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Models\Setting;
|
||
use App\Models\Video;
|
||
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 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);
|
||
|
||
if (is_dir($hlsPath)) {
|
||
Storage::deleteDirectory($hlsDir);
|
||
}
|
||
Storage::makeDirectory($hlsDir);
|
||
|
||
$variants = [
|
||
['height' => 480, 'name' => '480p', 'bitrate' => '1000k'],
|
||
['height' => 720, 'name' => '720p', 'bitrate' => '2500k'],
|
||
['height' => 1080, 'name' => '1080p', 'bitrate' => '5000k'],
|
||
];
|
||
|
||
foreach ($variants as $v) {
|
||
@mkdir($hlsPath . '/' . $v['name'], 0755, true);
|
||
}
|
||
|
||
try {
|
||
$ffmpegBin = \App\Models\Setting::ffmpegBinary();
|
||
$gpuEnabled = Setting::gpuEnabled();
|
||
$encoder = Setting::gpuEncoder(); // h264_nvenc / hevc_nvenc / libx264
|
||
$preset = Setting::gpuPreset(); // p1–p7 for NVENC, fast/medium/slow for x264
|
||
$device = Setting::gpuDevice(); // GPU index (0, 1, …)
|
||
$hwaccel = Setting::gpuHwaccel(); // cuda / none
|
||
|
||
$cmd = [escapeshellcmd($ffmpegBin)];
|
||
|
||
// Hardware-accelerated decode when GPU is in use
|
||
if ($gpuEnabled && $hwaccel !== 'none') {
|
||
$cmd[] = "-hwaccel {$hwaccel}";
|
||
$cmd[] = "-hwaccel_device {$device}";
|
||
}
|
||
|
||
$cmd[] = '-i ' . escapeshellarg($sourcePath);
|
||
|
||
// One video+audio stream per variant
|
||
$n = count($variants);
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$cmd[] = '-map 0:v:0';
|
||
$cmd[] = '-map 0:a:0?';
|
||
}
|
||
|
||
// Video codec
|
||
$cmd[] = "-c:v {$encoder}";
|
||
if ($gpuEnabled && str_contains($encoder, 'nvenc')) {
|
||
$cmd[] = "-preset {$preset}";
|
||
$cmd[] = '-rc vbr';
|
||
$cmd[] = '-cq 23';
|
||
$cmd[] = "-gpu {$device}";
|
||
} else {
|
||
$cmd[] = "-preset {$preset}";
|
||
$cmd[] = '-crf 23';
|
||
}
|
||
$cmd[] = '-pix_fmt yuv420p';
|
||
|
||
// Audio codec
|
||
$cmd[] = '-c:a aac';
|
||
$cmd[] = '-b:a 128k';
|
||
$cmd[] = '-ar 48000';
|
||
|
||
// Per-variant scale + bitrate
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$cmd[] = "-filter:v:{$i} scale=-2:{$variants[$i]['height']}";
|
||
$cmd[] = "-b:v:{$i} {$variants[$i]['bitrate']}";
|
||
}
|
||
|
||
// HLS muxer options
|
||
$cmd[] = '-g 48';
|
||
$cmd[] = '-sc_threshold 0';
|
||
$cmd[] = '-f hls';
|
||
$cmd[] = '-hls_time 6';
|
||
$cmd[] = '-hls_list_size 0';
|
||
$cmd[] = '-hls_flags independent_segments';
|
||
$cmd[] = '-hls_segment_filename ' . escapeshellarg($hlsPath . '/%v/%03d.ts');
|
||
$cmd[] = '-master_pl_name playlist.m3u8';
|
||
|
||
$vsm = implode(' ', array_map(
|
||
fn ($i, $v) => "v:{$i},a:{$i},name:{$v['name']}",
|
||
array_keys($variants),
|
||
$variants
|
||
));
|
||
$cmd[] = '-var_stream_map ' . escapeshellarg($vsm);
|
||
|
||
$cmd[] = escapeshellarg($hlsPath . '/%v/index.m3u8');
|
||
|
||
$fullCmd = implode(' ', $cmd) . ' 2>&1';
|
||
|
||
Log::info('GenerateHlsJob: Starting', [
|
||
'video_id' => $video->id,
|
||
'gpu' => $gpuEnabled,
|
||
'encoder' => $encoder,
|
||
'device' => $gpuEnabled ? $device : 'cpu',
|
||
]);
|
||
|
||
exec($fullCmd, $output, $exitCode);
|
||
|
||
if ($exitCode !== 0) {
|
||
$tail = implode("\n", array_slice($output, -30));
|
||
throw new \RuntimeException("FFmpeg exited {$exitCode}:\n{$tail}");
|
||
}
|
||
|
||
$video->update(['has_hls' => true, 'hls_path' => $hlsDir]);
|
||
|
||
Log::info('GenerateHlsJob: HLS generated', [
|
||
'video_id' => $video->id,
|
||
'variants' => array_column($variants, 'name'),
|
||
'encoder' => $encoder,
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('GenerateHlsJob failed: ' . $e->getMessage(), ['video_id' => $video->id]);
|
||
Storage::deleteDirectory($hlsDir);
|
||
}
|
||
}
|
||
}
|