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) <[email protected]>
This commit is contained in:
1 parent
e0aa6e4817
commit
76f5ef0c84
5 files changed
+170
-68
No files matched your search
@@ -4,8 +4,6 @@ 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;
|
||||
@@ -13,7 +11,6 @@ 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
|
||||
{
|
||||
@@ -30,7 +27,6 @@ class CompressVideoJob implements ShouldQueue
|
||||
{
|
||||
$video = $this->video;
|
||||
|
||||
// Get original file path
|
||||
$originalPath = storage_path('app/' . $video->path);
|
||||
|
||||
if (!file_exists($originalPath)) {
|
||||
@@ -38,98 +34,114 @@ class CompressVideoJob implements ShouldQueue
|
||||
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);
|
||||
$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);
|
||||
|
||||
// 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();
|
||||
$encoder = Setting::gpuEncoder(); // h264_nvenc / libx264
|
||||
$preset = Setting::gpuPreset(); // p1–p7 for NVENC, fast/medium/slow for x264
|
||||
$device = Setting::gpuDevice();
|
||||
$hwaccel = Setting::gpuHwaccel();
|
||||
|
||||
if ($gpuEnabled) {
|
||||
$videoPasses = [
|
||||
"-c:v {$encoder}",
|
||||
"-preset {$preset}",
|
||||
'-rc vbr',
|
||||
'-cq 23',
|
||||
'-profile:v high',
|
||||
'-pix_fmt yuv420p',
|
||||
"-gpu {$device}",
|
||||
];
|
||||
// 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 {
|
||||
$videoPasses = [
|
||||
'-c:v libx264',
|
||||
'-preset fast',
|
||||
'-crf 23',
|
||||
'-profile:v high',
|
||||
'-pix_fmt yuv420p',
|
||||
];
|
||||
$cmd[] = "-preset {$preset}";
|
||||
$cmd[] = '-crf 23';
|
||||
}
|
||||
$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);
|
||||
$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}");
|
||||
}
|
||||
|
||||
// Check if compressed file was created and is smaller
|
||||
if (file_exists($compressedPath)) {
|
||||
$originalSize = filesize($originalPath);
|
||||
$originalSize = filesize($originalPath);
|
||||
$compressedSize = filesize($compressedPath);
|
||||
|
||||
// Only use compressed file if it's smaller
|
||||
if ($compressedSize < $originalSize) {
|
||||
// Delete original and rename compressed
|
||||
if ($compressedSize < $originalSize || $forceReplace) {
|
||||
unlink($originalPath);
|
||||
rename($compressedPath, $originalPath);
|
||||
|
||||
// Update video record
|
||||
$video->update([
|
||||
'size' => $compressedSize,
|
||||
'filename' => $video->filename, // Keep same filename
|
||||
'size' => $compressedSize,
|
||||
'mime_type' => 'video/mp4',
|
||||
]);
|
||||
|
||||
Log::info('CompressVideoJob: Video compressed', [
|
||||
'video_id' => $video->id,
|
||||
'original_size' => $originalSize,
|
||||
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,
|
||||
'saved' => round(($originalSize - $compressedSize) / max(1, $originalSize) * 100) . '%',
|
||||
'encoder' => $encoder,
|
||||
'gpu' => $gpuEnabled,
|
||||
'src_codec' => $srcCodec,
|
||||
'forced' => $forceReplace && $compressedSize >= $originalSize,
|
||||
]);
|
||||
} else {
|
||||
// Compressed file is larger, delete it
|
||||
unlink($compressedPath);
|
||||
Log::info('CompressVideoJob: Compression made file larger, keeping original');
|
||||
Log::info('CompressVideoJob: Compression made file larger, keeping original', ['src_codec' => $srcCodec]);
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
if (file_exists($compressedPath)) @unlink($compressedPath);
|
||||
$video->update(['status' => 'ready']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user