Keep mobile controls visible and make recorded VOD remux seekable

This commit is contained in:
Technomancer702
2026-10-13 22:00:19 -07:00
parent a922c6dbb5
commit f72675030a
13 changed files with 172 additions and 45 deletions

View File

@ -3363,8 +3363,8 @@ kbd {
height: calc(100vh - var(--navbar-height));
/* fallback */
height: calc(100dvh - var(--navbar-height));
/* dynamic viewport for modern browsers */
min-height: 400px;
/* Keep controls inside short landscape viewports too. */
min-height: 0;
background: #000;
}

View File

@ -10,7 +10,7 @@
<title>nodecast-tv | IPTV Player</title>
<meta name="description" content="Stream live TV, movies, and series with EPG support">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/main.css?v=2.2.7">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@800&display=swap"
@ -1105,7 +1105,7 @@
<script src="/js/api.js?v=auth-2"></script>
<script src="/js/components/PlaybackSessions.js?v=1"></script>
<script src="/js/components/VodDuration.js?v=1"></script>
<script src="/js/components/CaptionTracks.js?v=3"></script>
<script src="/js/components/CaptionTracks.js?v=4"></script>
<script src="/js/components/HlsAudioRecovery.js?v=1"></script>
<script src="/js/components/PlaybackProbe.js?v=2"></script>
<script src="/js/components/PlayerFullscreen.js?v=1"></script>
@ -1119,7 +1119,7 @@
<script src="/js/pages/MoviesPage.js?v=4"></script>
<script src="/js/pages/SeriesPage.js?v=1"></script>
<script src="/js/pages/Settings.js?v=2"></script>
<script src="/js/pages/WatchPage.js?v=9"></script>
<script src="/js/pages/WatchPage.js?v=10"></script>
<script src="/js/app.js?v=5"></script>
</body>

View File

@ -50,6 +50,7 @@ class CaptionTracks {
this.generation++;
this.hls = null;
this.probe = null;
this.pendingRemoteSelection = null;
this.timeOffset = 0;
this.retireNativeTracks();
this.onChange();
@ -124,9 +125,18 @@ class CaptionTracks {
this.remoteTracks.push(element);
this.video.appendChild(element);
}
const selected = this.remoteTracks[this.pendingRemoteSelection];
this.pendingRemoteSelection = null;
if (selected) selected.track.mode = 'showing';
}
captureRemoteSelection() {
this.pendingRemoteSelection = this.remoteTracks.findIndex(element => element.track.mode === 'showing');
}
bindHls(hls, events) {
const selected = this.remoteTracks.findIndex(element => element.track.mode === 'showing');
if (selected >= 0) this.pendingRemoteSelection = selected;
this.generation++;
this.hls = hls;
this.manifestReady = false;

View File

@ -117,15 +117,6 @@ class WatchPage {
window.addEventListener('resize', updateIosUiBottom);
}
// iOS: use custom --vh unit to avoid 100vh issues with dynamic toolbar
const isIOS = /iP(hone|ad|od)/.test(navigator.userAgent);
const watchVideoSection = document.querySelector('.watch-video-section');
if (isIOS && watchVideoSection) {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
watchVideoSection.style.height = 'calc(var(--vh) * 100)';
}
// Apply safe area + iOS toolbar padding to overlay
if (this.overlay) {
this.overlay.style.paddingBottom = 'calc(env(safe-area-inset-bottom, 0px) + var(--ios-ui-bottom, 0px) + 12px)';
@ -481,17 +472,9 @@ class WatchPage {
this.setVolumeFromStorage();
return;
} else if (info.needsRemux) {
// Remux (container swap) currently doesn't use session logic, uses direct stream
// TODO: Move remux to session logic if seeking is needed for TS files
console.log('[WatchPage] Auto: Using remux (.ts container)');
console.log('[WatchPage] Auto: Using seekable remux session');
this.updateTranscodeStatus('remuxing', 'Remux (Auto)');
this.getCaptionTracks().useNative();
const finalUrl = this.getRemuxUrl(url, info.audioCodecs || [info.audio], program);
this.video.src = finalUrl;
this.video.play().catch(e => {
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
});
this.setVolumeFromStorage();
await this.playRemuxSession(url, info, program, playbackGeneration);
return;
}
// Compatible - fall through to normal playback
@ -563,14 +546,7 @@ class WatchPage {
}
}
if (this.playbackGeneration !== playbackGeneration) return;
this.getCaptionTracks().setProbe(probeInfo, url);
this.getCaptionTracks().useNative();
const finalUrl = this.getRemuxUrl(url, probeInfo?.audioCodecs || (probeInfo?.audio ? [probeInfo.audio] : []), program);
this.video.src = finalUrl;
this.video.play().catch(e => {
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
});
this.setVolumeFromStorage();
await this.playRemuxSession(url, probeInfo, program, playbackGeneration);
return;
}
@ -598,11 +574,22 @@ class WatchPage {
this.setVolumeFromStorage();
}
/**
* Play HLS stream using Hls.js
*/
/** Start a seekable VOD session without re-encoding compatible video. */
async playRemuxSession(url, info, program, generation) {
this.getCaptionTracks().setProbe(info, url);
const playlist = await this.startTranscodeSession(url, {
program, videoMode: 'copy', seekOffset: this.resumeTime,
videoCodec: info?.video, audioCodec: info?.audio,
audioChannels: info?.audioChannels, videoHeight: info?.height
});
if (!playlist || this.playbackGeneration !== generation) return;
this.playHls(playlist);
this.setVolumeFromStorage();
}
playHls(url) {
if (this.hls) {
this.captionTracks?.captureRemoteSelection();
this.hls.destroy();
}
@ -631,6 +618,15 @@ class WatchPage {
});
});
this.hls.on(Hls.Events.LEVEL_LOADED, (event, data) => {
if (this.hls !== hls || this.playbackGeneration !== generation) return;
// A partial playlist is not the movie's total. Only learn an unknown duration at ENDLIST.
const duration = Number(data?.details?.totalduration);
if (this.currentSessionId && !this.sourceDuration && data?.details?.live === false && Number.isFinite(duration) && duration > 0) {
this.setSourceDuration((this.playbackTimeOffset || 0) + duration);
}
});
this.hls.on(Hls.Events.ERROR, (event, data) => {
if (this.hls !== hls || this.playbackGeneration !== generation) return;
const mediaError = data.type === Hls.ErrorTypes.MEDIA_ERROR;
@ -737,6 +733,7 @@ class WatchPage {
if (!this.currentSessionId) { this.video.currentTime = target; return; }
const generation = this.playbackGeneration;
this.pendingSeekProgress = target;
this.captionTracks?.captureRemoteSelection();
this.hls?.destroy();
this.hls = null;
this.video.pause();