takeone-youtube-clone/app/Jobs/CompressVideoJob.php
ghassan 76f5ef0c84 Fix match-video playback: HEVC → H.264 transcode + correct HLS URL
- CompressVideoJob: rewrite to exec ffmpeg directly. php-ffmpeg's X264
  format class validates the codec name and only accepts libx264, so
  every job dispatched with h264_nvenc had been throwing within ~1s and
  leaving the source untouched. The job now honours the GPU encoder
  settings, and always replaces the source when the codec is HEVC/H.265
  so downloads and the MP4 fallback stay playable.
- VideoController + video-player component: point HLS URLs at
  playlist.m3u8 (the actual master file GenerateHlsJob writes) instead
  of the non-existent master.m3u8, which was 404-ing and forcing the
  player back to the raw MP4.
- VideoController@store: on the NAS-success branch, flip status to
  'ready' and populate duration/is_shorts before dispatching
  GenerateHlsJob, so the job no longer silently no-ops.
- Upload chooser: render as a bottom sheet on mobile, and forward the
  picked type to /videos/create?type= so the create page pre-selects it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 02:08:25 +03:00

148 lines
5.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Jobs;
use App\Models\Setting;
use App\Models\Video;
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;
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;
$originalPath = storage_path('app/' . $video->path);
if (!file_exists($originalPath)) {
Log::error('CompressVideoJob: Original file not found: ' . $originalPath);
return;
}
$compressedFilename = 'compressed_' . $video->filename;
$compressedPath = dirname($originalPath) . '/' . $compressedFilename;
try {
$ffmpegConfig = Config::get('ffmpeg');
$ffmpegBin = $ffmpegConfig['ffmpeg'] ?? '/usr/bin/ffmpeg';
$ffprobeBin = $ffmpegConfig['ffprobe'] ?? '/usr/bin/ffprobe';
// Detect the source video codec. Browsers on Chrome/Firefox/Android can't
// decode HEVC (H.265/hev1/hvc1) — those sources must be replaced with H.264
// even if the transcode is larger, so downloads and MP4 fallback stay playable.
$srcCodec = '';
try {
$probeOut = [];
exec(escapeshellcmd($ffprobeBin) . ' -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 ' . escapeshellarg($originalPath), $probeOut);
$srcCodec = strtolower(trim($probeOut[0] ?? ''));
} catch (\Throwable $e) {}
$forceReplace = in_array($srcCodec, ['hevc', 'h265', 'hev1', 'hvc1'], true);
$gpuEnabled = Setting::gpuUsable();
$encoder = Setting::gpuEncoder(); // h264_nvenc / libx264
$preset = Setting::gpuPreset(); // p1p7 for NVENC, fast/medium/slow for x264
$device = Setting::gpuDevice();
$hwaccel = Setting::gpuHwaccel();
// Build ffmpeg command directly — php-ffmpeg's X264 format validator
// only accepts 'libx264' and throws on 'h264_nvenc'.
$cmd = [escapeshellcmd($ffmpegBin), '-y'];
if ($gpuEnabled && $hwaccel !== 'none') {
$cmd[] = "-hwaccel {$hwaccel}";
$cmd[] = "-hwaccel_device {$device}";
}
$cmd[] = '-i ' . escapeshellarg($originalPath);
$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[] = '-profile:v high';
$cmd[] = '-pix_fmt yuv420p';
$cmd[] = '-c:a aac';
$cmd[] = '-b:a 192k';
$cmd[] = '-movflags +faststart';
$cmd[] = escapeshellarg($compressedPath);
$fullCmd = implode(' ', $cmd) . ' 2>&1';
Log::info('CompressVideoJob: Starting', [
'video_id' => $video->id,
'src_codec' => $srcCodec,
'encoder' => $encoder,
'gpu' => $gpuEnabled,
]);
$output = [];
exec($fullCmd, $output, $exitCode);
if ($exitCode !== 0) {
$tail = implode("\n", array_slice($output, -30));
throw new \RuntimeException("FFmpeg exited {$exitCode}:\n{$tail}");
}
if (file_exists($compressedPath)) {
$originalSize = filesize($originalPath);
$compressedSize = filesize($compressedPath);
if ($compressedSize < $originalSize || $forceReplace) {
unlink($originalPath);
rename($compressedPath, $originalPath);
$video->update([
'size' => $compressedSize,
'mime_type' => 'video/mp4',
]);
Log::info('CompressVideoJob: Video compressed', [
'video_id' => $video->id,
'original_size' => $originalSize,
'compressed_size' => $compressedSize,
'saved' => round(($originalSize - $compressedSize) / max(1, $originalSize) * 100) . '%',
'encoder' => $encoder,
'gpu' => $gpuEnabled,
'src_codec' => $srcCodec,
'forced' => $forceReplace && $compressedSize >= $originalSize,
]);
} else {
unlink($compressedPath);
Log::info('CompressVideoJob: Compression made file larger, keeping original', ['src_codec' => $srcCodec]);
}
}
$video->update(['status' => 'ready']);
\App\Jobs\GenerateHlsJob::dispatch($video);
} catch (\Exception $e) {
Log::error('CompressVideoJob failed: ' . $e->getMessage());
if (file_exists($compressedPath)) @unlink($compressedPath);
$video->update(['status' => 'ready']);
}
}
}