(function () { class LanPlayPlayer { constructor(video, wrapper, onError, onLoading, onNext) { this.video = video; this.wrapper = wrapper; this.onError = onError; this.onLoading = onLoading; this.onNext = onNext; this.dashPlayer = null; this.controlBar = null; this.sessionId = null; this.mediaPath = null; this.dashPreferencesApplied = false; this.currentEntry = null; this.nextAvailable = false; this.restarting = false; this.operation = 0; this.freezeFrame = null; this.playingHandler = null; this.subtitleUrls = []; } async play(entry, startSeconds = 0, preserveFrame = false) { const framePreserved = preserveFrame && this.captureFrame(); this.onLoading(true, framePreserved); this.stop({ preserveFrame: framePreserved }); const operation = ++this.operation; this.currentEntry = entry; this.mediaPath = entry.path; this.dashPreferencesApplied = false; const session = await window.LanPlayApi.startDash(entry.path, startSeconds); if (operation !== this.operation) { window.LanPlayApi.stopDash(session.session_id).catch(() => {}); return; } this.sessionId = session.session_id; this.dashPlayer = dashjs.MediaPlayer().create(); this.dashPlayer.updateSettings({ streaming: { buffer: { bufferTimeDefault: 8, bufferTimeAtTopQuality: 8, bufferTimeAtTopQualityLongForm: 8 } } }); this.dashPlayer.initialize(this.video, null, true); await this.addExternalSubtitles(entry.path, session.start_seconds, operation); if (operation !== this.operation) return; this.controlBar = new DashControlBarModule.ControlBar(this.dashPlayer, this.video); this.controlBar.init(this.wrapper); this.controlBar.setVodDuration(session.duration_seconds, session.start_seconds); if (typeof this.controlBar.onSeekRequested === 'function') { this.controlBar.onSeekRequested((time) => this.seek(time)); } if (typeof this.controlBar.onNextRequested === 'function') { this.controlBar.onNextRequested(() => this.onNext()); } if (typeof this.controlBar.setNextAvailable === 'function') { this.controlBar.setNextAvailable(this.nextAvailable); } this.controlBar.onTrackSelected((type, track) => this.rememberTrack(type, track)); this.controlBar.disable(); const controlBar = this.controlBar; this.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (event) => { if (operation !== this.operation) return; const message = event && event.error && event.error.message; this.onError(message || 'DASH playback error'); }); this.dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { if (operation !== this.operation || this.controlBar !== controlBar) return; this.applyDashPreferences(); controlBar.enable(); controlBar.refreshTracks(); }); this.playingHandler = () => { if (operation !== this.operation) return; this.playingHandler = null; this.clearFreezeFrame(); this.onLoading(false, false); }; this.video.addEventListener('playing', this.playingHandler, { once: true }); this.dashPlayer.attachSource(session.manifest_url); if (!framePreserved) this.onLoading(false, false); } async seek(time) { if (this.restarting || !this.currentEntry) return; this.restarting = true; const entry = this.currentEntry; try { await this.play(entry, time, true); } catch (error) { this.onError(error.message); this.clearFreezeFrame(); this.onLoading(false, false); } finally { this.restarting = false; } } setNextAvailable(available) { this.nextAvailable = Boolean(available); if (this.controlBar && typeof this.controlBar.setNextAvailable === 'function') { this.controlBar.setNextAvailable(this.nextAvailable); } } applyDashPreferences() { if (this.dashPreferencesApplied || !this.mediaPath) return; ['audio', 'video'].forEach((type) => { const tracks = this.dashPlayer.getTracksFor(type) || []; const preferred = window.LanPlayPreferences.find(this.mediaPath, type, tracks); if (preferred) this.dashPlayer.setCurrentTrack(preferred); }); const textTracks = this.dashPlayer.getTracksFor('text') || []; const preferredText = window.LanPlayPreferences.find(this.mediaPath, 'subtitle', textTracks); if (preferredText === null) { this.dashPlayer.setTextTrack(-1); } else if (preferredText) { const index = textTracks.indexOf(preferredText); if (index >= 0) this.dashPlayer.setTextTrack(index); } this.dashPreferencesApplied = true; } rememberTrack(type, track) { if (!this.mediaPath) return; if (type === 'nativeText') { const tracks = Array.from(this.video.textTracks || []); window.LanPlayPreferences.remember(this.mediaPath, 'subtitle', track, tracks.indexOf(track)); return; } const preferenceType = type === 'text' ? 'subtitle' : type; const tracks = this.dashPlayer.getTracksFor(type) || []; window.LanPlayPreferences.remember(this.mediaPath, preferenceType, track, tracks.indexOf(track)); } async addExternalSubtitles(path, startSeconds = 0, operation = this.operation) { let subtitles = []; try { subtitles = await window.LanPlayApi.subtitles(path); } catch (_) { return; } if (operation !== this.operation) return; const loaded = await Promise.all(subtitles.filter((item) => item.supported).map(async (subtitle, ordinal) => { const query = new URLSearchParams({ path, start: String(startSeconds) }); const url = subtitle.source === 'external' ? `/api/subtitles/external/${encodeURIComponent(subtitle.id)}?${query}` : `/api/subtitles/${subtitle.index}?${query}`; try { const response = await fetch(url); if (!response.ok) return null; const contents = await response.text(); const objectUrl = URL.createObjectURL(new Blob([contents], { type: 'text/vtt' })); return { subtitle, ordinal, objectUrl }; } catch (_) { return null; } })); if (operation !== this.operation) { loaded.filter(Boolean).forEach((item) => URL.revokeObjectURL(item.objectUrl)); return; } loaded.filter(Boolean).forEach(({ subtitle, ordinal, objectUrl }) => { const label = subtitle.title || subtitle.language || `Subtitle ${ordinal + 1}`; const idPart = subtitle.source === 'external' ? subtitle.id : subtitle.index; this.subtitleUrls.push(objectUrl); this.dashPlayer.addExternalSubtitle(new dashjs.ExternalSubtitle({ id: `lan-play-${subtitle.source}-${idPart}`, url: objectUrl, language: label, mimeType: 'text/vtt', bandwidth: 256, periodId: '0' })); }); } captureFrame() { if (this.video.readyState < 2 || !this.video.videoWidth || !this.video.videoHeight) return false; try { this.clearFreezeFrame(); const canvas = document.createElement('canvas'); const width = Math.max(1, this.wrapper.clientWidth); const height = Math.max(1, this.wrapper.clientHeight); canvas.width = Math.min(width, 1920); canvas.height = Math.min(height, 1080); canvas.className = 'player-freeze-frame'; const context = canvas.getContext('2d'); context.fillStyle = '#000'; context.fillRect(0, 0, canvas.width, canvas.height); const scale = Math.min(canvas.width / this.video.videoWidth, canvas.height / this.video.videoHeight); const drawWidth = this.video.videoWidth * scale; const drawHeight = this.video.videoHeight * scale; context.drawImage( this.video, (canvas.width - drawWidth) / 2, (canvas.height - drawHeight) / 2, drawWidth, drawHeight ); this.wrapper.appendChild(canvas); this.freezeFrame = canvas; return true; } catch (_) { this.clearFreezeFrame(); return false; } } clearFreezeFrame() { if (this.freezeFrame) this.freezeFrame.remove(); this.freezeFrame = null; } stop(options = {}) { this.operation += 1; if (this.playingHandler) { this.video.removeEventListener('playing', this.playingHandler); this.playingHandler = null; } if (!options.preserveFrame) this.clearFreezeFrame(); this.revokeSubtitleUrls(); if (this.controlBar) { this.controlBar.destroy(); this.controlBar = null; } if (this.dashPlayer) { this.dashPlayer.destroy(); this.dashPlayer = null; } this.video.pause(); this.video.removeAttribute('src'); while (this.video.firstChild) this.video.removeChild(this.video.firstChild); this.video.load(); this.mediaPath = null; this.dashPreferencesApplied = false; this.currentEntry = null; if (this.sessionId) { const sessionId = this.sessionId; this.sessionId = null; window.LanPlayApi.stopDash(sessionId).catch(() => {}); } } dispose() { this.operation += 1; if (this.playingHandler) this.video.removeEventListener('playing', this.playingHandler); this.playingHandler = null; this.clearFreezeFrame(); this.revokeSubtitleUrls(); if (this.controlBar) this.controlBar.destroy(); if (this.dashPlayer) this.dashPlayer.destroy(); if (this.sessionId) { fetch(`/api/dash/${this.sessionId}`, { method: 'DELETE', keepalive: true }).catch(() => {}); } this.controlBar = null; this.dashPlayer = null; this.sessionId = null; this.currentEntry = null; } revokeSubtitleUrls() { this.subtitleUrls.forEach((url) => URL.revokeObjectURL(url)); this.subtitleUrls = []; } } window.LanPlayPlayer = LanPlayPlayer; })();