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:
ghassanandClaude Opus 4.7 committed 2026-08-08 02:08:25 +03:00
1 parent e0aa6e4817
commit 76f5ef0c84
5 files changed
+170 -68

No files matched your search

@@ -9,7 +9,7 @@
@php
// Force 16:9 for every video — orientation classes intentionally disabled
$orientationClass = '';
$hlsUrl = $video->has_hls ? route('videos.hls', ['video' => $video, 'file' => 'master.m3u8']) : null;
$hlsUrl = $video->has_hls ? route('videos.hls', ['video' => $video, 'file' => 'playlist.m3u8']) : null;
$mp4Url = route('videos.stream', $video) . '?v=' . $video->updated_at->timestamp;
$nextUrl = $nextVideo && $playlist ? route('videos.show', $nextVideo) .'?playlist='.$playlist->share_token : null;
$prevUrl = $previousVideo && $playlist ? route('videos.show', $previousVideo).'?playlist='.$playlist->share_token : null;
@@ -711,6 +711,55 @@
}
.utc-card:hover .utc-card-arr { opacity: 1; transform: translateX(0); }
/* ── Mobile: render the chooser as a bottom sheet ───────────── */
@media (max-width: 768px) {
.utc-overlay {
align-items: flex-end; justify-content: stretch;
padding: 0; background: rgba(0,0,0,.55);
backdrop-filter: blur(4px);
}
.utc-box {
max-width: none; width: 100%;
border-radius: 20px 20px 0 0;
border-left: none; border-right: none; border-bottom: none;
padding: 10px 18px calc(22px + env(safe-area-inset-bottom));
transform: translateY(100%); transition: transform .28s cubic-bezier(.22,1,.36,1);
box-shadow: 0 -12px 40px rgba(0,0,0,.6);
max-height: 88vh; overflow-y: auto;
}
.utc-overlay.show .utc-box { transform: translateY(0); }
/* Grabber handle at the top */
.utc-box::before {
content: ''; display: block; width: 40px; height: 4px;
background: #333; border-radius: 2px; margin: 4px auto 14px;
}
.utc-close { top: 12px; right: 12px; }
.utc-head { margin-bottom: 16px; }
.utc-head-icon { width: 44px; height: 44px; font-size: 20px; margin-bottom: 10px; }
.utc-title { font-size: 17px; }
.utc-sub { font-size: 12px; }
/* Row of compact tiles instead of full stacked cards */
.utc-grid { grid-template-columns: repeat(3, 1fr); gap: 10px; }
.utc-card {
padding: 16px 8px 14px; border-radius: 14px; border-width: 1px;
}
.utc-card-ico {
width: 44px; height: 44px; margin-bottom: 8px;
font-size: 20px; border-radius: 12px;
}
.utc-card-title { font-size: 13px; }
.utc-card-desc { display: none; }
.utc-card-arr { display: none; }
.utc-card:active {
transform: scale(.96);
background: color-mix(in srgb, var(--accent) 10%, #111);
border-color: var(--accent);
}
}
/* ════════════════════════════════════════════════════════════════
Unified control system — one consistent look for every field,
dropdown and picker in the upload modal. Scoped to #uploadModal
@@ -829,19 +878,28 @@ const _UTC_META = {
};
function openUploadChooser() {
// Mobile keeps the existing full-page create flow (with its own type picker)
if (window.innerWidth < 992) {
window.location.href = '{{ route("videos.create") }}';
return;
}
// Chooser is shown on every viewport — on mobile chooseUploadType() routes
// straight to the standalone create page with the picked ?type= preserved.
document.getElementById('upload-type-chooser').classList.add('show');
}
// Called from the desktop chooser cards on mobile — carries the picked type
// through to the standalone create page via ?type=.
function chooseUploadTypeMobile(type) {
const t = ['generic', 'music', 'match'].includes(type) ? type : 'generic';
window.location.href = '{{ route("videos.create") }}?type=' + encodeURIComponent(t);
}
function closeUploadChooser() {
document.getElementById('upload-type-chooser').classList.remove('show');
}
function chooseUploadType(type) {
// Mobile has no modal — route straight to the create page with the type preserved.
if (window.innerWidth < 992) {
chooseUploadTypeMobile(type);
return;
}
const meta = _UTC_META[type] || _UTC_META.generic;
closeUploadChooser();
openUploadModal();
+11
View File
@@ -784,6 +784,17 @@
setAudioMode(this.dataset.type === 'music');
});
});
// Honor ?type=generic|music|match forwarded from the desktop chooser
// when routed here on mobile — pre-select the corresponding option.
(function preselectTypeFromQuery() {
try {
const t = new URLSearchParams(window.location.search).get('type');
if (!t) return;
const opt = document.querySelector('#type-options .option-item[data-type="' + t + '"]');
if (opt) opt.click();
} catch (_) {}
}());
document.querySelectorAll('#visibility-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#visibility-options .option-item').forEach(o => o.classList.remove('active'));