// Jellyfin Web's supported window plugin API supplies the native playback manager. window.DecoderChannelPlayback = async () => class DecoderChannelPlayback { constructor({ playbackManager, toast, appRouter, loading, events }) { this.id = 'decoder-channel-playback'; this.name = 'Decoder channel playback'; this.type = 'integration'; let generation = 0, starting = null, cancelButton = null, watcher = null; const clearControls = () => { clearInterval(watcher); watcher = null; cancelButton?.remove(); cancelButton = null; }; events.on(playbackManager, 'playbackstop', () => { if (!starting) clearControls(); }); document.addEventListener('fullscreenchange', () => { if (cancelButton) (document.fullscreenElement || document.body).append(cancelButton); }); const leave = async () => { ++generation; starting = null; clearControls(); try { await playbackManager.stop(); } finally { loading.hide(); await appRouter.show('livetv?tab=2'); } }; const play = (id, serverId) => { if (starting) return starting; const ticket = ++generation; let request; request = (async () => { const api = window.ApiClient; if (!api?.getCurrentUserId() || (serverId && api.serverId() !== serverId)) return false; const item = await api.getItem(api.getCurrentUserId(), id); if (ticket !== generation) return false; if (item.Type !== 'TvChannel') throw Error('Expected a TV channel'); clearControls(); cancelButton = document.createElement('button'); cancelButton.id = 'decoderPlaybackExit'; cancelButton.textContent = 'Zatrzymaj i wróć do kanałów'; cancelButton.style.cssText = 'position:fixed;right:16px;top:72px;z-index:2147483647;background:#252525;color:white;border:1px solid #aaa;border-radius:6px;padding:12px;font:16px system-ui;cursor:pointer'; cancelButton.onclick = () => void leave(); document.body.append(cancelButton); let opened = false; const deadline = Date.now() + 25000; watcher = setInterval(() => { if (ticket !== generation) return; const video = document.querySelector('video'); const player = playbackManager.getCurrentPlayer(); if (player?.isLocalPlayer === false) { clearControls(); return; } if (!opened && video && player) { opened = true; void appRouter.showVideoOsd(); } if (video?.readyState >= 2 && video.currentTime > 0) return; if (Date.now() >= deadline) { void leave(); toast('Kanał nie przesyła obrazu. Powrót do listy kanałów.'); } }, 250); try { await playbackManager.play({ items: [item], fullscreen: true }); } catch (error) { if (ticket !== generation) return false; await leave(); throw error; } return true; })().finally(() => { if (starting === request) starting = null; }); starting = request; return request; }; window.addEventListener('keydown', event => { if (event.key === 'Escape' && cancelButton) { event.preventDefault(); event.stopImmediatePropagation(); void leave(); } }, true); let pending; const handleRoute = () => { clearTimeout(pending); const route = location.hash; const query = new URLSearchParams(route.split('?')[1] || ''); if (route.split('?')[0].toLowerCase() === '#/livetv' && !query.has('tab')) { query.set('tab', '2'); location.replace('#/livetv?' + query); return; } const id = query.get('decoderPlay'); if (!id || !/^[a-f\d]{32}$|^[a-f\d-]{36}$/i.test(id)) return; const deadline = Date.now() + 30000; const attempt = async () => { if (location.hash !== route) return; if (!window.ApiClient?.getCurrentUserId()) { if (Date.now() < deadline) pending = setTimeout(attempt, 250); return; } query.delete('decoderPlay'); history.replaceState(null, '', location.pathname + location.search + route.split('?')[0] + (query.size ? '?' + query : '')); try { await play(id, query.get('serverId')); } catch (error) { console.error('Decoder channel playback failed', error); toast('Nie udało się uruchomić kanału.'); } }; void attempt(); }; window.addEventListener('hashchange', handleRoute); handleRoute(); // Channel image/name clicks in native channel cards should also play immediately. document.addEventListener('click', event => { if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; const link = event.target.closest('a.cardImageContainer, a.itemAction.textActionButton'); const card = link?.closest('[data-type="TvChannel"]'); if (!card) return; const id = card.dataset.id; if (!id) return; event.preventDefault(); event.stopImmediatePropagation(); void play(id, card.dataset.serverid).catch(error => { console.error('Decoder channel playback failed', error); toast('Nie udało się uruchomić kanału.'); }); }, true); } };