Cancel superseded Enigma2 startup and select programs throughout playback

This commit is contained in:
Technomancer702
2026-10-13 17:56:26 -07:00
parent f2effe0832
commit 53b65b6a0d
15 changed files with 309 additions and 51 deletions

View File

@ -846,7 +846,7 @@ class ChannelList {
url: stream.stream_url, // M3U has direct URLs
program: stream.program || null,
zapBeforeStreaming: source?.zapBeforeStreaming || false,
zapDelay: source?.zapDelay || 1500,
zapDelay: source?.zapDelay ?? 1500,
groupId: `m3u_${sourceId}_${stream.category_id}`,
groupTitle: categories.find(c => String(c.category_id) === String(stream.category_id))?.category_name || 'Uncategorized',
sourceId,

View File

@ -266,7 +266,7 @@ class SourceManager {
<div class="form-group">
<label for="source-zap-delay">Zap delay (ms)</label>
<input type="number" id="source-zap-delay" class="form-input"
value="${source.zapDelay || 1500}" min="0" step="100">
value="${source.zapDelay ?? 1500}" min="0" max="10000" step="100">
</div>
` : '';
@ -282,7 +282,7 @@ class SourceManager {
const username = document.getElementById('source-username')?.value.trim() || null;
const password = document.getElementById('source-password')?.value.trim() || null;
const zapBeforeStreaming = document.getElementById('source-zap-before-streaming')?.checked || false;
const zapDelay = parseInt(document.getElementById('source-zap-delay')?.value) || 1500;
const zapDelay = Number(document.getElementById('source-zap-delay')?.value ?? 1500);
if (!name || !url) {
alert('Name and URL are required');
@ -347,7 +347,7 @@ class SourceManager {
}
if (type === 'm3u') {
data.zapBeforeStreaming = document.getElementById('source-zap-before-streaming')?.checked || false;
data.zapDelay = parseInt(document.getElementById('source-zap-delay')?.value) || 1500;
data.zapDelay = Number(document.getElementById('source-zap-delay')?.value ?? 1500);
}
await API.sources.update(id, data);
document.getElementById('modal').classList.remove('active');

View File

@ -646,13 +646,6 @@ class VideoPlayer {
* Play a channel
*/
async play(channel, streamUrl) {
if(this.currentChannel != channel && channel?.zapBeforeStreaming){
await this.zapBeforeStreaming(channel, streamUrl);
}
this.currentChannel = channel;
try {
// Stop any WatchPage playback (movies/series) before starting Live TV
window.app?.pages?.watch?.stop?.();
@ -660,6 +653,15 @@ class VideoPlayer {
// Stop current playback
this.stop();
const playbackGeneration = this.playbackGeneration;
if (channel?.zapBeforeStreaming) {
const controller = this.zapController = new AbortController();
await this.zapBeforeStreaming(channel, streamUrl, controller.signal);
if (this.playbackGeneration !== playbackGeneration) return;
this.zapController = null;
}
if (this.playbackGeneration !== playbackGeneration) return;
this.currentChannel = channel;
const programQuery = channel?.program == null ? '' : '&program=' + encodeURIComponent(channel.program);
let probeInfo = null;
this.updateTranscodeStatus('hidden');
@ -677,7 +679,7 @@ class VideoPlayer {
if (this.settings.autoTranscode) {
console.log('[Player] Auto Transcode enabled. Probing stream...');
try {
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}`);
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}${programQuery}`);
const info = await probeRes.json();
if (this.playbackGeneration !== playbackGeneration) return;
probeInfo = info;
@ -723,7 +725,7 @@ class VideoPlayer {
// Raw .ts container - use remux
console.log('[Player] Auto: Using remux (.ts container)');
this.updateTranscodeStatus('remuxing', 'Remux (Auto)');
const remuxUrl = this.getRemuxUrl(streamUrl, info.audioCodecs || [info.audio]);
const remuxUrl = this.getRemuxUrl(streamUrl, info.audioCodecs || [info.audio], channel?.program);
this.currentUrl = remuxUrl;
this.getCaptionTracks().setProbe(probeInfo, streamUrl);
this.getCaptionTracks().useNative();
@ -769,7 +771,7 @@ class VideoPlayer {
// Probe to get video codec for HEVC tag handling
let videoCodec = 'unknown';
try {
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}`);
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}${programQuery}`);
const info = await probeRes.json();
if (this.playbackGeneration !== playbackGeneration) return;
videoCodec = info.video;
@ -815,20 +817,20 @@ class VideoPlayer {
// Force Remux: Route through FFmpeg for container conversion
// Applies to: 1) .ts streams when detected, or 2) ALL non-HLS streams when enabled
if (this.settings.forceRemux && (isRawTs || isExtensionless)) {
if ((this.settings.forceRemux || channel?.program != null) && (isRawTs || isExtensionless)) {
console.log('[Player] Force Remux enabled. Routing through FFmpeg remux...');
console.log('[Player] Stream type:', isRawTs ? 'Raw TS' : 'Extension-less (assumed TS)');
this.updateTranscodeStatus('remuxing', 'Remux (Force)');
this.updateTranscodeStatus('remuxing', this.settings.forceRemux ? 'Remux (Force)' : 'Remux (Program)');
if (!probeInfo) {
try {
const response = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}`);
const response = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}${programQuery}`);
probeInfo = await response.json();
} catch (error) {
console.warn('[Player] Remux probe failed:', error.message);
}
}
if (this.playbackGeneration !== playbackGeneration) return;
const remuxUrl = this.getRemuxUrl(streamUrl, probeInfo?.audioCodecs || (probeInfo?.audio ? [probeInfo.audio] : []));
const remuxUrl = this.getRemuxUrl(streamUrl, probeInfo?.audioCodecs || (probeInfo?.audio ? [probeInfo.audio] : []), channel?.program);
this.currentUrl = remuxUrl;
this.getCaptionTracks().setProbe(probeInfo, streamUrl);
this.getCaptionTracks().useNative();
@ -902,6 +904,7 @@ class VideoPlayer {
window.dispatchEvent(new CustomEvent('channelChanged', { detail: channel }));
} catch (err) {
if (err.name === 'AbortError') return;
console.error('Error playing channel:', err);
this.showError('Failed to play channel');
}
@ -1109,8 +1112,9 @@ class VideoPlayer {
* Get remuxed URL for a stream (container conversion only, no re-encoding)
* Used for raw .ts streams that browsers can't play directly
*/
getRemuxUrl(url, audioCodecs = []) {
return `/api/remux?url=${encodeURIComponent(url)}&audioCodecs=${encodeURIComponent(audioCodecs.join(','))}`;
getRemuxUrl(url, audioCodecs = [], program = null) {
const selection = program == null ? '' : '&program=' + encodeURIComponent(program);
return `/api/remux?url=${encodeURIComponent(url)}&audioCodecs=${encodeURIComponent(audioCodecs.join(','))}${selection}`;
}
/**
@ -1129,6 +1133,8 @@ class VideoPlayer {
* Stop playback
*/
stop() {
this.zapController?.abort();
this.zapController = null;
this.playbackGeneration = (this.playbackGeneration || 0) + 1;
this.audioRecovery?.dispose();
this.audioRecovery = null;
@ -1311,11 +1317,13 @@ class VideoPlayer {
}
}
async zapBeforeStreaming(channel, streamUrl) {
async zapBeforeStreaming(channel, streamUrl, signal) {
if (!channel?.zapBeforeStreaming) return;
if (!channel?.streamId || !streamUrl) return;
const delay = parseInt(channel.zapDelay) || 1500;
const requestedDelay = Number(channel.zapDelay ?? 1500);
const delay = Number.isFinite(requestedDelay) ? Math.max(0, Math.min(10000, requestedDelay)) : 1500;
signal?.throwIfAborted();
try {
console.log('[ZAP] before streaming:', {
@ -1326,6 +1334,7 @@ class VideoPlayer {
// The server resolves the receiver from the configured source
const res = await fetch('/api/openwebif/zap', {
signal,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -1338,8 +1347,14 @@ class VideoPlayer {
console.warn('[ZAP] backend returned:', res.status);
}
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise((resolve, reject) => {
const abort = () => { clearTimeout(timer); reject(Object.assign(new Error('Playback was superseded'), { name: 'AbortError' })); };
const timer = setTimeout(() => { signal?.removeEventListener('abort', abort); resolve(); }, delay);
signal?.addEventListener('abort', abort, { once: true });
if (signal?.aborted) abort();
});
} catch (err) {
if (signal?.aborted || err.name === 'AbortError') throw err;
console.warn('[ZAP] failed, continuing stream:', err);
}
}

View File

@ -420,6 +420,8 @@ class WatchPage {
this.stop();
this.setSourceDuration(VodDuration.firstValid(VodDuration.fromContent(this.content || {}), knownDuration));
const playbackGeneration = this.playbackGeneration;
const program = this.content?.program ?? this.content?.data?.program ?? null;
const programQuery = program == null ? '' : '&program=' + encodeURIComponent(program);
let probeInfo = null;
// Show loading spinner
@ -444,7 +446,7 @@ class WatchPage {
console.log('[WatchPage] Auto Transcode enabled. Probing stream...');
try {
const ua = settings.userAgentPreset === 'custom' ? settings.userAgentCustom : settings.userAgentPreset;
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}${programQuery}`);
const info = await probeRes.json();
if (this.playbackGeneration !== playbackGeneration) return;
probeInfo = info;
@ -467,7 +469,7 @@ class WatchPage {
this.updateTranscodeStatus(statusMode, statusText);
if (this.playbackGeneration !== playbackGeneration) return;
const playlistUrl = await this.startTranscodeSession(url, {
program: this.content?.program || this.content?.data?.program || null,
program,
videoMode,
seekOffset: this.resumeTime, // Ensure seekOffset is passed
videoCodec: info.video,
@ -484,7 +486,7 @@ class WatchPage {
// TODO: Move remux to session logic if seeking is needed for TS files
console.log('[WatchPage] Auto: Using remux (.ts container)');
this.updateTranscodeStatus('remuxing', 'Remux (Auto)');
const finalUrl = this.getRemuxUrl(url, info.audioCodecs || [info.audio]);
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);
@ -509,7 +511,7 @@ class WatchPage {
this.updateTranscodeStatus(statusMode, statusText);
if (this.playbackGeneration !== playbackGeneration) return;
const playlistUrl = await this.startTranscodeSession(url, {
program: this.content?.program || this.content?.data?.program || null,
program,
videoMode: 'encode',
seekOffset: this.resumeTime
});
@ -527,7 +529,7 @@ class WatchPage {
let videoCodec = 'unknown';
try {
const ua = settings.userAgentPreset === 'custom' ? settings.userAgentCustom : settings.userAgentPreset;
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}${programQuery}`);
const info = await probeRes.json();
if (this.playbackGeneration !== playbackGeneration) return;
videoCodec = info.video;
@ -535,7 +537,7 @@ class WatchPage {
if (this.playbackGeneration !== playbackGeneration) return;
const playlistUrl = await this.startTranscodeSession(url, {
program: this.currentChannel?.program || null,
program,
videoMode: 'copy',
videoCodec,
seekOffset: this.resumeTime
@ -547,20 +549,20 @@ class WatchPage {
}
// Priority 2: Force Remux for raw TS streams
if (settings.forceRemux && isRawTs) {
if ((settings.forceRemux && isRawTs) || (program !== null && !looksLikeHls)) {
console.log('[WatchPage] Force Remux enabled');
this.updateTranscodeStatus('remuxing', 'Remux (Force)');
this.updateTranscodeStatus('remuxing', settings.forceRemux ? 'Remux (Force)' : 'Remux (Program)');
if (!probeInfo) {
try {
const ua = settings.userAgentPreset === 'custom' ? settings.userAgentCustom : settings.userAgentPreset;
const response = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
const response = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}${programQuery}`);
probeInfo = await response.json();
} catch (error) {
console.warn('[WatchPage] Remux probe failed:', error.message);
}
}
if (this.playbackGeneration !== playbackGeneration) return;
const finalUrl = this.getRemuxUrl(url, probeInfo?.audioCodecs || (probeInfo?.audio ? [probeInfo.audio] : []));
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);
@ -650,8 +652,9 @@ class WatchPage {
if (this.volumeSlider) this.volumeSlider.value = savedVolume;
}
getRemuxUrl(url, audioCodecs = []) {
return `/api/remux?url=${encodeURIComponent(url)}&audioCodecs=${encodeURIComponent(audioCodecs.join(','))}`;
getRemuxUrl(url, audioCodecs = [], program = null) {
const selection = program == null ? '' : '&program=' + encodeURIComponent(program);
return `/api/remux?url=${encodeURIComponent(url)}&audioCodecs=${encodeURIComponent(audioCodecs.join(','))}${selection}`;
}
stop() {