Cancel superseded Enigma2 startup and select programs throughout playback
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user