PageSourceSearch

https://ny1.com/etc/designs/news/clientlibs/js/mini-live-player.min.ACSHASHa7883b3ceb8b20113dd3789d920ce01d.js

js ny1.com collected 2026-09-24 06:42:28 UTC 131,485 bytes, 3,193 lines download raw bytes

1/**
2 * Mini Player Configuration
3 * Centralized player configuration settings for mini-live-player
4 */
5const MiniPlayerConfig = (function() {
6    'use strict';
7
8    function getRetryConfig() {
9        return {
10            timeout: 10000,
11            stallTimeout: 5000,
12            connectionTimeout: 1000,
13            maxAttempts: 3,
14            baseDelay: 1000,
15            backoffFactor: 2,
16            fuzzFactor: 0.5
17        };
18    }
19
20    function getPlayerInitConfig() {
21        const retryObject = getRetryConfig();
22        
23        return {
24            spectrum: {
25                noLicenseAuthHeader: false,
26                ignoreLoadErrorCodes: '7000',
27                skipPrerollAdBreaks: false,
28                useUpdateStartTime: true,
29                muted: true,
30                adBreakStartSeekOffsetSec: 0,
31                serviceWorkerLocation: '/serviceworker.js',
32                pixelAspectRatioAdjustment: "SCALEX",
33                localManifest: undefined,
34                netErrHandling: {
35                    ignoreErrors: false,
36                    seekOnBuffering: false,
37                    maxPosDiffSec: 8,
38                    seekDeltaSec: 6,
39                    seekDelaySec: 0
40                },
41                gapJumping: {
42                    enabled: false,
43                    pollingIntervalMs: 200,
44                    preJumpSec: 1,
45                    endOfAdBreakPreJumpSec: 3,
46                    overJumpSec: 0
47                },
48                followBaseUrlChanges: false
49            },
50            streaming: {
51                bufferingGoal: 18,
52                bufferBehind: 10,
53                rebufferingGoal: 2,
54                jumpLargeGaps: true,
55                reportGapJumps: true,
56                gapOverJump: 0,
57                gapDetectionThreshold: 0.5,
58                stallEnabled: true,
59                stallSkip: 2,
60                stallThreshold: 2,
61                appendWindowStartAdjustment: 0.1,
62                appendWindowEndAdjustment: 0.01,
63                pauseToGetBackInSeekRange: false,
64                retryParameters: retryObject
65            },
66            manifest: {
67                retryParameters: retryObject,
68                defaultPresentationDelay: 16
69            },
70            drm: {
71                retryParameters: retryObject
72            },
73            abr: {
74                bandwidthUpgradeTarget: 0.85,
75                bandwidthDowngradeTarget: 0.95,
76                defaultBandwidthEstimate: 206000,
77                switchInterval: 4
78            }
79        };
80    }
81
82    function getPlayerUIConfig() {
83        return {
84            addSeekBar: false,
85            addBigPlayButton: false,
86            singleClickForPlayAndPause: false,
87            controlPanelElements: ['play_stop', 'spacer', 'picture_in_picture', 'fullscreen', 'overflow_menu'],
88            overflowMenuButtons: ['captions', 'quality', 'airplay']
89        };
90    }
91
92    function getSpanishTranslations() {
93        return {
94            'CAPTIONS': 'Subtítulos',
95            'RESOLUTION': 'Resolución',
96            'PICTURE_IN_PICTURE': 'Imagen en imagen',
97            'PLAYBACK_RATE': 'Velocidad de reproducción',
98            'OFF': 'Apagar',
99            'ON': 'Encender',
100            'AUTO': 'Automático'
101        };
102    }
103
104    return {
105        getRetryConfig,
106        getPlayerInitConfig,
107        getPlayerUIConfig,
108        getSpanishTranslations
109    };
110})();
111
112/**
113 * Mini Player Utility Functions
114 * Browser detection, stream type detection, and other helpers for mini-live-player
115 */
116const MiniPlayerUtils = (function() {
117    'use strict';
118
119    function isAuthorMode() {
120        try {
121            // AEM Classic
122            if (window.CQ && CQ.WCM && typeof CQ.WCM.isEditMode === 'function') {
123                return !!CQ.WCM.isEditMode();
124            }
125            // AEM Touch UI
126            if (window.Granite && Granite.author && Granite.author.layer) {
127                return Granite.author.layer === 'edit';
128            }
129            return false;
130        } catch (e) {
131            return false;
132        }
133    }
134
135    function getStreamType() {
136        const result = bowser.getParser(navigator.userAgent).getResult();
137        return result.browser.name === "Safari" ? "hls" : "dash";
138    }
139
140    function getDrmType() {
141        const result = bowser.getParser(navigator.userAgent).getResult();
142        return result.browser.name === "Safari" ? "fairplay" : "cenc";
143    }
144
145    function getBrowserInfo() {
146        return bowser.getParser(navigator.userAgent).getResult();
147    }
148
149    function isSafari() {
150        return getBrowserInfo().browser.name === "Safari";
151    }
152
153    function isTargetedBot() {
154        const ua = navigator.userAgent;
155        return ua.includes('Chrome/66.0.3359') || 
156               ua.includes('Chrome/60.0.3112') || 
157               ua.includes('Version/11.1.1 Safari') || 
158               ua.includes('Firefox/59.0') || 
159               ua.includes('Edge/17.17134');
160    }
161
162    function isTokenExpired(player) {
163        if (player.drmAuthHeader) {
164            const timeUntilExpiredMs = player.drmAuthHeader.expirationTimeMs - Date.now();
165            return timeUntilExpiredMs < 0;
166        }
167        return false;
168    }
169
170    return {
171        isAuthorMode,
172        getStreamType,
173        getDrmType,
174        getBrowserInfo,
175        isSafari,
176        isTargetedBot,
177        isTokenExpired
178    };
179})();
180
181/**
182 * Caption Handler
183 * Manages caption track normalization and UI cleanup
184 */
185const CaptionHandler = (function() {
186    'use strict';
187
188    let isCaptionDOMchangeRequired = false;
189    let FALLBACK_LABEL = 'English'; // Default, can be overridden
190
191    // Helper functions
192    function stripEmptyParens(s) {
193        return (s || '').replace(/\(\s*\)/g, '').replace(/\s{2,}/g, ' ').trim();
194    }
195
196    function normalizeUndefinedText(s) {
197        // Map any "Unrecognized"/"und"/"undetermined" to fallback label
198        // Also handle Spanish tokens: "sin especificar", "no especificado", "Apagar", "Automático", "Listo"
199        const spanishTokens = /sin\s+especificar|no\s+especificado|Apagar|Automático|Listo/i;
200        const englishTokens = /(unrecogn[i|s]ed(?:\s+language)?|\bund(?:etermined)?\b)/i;
201        
202        return (englishTokens.test(s || '') || spanishTokens.test(s || '')) ? FALLBACK_LABEL : s;
203    }
204
205    function cleanNodeText(el) {
206        if (!el) return;
207        const t = (el.textContent || '').trim();
208        const newT = normalizeUndefinedText(stripEmptyParens(t));
209        if (newT !== t) el.textContent = newT;
210    }
211
212    function cleanSettingsButtonText(scope) {
213        const root = scope || document;
214        const spans = root.querySelectorAll(
215            '#html5-player shaka-settings-menu > button > span, shaka-settings-menu > button > span'
216        );
217        for (let i = 0; i < spans.length; i++) {
218            cleanNodeText(spans[i]);
219        }
220    }
221
222    // Patch Shaka's buildTextTrackLabel to remove empty parentheses
223    function patchShakaTextTrackLabel() {
224        const patch = (utils) => {
225            if (!utils || !utils.buildTextTrackLabel) return;
226            if (utils.buildTextTrackLabel.__patched) return;
227
228            const original = utils.buildTextTrackLabel;
229
230            utils.buildTextTrackLabel = function(track, localization) {
231                let label = original.call(this, track, localization);
232                return (label || '').replace(/\(\s*\)/g, '').trim();
233            };
234
235            utils.buildTextTrackLabel.__patched = true;
236        };
237
238        // Patch raw Shaka
239        if (window.shaka?.ui?.Utils) {
240            patch(window.shaka.ui.Utils);
241        }
242        // Patch Spectrum-wrapped Shaka
243        if (window.SpectrumPlayer?.shakaUi?.ui?.Utils) {
244            patch(window.SpectrumPlayer.shakaUi.ui.Utils);
245        }
246    }
247
248    // Normalize caption tracks
249    function normalizeTextTracks(playerWrapper, fallbackLabel) {
250        try {
251            // Update fallback label if provided
252            if (fallbackLabel) {
253                FALLBACK_LABEL = fallbackLabel;
254            }
255
256            const tracks = playerWrapper.getTextTracks() || [];
257
258            tracks.forEach(track => {
259                // Normalize unknown languages to use fallback label
260                if (!track.language || track.language === 'und' || track.language === 'zxx') {
261                    track.language = 'en';
262                    track.label = FALLBACK_LABEL;
263                }
264                // Remove empty roles to prevent dangling "()"
265                if (!track.roles || track.roles.length === 0) {
266                    delete track.roles;
267                }
268            });
269
270            // Force CC UI refresh
271            playerWrapper.updateCCState();
272
273            // DOM cleanup
274            setTimeout(() => {
275                cleanCaptionDOM();
276            }, 0);
277        } catch (e) {
278            console.warn('Caption normalization failed:', e);
279        }
280    }
281
282    function cleanCaptionDOM() {
283        try {
284            const selectors = [
285                '.shaka-current-selection-span',
286                '.shaka-chosen-item',
287                '.shaka-text-language'
288            ];
289            selectors.forEach(sel => {
290                document.querySelectorAll(sel).forEach(el => {
291                    if (!el || !el.textContent) return;
292                    let txt = el.textContent.trim();
293                    txt = stripEmptyParens(txt);
294                    txt = normalizeUndefinedText(txt);
295                    el.textContent = txt;
296                });
297            });
298
299            cleanSettingsButtonText();
300        } catch (e) {
301            console.warn('Caption DOM cleanup failed', e);
302        }
303    }
304
305    // Legacy caption manipulation (for backward compatibility)
306    function captionUnrecognised() {
307        const list = document.querySelector('#html5-player .shaka-text-languages') || 
308                     document.querySelector('.shaka-text-languages');
309        if (!list || !list.childNodes) return;
310
311        let counter = 0;
312        list.childNodes.forEach((node) => {
313            const hasClass = node && typeof node.getAttribute === 'function' && node.getAttribute('class');
314            if (!hasClass && node && node.firstChild && node.firstChild.innerHTML) {
315                const html = node.firstChild.innerHTML.trim();
316                const needsReplacement = /Unrecog/i.test(html) || 
317                                        /\bund(?:etermined)?\b/i.test(html) ||
318                                        /sin\s+especificar/i.test(html) ||
319                                        /no\s+especificado/i.test(html) ||
320                                        /Apagar/i.test(html) ||
321                                        /Automático/i.test(html) ||
322                                        /Listo/i.test(html);
323                const hasEmptyParens = /\(\s*\)/.test(html);
324
325                if (needsReplacement || hasEmptyParens) {
326                    if (counter < 1) {
327                        isCaptionDOMchangeRequired = true;
328                        setTimeout(() => {
329                            let newHtml = html;
330                            newHtml = newHtml.replace(/\(\s*\)/g, '').trim();
331                            newHtml = newHtml.replace(/Unrecogn[i|s]ed(?:\s+language)?/i, FALLBACK_LABEL);
332                            newHtml = newHtml.replace(/\bund(?:etermined)?\b/i, FALLBACK_LABEL);
333                            newHtml = newHtml.replace(/sin\s+especificar/i, FALLBACK_LABEL);
334                            newHtml = newHtml.replace(/no\s+especificado/i, FALLBACK_LABEL);
335                            newHtml = newHtml.replace(/Apagar/i, FALLBACK_LABEL);
336                            newHtml = newHtml.replace(/Automático/i, FALLBACK_LABEL);
337                            newHtml = newHtml.replace(/Listo/i, FALLBACK_LABEL);
338                            node.firstChild.innerHTML = newHtml || FALLBACK_LABEL;
339                        }, 5);
340                    } else {
341                        try { node.remove(); } catch (e) {}
342                    }
343                    counter++;
344                }
345            }
346        });
347    }
348
349    function changeDOMforCaption() {
350        captionUnrecognised();
351        const header = document.querySelector('#html5-player .shaka-current-selection-span') || 
352                      document.querySelector('.shaka-current-selection-span');
353        if (header && header.innerHTML) {
354            setTimeout(() => {
355                let t = header.innerHTML.trim();
356                t = t.replace(/\(\s*\)/g, '').trim();
357                t = t.replace(/Unrecogn[i|s]ed(?:\s+language)?/i, FALLBACK_LABEL);
358                t = t.replace(/\bund(?:etermined)?\b/i, FALLBACK_LABEL);
359                t = t.replace(/sin\s+especificar/i, FALLBACK_LABEL);
360                t = t.replace(/no\s+especificado/i, FALLBACK_LABEL);
361                t = t.replace(/Apagar/i, FALLBACK_LABEL);
362                t = t.replace(/Automático/i, FALLBACK_LABEL);
363                t = t.replace(/Listo/i, FALLBACK_LABEL);
364                header.innerHTML = t || FALLBACK_LABEL;
365            }, 5);
366        }
367    }
368
369    function captionsManipulations() {
370        captionUnrecognised();
371        if (isCaptionDOMchangeRequired) {
372            const list = document.querySelector('#html5-player .shaka-text-languages') || 
373                        document.querySelector('.shaka-text-languages');
374            if (list && !list.__captionFixClickBound) {
375                list.addEventListener('click', () => {
376                    changeDOMforCaption();
377                });
378                list.__captionFixClickBound = true;
379            }
380        }
381    }
382
383    // Observe caption UI for changes
384    function observeCaptionUI() {
385        function safeClean(root) {
386            if (!root) return;
387            const list = root.querySelector('#html5-player .shaka-text-languages') || 
388                        root.querySelector('.shaka-text-languages');
389            if (list) {
390                list.querySelectorAll('.shaka-text-language, .shaka-current-selection-span, .shaka-chosen-item')
391                    .forEach(el => cleanNodeText(el));
392            }
393            cleanSettingsButtonText(root);
394        }
395
396        function attachWhenReady() {
397            const root = document;
398            const captionList = root.querySelector('#html5-player .shaka-text-languages') || 
399                               root.querySelector('.shaka-text-languages');
400            const settingsMenu = root.querySelector('#html5-player shaka-settings-menu') || 
401                                root.querySelector('shaka-settings-menu');
402
403            if (!captionList && !settingsMenu) {
404                setTimeout(attachWhenReady, 100);
405                return;
406            }
407
408            // Initial pass
409            safeClean(root);
410
411            // Observe captions list
412            if (captionList) {
413                const observer = new MutationObserver(() => safeClean(root));
414                observer.observe(captionList, { childList: true, subtree: true, characterData: true });
415            }
416            // Observe settings menu
417            if (settingsMenu) {
418                const settingsObserver = new MutationObserver(() => cleanSettingsButtonText(root));
419                settingsObserver.observe(settingsMenu, { childList: true, subtree: true, characterData: true });
420            }
421
422            // Poll briefly for microtasks
423            let tries = 0, maxTries = 30;
424            const poll = setInterval(() => {
425                safeClean(root);
426                tries++;
427                if (tries >= maxTries) clearInterval(poll);
428            }, 200);
429        }
430
431        attachWhenReady();
432    }
433
434    // Setup localization overrides
435    function setupLocalization(controls, fallbackLabel) {
436        const localization = controls.getLocalization();
437        
438        // Update fallback label if provided
439        if (fallbackLabel) {
440            FALLBACK_LABEL = fallbackLabel;
441        }
442        
443        // Override Shaka fallback for unknown languages
444        localization.insert('en', new Map([
445            ['UNRECOGNIZED_LANGUAGE', 'English']
446        ]));
447        localization.insert('es', new Map([
448            ['UNRECOGNIZED_LANGUAGE', 'Inglés']
449        ]));
450
451        // Spanish localization for spectrumnoticias.com
452        if (window.location.host.includes('spectrumnoticias.com')) {
453            localization.changeLocale(["es"]);
454            const translations = {
455                'CAPTIONS': 'Subtítulos',
456                'RESOLUTION': 'Resolución',
457                'PICTURE_IN_PICTURE': 'Imagen en imagen',
458                'PLAYBACK_RATE': 'Velocidad de reproducción',
459                'OFF': 'Apagar',
460                'ON': 'Encender',
461                'AUTO': 'Automático'
462            };
463            const translation_map = new Map(Object.entries(translations));
464            localization.insert('es', translation_map);
465        }
466    }
467
468    return {
469        patchShakaTextTrackLabel,
470        normalizeTextTracks,
471        cleanCaptionDOM,
472        captionsManipulations,
473        observeCaptionUI,
474        setupLocalization
475    };
476})();
477
478/**
479 * DRM Strategy
480 * Handles DRM configuration for different IPVS versions
481 */
482const DRMStrategy = (function() {
483    'use strict';
484
485    function buildV5Config(response, config) {
486        const info = response.data;
487        const headers = response.headers;
488
489        const LAUrls = {
490            widevine: config.widevineUrl,
491            fairplayQueryString: '',
492            fairplay: '',
493            fairplayReplaceUrl: config.fairplayLA,
494            fairplayCertificateURL: config.fairplayCertUrl
495        };
496
497        const drmAuthHeaders = {
498            "Authorization": "Bearer " + News.App.Pinxt.TokenManager.getCurrentAccessToken(),
499            "AST-Authorization": headers["x-set-ast"]
500        };
501
502        return { info, headers, LAUrls, drmAuthHeaders };
503    }
504
505    function buildLegacyConfig(response, config) {
506        const info = response;
507        let contId = info.drmContentId;
508        
509        if (contId.indexOf('_') > 0) {
510            contId = contId.split('_')[0];
511        }
512
513        const LAUrls = {
514            widevine: config.widevineUrl + "?CrmId=twc&AccountId=twc" + "&ContentId=" + contId,
515            fairplayQueryString: '',
516            fairplay: '',
517            fairplayReplaceUrl: config.fairplayLA,
518            fairplayCertificateURL: config.fairplayCertUrl
519        };
520
521        const drmAuthHeaders = {
522            "Authorization": "Bearer " + info.jwtToken
523        };
524
525        return { info, headers: null, LAUrls, drmAuthHeaders };
526    }
527
528    function buildDRMConfig(response, config) {
529        if (config.ipvsV5Switch === true) {
530            return buildV5Config(response, config);
531        } else {
532            return buildLegacyConfig(response, config);
533        }
534    }
535
536    function isTokenExpired(player) {
537        if (player.drmAuthHeader) {
538            const timeUntilExpiredMs = player.drmAuthHeader.expirationTimeMs - Date.now();
539            return timeUntilExpiredMs < 0;
540        }
541        return false;
542    }
543
544    return {
545        buildDRMConfig,
546        isTokenExpired
547    };
548})();
549
550/**
551 * Analytics Helper
552 * Simplifies analytics event triggering
553 */
554const AnalyticsHelper = (function() {
555    'use strict';
556
557    function withMediaAnalytics(MAInit, callback) {
558        if (MAInit) {
559            MAInit.done(function(res, status, xhr) {
560                const location = xhr.getResponseHeader('Location');
561                callback(location, xhr);
562            });
563        }
564    }
565
566    function trackPlaybackStart(bitrate) {
567        const QA = News.App.QuantumAnalytics;
568        
569        if (!QA.isPlayBackStarted && QA.isPlaybackAttemptRestart) {
570            QA.playbackStartRetrySuccess("SpectrumLocal_playbackStart_retrySuccess", bitrate);
571        } else if (QA.isPlayBackStarted && QA.isPlaybackAttemptRestart && QA.isPlaybackFailedBeforeRetry) {
572            QA.playbackStartAfterRetry('SpectrumLocal_playbackStartAfterRetry', bitrate);
573        }
574    }
575
576    function trackBitrateChange(newBitRate, currentBitrate) {
577        let eventSent = false;
578        
579        if (newBitRate > currentBitrate) {
580            eventSent = News.App.QuantumAnalytics.playbackBitRateShift(
581                'SpectrumLocal_Playback_Bit_Rate_Upshift_linear', 
582                newBitRate
583            );
584        } else if (newBitRate < currentBitrate) {
585            eventSent = News.App.QuantumAnalytics.playbackBitRateShift(
586                'SpectrumLocal_Playback_Bit_Rate_Downshift_linear', 
587                newBitRate
588            );
589        }
590        
591        return eventSent;
592    }
593
594    function trackUnloadPage(MAInit, isMiniLivePlayer = false) {
595        const QA = News.App.QuantumAnalytics;
596        const MA = News.App.MediaAnalytics;
597
598        // Mini-live-player specific checks
599        if (isMiniLivePlayer) {
600            if (News.VideoPlayerController.isVideoCurrentlyActive('mini-live-player') && 
601                QA.isPlayBackSelected && !QA.isPlayBackStarted && !QA.isPlaybackFailedBeforeRetry) {
602                QA.playbackExitBeforeStartByUser('SpectrumLocal_playbackExitBeforeStart_linear_byUser');
603            } else if (News.VideoPlayerController.isVideoCurrentlyActive('mini-live-player') && 
604                       QA.isPlayBackSelected && !QA.isPlaybackStartRetrySuccess && QA.isPlaybackFailedBeforeRetry) {
605                QA.playbackExitBeforeStartRetryByUser('SpectrumLocal_playbackExitBeforeStart_retry_byUser');
606            } else if (News.VideoPlayerController.isVideoCurrentlyActive('mini-live-player') && 
607                       QA.isPlayBackStarted && !QA.isPlaybackFailedBeforeRetry) {
608                QA.playbackStop('SpectrumLocal_Playback_Stop_Linear');
609            }
610        } else {
611            // Watch-live logic
612            if (QA.isPlayBackSelected && !QA.isPlayBackStarted && !QA.isPlaybackFailedBeforeRetry) {
613                QA.playbackExitBeforeStartByUser('SpectrumLocal_playbackExitBeforeStart_linear_byUser');
614            } else if (QA.isPlayBackSelected && !QA.isPlaybackStartRetrySuccess && QA.isPlaybackFailedBeforeRetry) {
615                QA.playbackExitBeforeStartRetryByUser('SpectrumLocal_playbackExitBeforeStart_retry_byUser');
616            } else if (QA.isPlayBackStarted && !QA.isPlaybackFailedBeforeRetry) {
617                QA.playbackStop('SpectrumLocal_Playback_Stop_Linear');
618            }
619        }
620
621        // Use sendBeacon API to avoid browser canceling async requests
622        withMediaAnalytics(MAInit, (location) => {
623            if (MA.isfullscreenStarted) {
624                MA.triggerEvent(location, MA.fullscreenStop(), true);
625            }
626            if (MA.isMuted) {
627                MA.triggerEvent(location, MA.playbackMuteEnd(), true);
628            } else if (isMiniLivePlayer) {
629                // Mini-player specific: end miniPlayerUnmuted state
630                MA.triggerEvent(location, MA.miniPlayerUnmutedEnd(), true);
631            }
632            if (isMiniLivePlayer && MA.isMiniPlayerStarted) {
633                MA.triggerEvent(location, MA.miniPlayerStop(), true);
634            }
635        });
636    }
637
638    function trackAdTracking(trackingUrl) {
639        const startTime = new Date().getTime();
640        const workingURL = new URL(trackingUrl);
641        
642        if (workingURL.protocol === 'http:') {
643            workingURL.protocol = 'https:';
644        }
645        trackingUrl = workingURL.toString();
646
647        $.ajax({
648            url: trackingUrl,
649            type: 'GET',
650            dataType: 'text'
651        }).done(function(data, textStatus, jqXHR) {
652            News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
653                trackingUrl, jqXHR.status, "All_Generic_API_Call_Success", 
654                startTime, "GET", "", "", jqXHR
655            );
656        }).fail(function(jqXHR) {
657            const responseText = News.App.QuantumAnalytics.extractAjaxErrorMessage(jqXHR);
658            News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
659                trackingUrl, jqXHR.status, "All_Generic_API_Call_Failure", 
660                startTime, "GET", "", responseText, jqXHR
661            );
662        });
663    }
664
665    function normalizeAudioCodecLabel(codec) {
666        if (codec === "mp4a.40.2") return "AAC";
667        if (codec === "ec-3") return "EAC3";
668        return codec;
669    }
670
671    function trackAudioCodecChange(audioCodec, availableAudioTracks) {
672        var normalizedCodec = normalizeAudioCodecLabel(audioCodec);
673        // availableAudioTracks contains language track labels (e.g. ["en", "es"]) — pass through as-is
674        var tracks = Array.isArray(availableAudioTracks) ? availableAudioTracks : [];
675
676        News.App.QuantumAnalytics.playbackTrackChange_audioCodec(
677            'SpectrumLocal_playbackTrackChange_audioCodec',
678            normalizedCodec,
679            tracks
680        );
681    }
682
683    /**
684     * Detect and track audio codec after playbackStarted event has fired.
685     * Uses Shaka player to get active codec, language tracks from stored event data.
686     * @param {Object} context - The MiniLivePlayerInstance context
687     */
688    function trackAudioCodecAfterPlaybackStarted(context) {
689        var audioCodec = null;
690
691        // Resolve the active audio codec from Shaka variant tracks
692        if (context.playerWrapper && context.playerWrapper.shakaPlayer) {
693            try {
694                var tracks = context.playerWrapper.shakaPlayer.getVariantTracks();
695                console.log('[AudioCodec] mini-player Shaka variant tracks count:', tracks ? tracks.length : 0);
696                if (tracks && tracks.length > 0) {
697                    var activeTrack = tracks.find(function(t) { return t.active; });
698                    if (activeTrack && activeTrack.audioCodec) {
699                        audioCodec = activeTrack.audioCodec;
700                    }
701                }
702            } catch (e) {
703                console.warn('[AudioCodec] mini-player Shaka getVariantTracks() failed:', e);
704            }
705        }
706
707        // Safari fallback: use codec detected from manifest fetch + canPlayType
708        if (!audioCodec && context.detectedAudioCodec) {
709            console.log('[AudioCodec] mini-player using Safari manifest fallback:', context.detectedAudioCodec);
710            audioCodec = context.detectedAudioCodec;
711        }
712
713        // availableAudioTracks: use language labels stored from the PLAYBACK_STARTED event
714        var availableAudioTracks = (context.availableAudioTracksFromEvent && context.availableAudioTracksFromEvent.length > 0)
715            ? context.availableAudioTracksFromEvent
716            : [];
717        console.log('[AudioCodec] mini-player final audioCodec:', audioCodec, '| availableAudioTracks:', availableAudioTracks);
718
719        if (audioCodec) {
720            trackAudioCodecChange(audioCodec, availableAudioTracks);
721        }
722    }
723
724    return {
725        withMediaAnalytics,
726        trackPlaybackStart,
727        trackBitrateChange,
728        trackUnloadPage,
729        trackAdTracking,
730        trackAudioCodecChange,
731        trackAudioCodecAfterPlaybackStarted
732    };
733})();
734
735/**
736 * Mini Player UI & Volume Helpers
737 * Handles UI manipulation and volume management specific to mini-live-player
738 */
739const MiniPlayerUI = (function($) {
740    'use strict';
741
742    // ==================== UI METHODS ====================
743
744    /**
745     * Toggle play/stop button state
746     */
747    function togglePlayStopButton() {
748        const playStopButton = document.querySelector('.shaka-playstop-button');
749        if (!playStopButton) return;
750
751        if (playStopButton.classList.contains('playing')) {
752            playStopButton.classList.remove('playing');
753            playStopButton.classList.add('stopped');
754            playStopButton.textContent = 'play_arrow';
755            $('.live-player-container .thumbnail').show();
756        } else {
757            playStopButton.classList.remove('stopped');
758            playStopButton.classList.add('playing');
759            playStopButton.textContent = 'stop';
760            $('.live-player-container .thumbnail').hide();
761        }
762    }
763
764    /**
765     * Show/hide error container
766     * @param {boolean} val - true to show, false to hide
767     */
768    function showErrorContainer(val) {
769        $('#html5-player .error-container').toggle(val);
770        // Hide all the controls
771        $('#html5-player .shaka-controls-button-panel').toggle(!val);
772        $('#html5-player .shaka-play-button').toggle(!val);
773    }
774
775    /**
776     * Show concurrency error message
777     */
778    function showConcurrencyErrorMessage() {
779        const pathName = window.location.pathname;
780        const rootPath = $('.live-stream-promo--container.mini-player').data('root-path');
781        const rootPathMinusContentNews = pathName.startsWith("/content/news") ? 
782            rootPath : rootPath.replace("/content/news", "");
783        const concurrencyErrorPath = $('.site-header .header-ele').data('noticias-region') ? 
784            "/video-en-vivo/limit-exceeded" : "/live-video/limit-exceeded";
785        const concurrencyErrorPage = rootPathMinusContentNews + concurrencyErrorPath + 
786            (pathName.endsWith('.html') ? '.html' : '');
787        
788        const htmlString = $('.site-header .header-ele').data('noticias-region') ?
789            `<p>Se alcanzó el límite de sesiónes de transmisión. <a target='_blank' href='${concurrencyErrorPage}'>&#9432;</a></p>` :
790            `<p>Streaming session limit reached. <a target='_blank' href='${concurrencyErrorPage}'>&#9432;</a></p>`;
791        
792        $('.error-container').html(htmlString);
793        showErrorContainer(true);
794    }
795
796    /**
797     * Show temp pass expired message
798     */
799    function showTempPassExpiredMessage() {
800        const pathName = window.location.pathname;
801        const rootPath = $('.live-stream-promo--container.mini-player').data('root-path');
802        const rootPathMinusContentNews = pathName.startsWith("/content/news") ? 
803            rootPath : rootPath.replace("/content/news", "");
804        const loginPath = $('.site-header .header-ele').data('noticias-region') ? 
805            "/video-en-vivo/login" : "/live-video/login";
806        const loginPage = rootPathMinusContentNews + loginPath + 
807            (pathName.endsWith('.html') ? '.html' : '');
808        
809        const htmlString = $('.site-header .header-ele').data('noticias-region') ?
810            `<p>Tu pase temporal ha expirado. <a href='${loginPage}'>Inicia sesión</a> para continuar viendo.</p>` :
811            `<p>Your temporary pass has expired. <a href='${loginPage}'>Sign in</a> to continue watching.</p>`;
812        
813        $('.error-container').html(htmlString);
814        showErrorContainer(true);
815    }
816
817    /**
818     * Setup custom mute/unmute button handlers
819     * @param {Object} playerWrapper - Spectrum player instance
820     */
821    function setupMuteButtons(playerWrapper) {
822        $('.mini-player-controls .mute, .mini-player-controls .unmute').click((e) => {
823            const muted = playerWrapper.isMuted();
824            if (muted) {
825                $('.mini-player-controls .mute').hide();
826                $('.mini-player-controls .unmute').show();
827            } else {
828                $('.mini-player-controls .mute').show();
829                $('.mini-player-controls .unmute').hide();
830            }
831            playerWrapper.setMuted(!muted);
832        });
833    }
834
835    /**
836     * Setup pop-out button to launch full player
837     */
838    function setupPopOutButton() {
839        $('.mini-player-controls .pop-out').click(function(e) {
840            const $container = $('.live-stream-promo--container.mini-player');
841            const fallbackChannel = $('#html5-player').data('fallbackchannel');
842            const isFallbackActive = $('.mini-player-fallback').is(':visible');
843            
844            // Use fallback channel path if fallback is currently playing, otherwise default
845            let cfPath;
846            if (isFallbackActive && fallbackChannel) {
847                cfPath = fallbackChannel.split('/').pop();
848            } else {
849                const cfPathFull = $container.data('cfpath');
850                cfPath = cfPathFull.split('/').pop();
851            }
852            
853            const pathName = window.location.pathname;
854            const rootPath = $container.data('root-path');
855            const noticiasRegion = $('.site-header .header-ele').data('noticias-region');
856            const livePathText = window.location.host.includes('spectrumnoticias') || noticiasRegion ? 
857                'video-en-vivo' : 'live-video';
858            const livePath = pathName.endsWith(".html") ? 
859                "/" + livePathText + "/watch_spectrum_news_live.html" : 
860                "/" + livePathText + "/watch_spectrum_news_live";
861            const rootPathModified = pathName.startsWith("/content/news") ? 
862                rootPath : rootPath.replace("/content/news", "");
863            const href = rootPathModified + livePath + '?channelPath=' + cfPath;
864            window.open(href, "_self");
865        });
866    }
867
868    /**
869     * Move mini-player controls into Shaka controls container
870     */
871    function moveControlsIntoPlayer() {
872        $('#html5-player .mini-player-controls').prependTo('#html5-player .shaka-controls-container');
873    }
874
875    /**
876     * Setup Safari fullscreen exit handler
877     */
878    function setupSafariFullscreenHandler() {
879        $('#html5-player .shaka-fullscreen-button').click((e) => {
880            if ($(e.target).text() === 'fullscreen_exit') {
881                document.webkitExitFullscreen && document.webkitExitFullscreen();
882            }
883        });
884    }
885
886    /**
887     * Initialize all UI components
888     * @param {Object} playerWrapper - Spectrum player instance
889     */
890    function initializeUI(playerWrapper) {
891        moveControlsIntoPlayer();
892        setupSafariFullscreenHandler();
893        setupMuteButtons(playerWrapper);
894        setupPopOutButton();
895    }
896
897    // ==================== VOLUME METHODS ====================
898
899    /**
900     * Handle unmuting - restore user's previous volume preference
901     * @param {Object} player - Spectrum player instance
902     * @returns {boolean} - Always returns false
903     */
904    function onSpectrumPlayerUnmute(player) {
905        const playerVol = player.getVolume() * 100;
906        
907        if (localStorage.getItem("vod-volume")) {
908            const vol = parseInt(localStorage.getItem("vod-volume")) / 100;
909            if (vol > 0) {
910                player.setVolume(vol);
911            }
912        } else {
913            localStorage.setItem("vod-volume", playerVol.toString());
914        }
915        
916        return false;
917    }
918
919    /**
920     * Handle volume change - save user's volume preference
921     * @param {number} targetVolume - New volume level (0-100)
922     * @param {boolean} mutedByUser - Whether change was due to muting
923     */
924    function onSpectrumPlayerVolumeChange(targetVolume, mutedByUser) {
925        if (!mutedByUser) {
926            setTimeout(function() {
927                const vol = parseInt(targetVolume);
928                if (vol > 0) {
929                    localStorage.setItem("vod-volume", vol.toString());
930                }
931            }, 200);
932        }
933    }
934
935    /**
936     * Get stored volume preference
937     * @returns {number|null} - Stored volume (0-100) or null if not set
938     */
939    function getStoredVolume() {
940        const stored = localStorage.getItem("vod-volume");
941        return stored ? parseInt(stored) : null;
942    }
943
944    /**
945     * Get mute state from storage
946     * @returns {boolean} - Whether player should be muted
947     */
948    function getStoredMuteState() {
949        return localStorage.getItem("muted") === "true";
950    }
951
952    /**
953     * Set mute state in storage
954     * @param {boolean} isMuted - Whether player is muted
955     */
956    function setStoredMuteState(isMuted) {
957        localStorage.setItem("muted", isMuted ? "true" : "false");
958    }
959
960    return {
961        // UI methods
962        togglePlayStopButton,
963        showErrorContainer,
964        showConcurrencyErrorMessage,
965        showTempPassExpiredMessage,
966        setupMuteButtons,
967        setupPopOutButton,
968        moveControlsIntoPlayer,
969        setupSafariFullscreenHandler,
970        initializeUI,
971        // Volume methods
972        onSpectrumPlayerUnmute,
973        onSpectrumPlayerVolumeChange,
974        getStoredVolume,
975        getStoredMuteState,
976        setStoredMuteState
977    };
978})(jQuery);
979
980/**
981 * Mini Player Integration
982 * Handles PiP, VideoController, and external integrations for mini-live-player
983 */
984const MiniPlayerIntegration = (function($) {
985    'use strict';
986
987    // ==================== PIP METHODS ====================
988
989    /**
990     * Update link targets based on PiP state
991     * When PiP is active, all links open in new tab to prevent navigation
992     * @param {boolean} isPipOn - Whether PiP is currently active
993     */
994    function updateLinkTargetBasedOnPip(isPipOn) {
995        $('a').each(function() {
996            const $link = $(this);
997            
998            // Store original target on first run
999            if (!$link.attr('data-original-target')) {
1000                const originalTarget = $link.attr('target') || '_self';
1001                $link.attr('data-original-target', originalTarget);
1002            }
1003            
1004            // Update target based on PiP state
1005            if (isPipOn) {
1006                $link.attr('target', '_blank');
1007            } else {
1008                const originalTarget = $link.attr('data-original-target');
1009                $link.attr('target', originalTarget);
1010            }
1011        });
1012    }
1013
1014    /**
1015     * Monitor Picture-in-Picture events
1016     * @param {HTMLVideoElement} videoElement - The video element to monitor
1017     * @param {Function} onEnterPip - Callback when entering PiP
1018     * @param {Function} onLeavePip - Callback when leaving PiP
1019     */
1020    function monitorPiP(videoElement, onEnterPip, onLeavePip) {
1021        if (!document.pictureInPictureEnabled) {
1022            console.warn("Picture-in-Picture is not supported in this browser.");
1023            return;
1024        }
1025
1026        // Store reference in window for global access
1027        window._miniLivePlayer = window._miniLivePlayer || {};
1028        window._miniLivePlayer.videoElement = videoElement;
1029
1030        // Listen for when PiP mode is entered
1031        videoElement.addEventListener('enterpictureinpicture', () => {
1032            window._miniLivePlayer.isPipOn = true;
1033            updateLinkTargetBasedOnPip(true);
1034            
1035            if (typeof onEnterPip === 'function') {
1036                onEnterPip();
1037            }
1038        });
1039
1040        // Listen for when PiP mode is exited
1041        videoElement.addEventListener('leavepictureinpicture', () => {
1042            window._miniLivePlayer.isPipOn = false;
1043            updateLinkTargetBasedOnPip(false);
1044            
1045            if (typeof onLeavePip === 'function') {
1046                onLeavePip();
1047            }
1048        });
1049    }
1050
1051    /**
1052     * Check if PiP is currently active
1053     * @returns {boolean} - Whether PiP is active
1054     */
1055    function isPiPActive() {
1056        return window._miniLivePlayer && window._miniLivePlayer.isPipOn === true;
1057    }
1058
1059    // ==================== VIDEO CONTROLLER METHODS ====================
1060
1061    /**
1062     * Register mini-live-player with VideoPlayerController
1063     * @param {Object} playerInstance - The mini-live-player instance
1064     */
1065    function registerWithController(playerInstance) {
1066        if (News.VideoPlayerController && News.VideoPlayerController.registerNewVideo) {
1067            // Register with VideoPlayerController if it's ready
1068            News.VideoPlayerController.registerNewVideo(playerInstance);
1069        } else {
1070            // Otherwise, add to the window.initializedVideos queue to register when ready
1071            window.initializedVideos = window.initializedVideos || [];
1072            window.initializedVideos.push(playerInstance);
1073        }
1074    }
1075
1076    /**
1077     * Register that mini-live-player is loading
1078     * Called before player initialization
1079     */
1080    function registerLoading() {
1081        if (News.VideoPlayerController) {
1082            News.VideoPlayerController.registerMiniLivePlayerLoading();
1083        }
1084    }
1085
1086    /**
1087     * Reset currently active player in VideoController
1088     */
1089    function resetCurrentlyActive() {
1090        if (News.VideoPlayerController) {
1091            News.VideoPlayerController.resetCurrentlyActive();
1092        }
1093    }
1094
1095    /**
1096     * Request new playback from VideoController
1097     * @param {string} playerType - Type of player requesting playback
1098     */
1099    function requestNewPlayback(playerType) {
1100        if (News.VideoPlayerController) {
1101            News.VideoPlayerController.requestNewPlayback(playerType);
1102        }
1103    }
1104
1105    return {
1106        // PiP methods
1107        monitorPiP,
1108        updateLinkTargetBasedOnPip,
1109        isPiPActive,
1110        // VideoController methods
1111        registerWithController,
1112        registerLoading,
1113        resetCurrentlyActive,
1114        requestNewPlayback
1115    };
1116})(jQuery);
1117
1118/**
1119 * Mini Player Event Handlers
1120 * Manages all player event listeners specific to mini-live-player
1121 */
1122const MiniPlayerEvents = (function () {
1123    'use strict';
1124
1125    /**
1126     * Setup playback events (PLAYBACK_STARTED, PLAYBACK_PLAYING)
1127     * @param {Object} playerWrapper - Spectrum player instance
1128     * @param {Object} context - Player instance context (this)
1129     */
1130    function setupPlaybackEvents(playerWrapper, context) {
1131        const {PLAYBACK_STARTED, PLAYBACK_PLAYING} = playerWrapper.events;
1132
1133        playerWrapper.on(PLAYBACK_STARTED, (data) => {
1134            context.onPlaybackStarted(data);
1135        });
1136
1137        playerWrapper.on(PLAYBACK_PLAYING, (data) => {
1138            setTimeout(() => {
1139                CaptionHandler.captionsManipulations();
1140            }, 10);
1141
1142            News.App.QuantumAnalytics.playbackUnPause('SpectrumLocal_Playback_Unpause_linear');
1143
1144            if (News.App.MediaAnalytics.IsPaused) {
1145                context.MAInit.done((res, status, xhr) => {
1146                    const location = xhr.getResponseHeader('Location');
1147                    if (News.App.QuantumAnalytics.isplayerUnmutedBefore) {
1148                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.unPause(playerWrapper));
1149                    }
1150
1151                    // Start or restart muted or unmuted playback states
1152                    if ((News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore) && !Ne
1152ws.App.MediaAnalytics.isfullscreenStarted) {
1153                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playbackMuteStart());
1154                    } else if ((!News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore) && !News.App.MediaAnalytics.isfullscreenStarted) {
1155                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerUnmutedStart());
1156                    }
1157
1158                    News.App.MediaAnalytics.clearBufferPingInterval();
1159                    News.App.MediaAnalytics.clearPausePingInterval();
1160                    if (News.App.QuantumAnalytics.isplayerUnmutedBefore) {
1161                        News.App.MediaAnalytics.streamPingInterval(playerWrapper, xhr.getResponseHeader('Location'));
1162                    }
1163                });
1164            }
1165        });
1166    }
1167
1168    /**
1169     * Setup buffering events (STALL_STARTED, STALL_ENDED)
1170     * @param {Object} playerWrapper - Spectrum player instance
1171     * @param {Object} context - Player instance context (this)
1172     */
1173    function setupBufferingEvents(playerWrapper, context) {
1174        const {STALL_STARTED, STALL_ENDED} = playerWrapper.events;
1175
1176        playerWrapper.on(STALL_STARTED, () => {
1177            setTimeout(() => {
1178                CaptionHandler.captionsManipulations();
1179            }, 200);
1180
1181            News.App.QuantumAnalytics.bufferStart('SpectrumLocal_Buffering_Start_linear');
1182
1183            context.MAInit.done((res, status, xhr) => {
1184                const location = xhr.getResponseHeader('Location');
1185                News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.bufferStart(playerWrapper));
1186                News.App.MediaAnalytics.clearStreamPlayheadPingInterval();
1187                if (News.App.QuantumAnalytics.isplayerUnmutedBefore) {
1188                    News.App.MediaAnalytics.bufferingPingInterval(playerWrapper, location);
1189                }
1190            });
1191        });
1192
1193        playerWrapper.on(STALL_ENDED, () => {
1194            News.App.QuantumAnalytics.bufferStop('SpectrumLocal_Buffering_Stop_linear');
1195
1196            context.MAInit.done((res, status, xhr) => {
1197                const location = xhr.getResponseHeader('Location');
1198                if (News.App.QuantumAnalytics.isplayerUnmutedBefore) {
1199                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playEvent(playerWrapper));
1200                }
1201                // stop buffer pings if active
1202                News.App.MediaAnalytics.clearBufferPingInterval();
1203                if (News.App.QuantumAnalytics.isplayerUnmutedBefore) {
1204                    News.App.MediaAnalytics.streamPingInterval(playerWrapper, location);
1205                }
1206            });
1207        });
1208    }
1209
1210    /**
1211     * Setup bitrate change events
1212     * @param {Object} playerWrapper - Spectrum player instance
1213     * @param {Object} context - Player instance context (this)
1214     */
1215    function setupBitrateEvents(playerWrapper, context) {
1216        const {VIDEO_BITRATE_CHANGED} = playerWrapper.events;
1217
1218        playerWrapper.on(VIDEO_BITRATE_CHANGED, (data) => {
1219            const newBitRate = data.newBitRate;
1220            const eventSent = AnalyticsHelper.trackBitrateChange(newBitRate, context.currentBitrate);
1221            if (eventSent) {
1222                context.currentBitrate = newBitRate;
1223            }
1224        });
1225    }
1226
1227    /**
1228     * Setup error events
1229     * @param {Object} playerWrapper - Spectrum player instance
1230     * @param {Object} context - Player instance context (this)
1231     */
1232    function setupErrorEvents(playerWrapper, context) {
1233        const {ERROR, PLAYER_ERROR} = playerWrapper.events;
1234
1235        playerWrapper.on(PLAYER_ERROR, (error) => {
1236            context.retry(playerWrapper, error.status, error.message, error);
1237        });
1238
1239        playerWrapper.on(ERROR, (error) => {
1240            let errMsg;
1241            // Retry only if the failed URL was different from the previous failed URL
1242            if (error.data && error.data[0] !== context.errURL) {
1243                context.errURL = error.data[0];
1244                if (error.data[1]) {
1245                    errMsg = error.data[1].message || "Failed to fetch";
1246                }
1247                context.retry(playerWrapper, error.code, errMsg, error);
1248            }
1249        });
1250    }
1251
1252    /**
1253     * Setup ad events (DAI)
1254     * @param {Object} playerWrapper - Spectrum player instance
1255     * @param {Object} context - Player instance context (this)
1256     */
1257    function setupAdEvents(playerWrapper, context) {
1258        const {AD_TRACK_REQUEST, AD_BREAK_STARTED, AD_BREAK_ENDED, AD_STARTED, AD_ENDED} = playerWrapper.events;
1259
1260        playerWrapper.on(AD_TRACK_REQUEST, (data) => {
1261            const trackingEvents = data.adInstance.trackingEvents;
1262            const matchingEvent = trackingEvents.find(event => event.eventType === data.type);
1263            const trackingUrls = matchingEvent.trackingUrls;
1264
1265            trackingUrls.forEach(function (trackingUrl) {
1266                AnalyticsHelper.trackAdTracking(trackingUrl);
1267            });
1268        });
1269
1270        playerWrapper.on(AD_BREAK_STARTED, (data) => {
1271            context.adNumber = 0;
1272            if (data && data.currentAdBreak) {
1273                News.App.QuantumAnalytics.adBreakStarted(context.adBreakNumber);
1274                context.adBreakNumber = context.adBreakNumber + 1;
1275            }
1276        });
1277
1278        playerWrapper.on(AD_BREAK_ENDED, (data) => {
1279            News.App.QuantumAnalytics.adBreakEnded();
1280        });
1281
1282        playerWrapper.on(AD_STARTED, (data) => {
1283            if (data && data.currentAd) {
1284                const currentAd = data.currentAd;
1285                context.adNumber++;
1286                News.App.QuantumAnalytics.adStarted(currentAd.id, context.adNumber, currentAd.title);
1287            }
1288        });
1289
1290        playerWrapper.on(AD_ENDED, (data) => {
1291            if (data) {
1292                News.App.QuantumAnalytics.adEnded(data.adDurationSec, data.adStoppedReason);
1293            }
1294        });
1295    }
1296
1297    /**
1298     * Setup volume events with mini-player specific logic
1299     * @param {Object} playerWrapper - Spectrum player instance
1300     * @param {Object} context - Player instance context (this)
1301     */
1302    function setupVolumeEvents(playerWrapper, context) {
1303        const {VOLUME_CHANGED} = playerWrapper.events;
1304
1305        playerWrapper.on(VOLUME_CHANGED, async (e) => {
1306            MiniPlayerUI.onSpectrumPlayerVolumeChange((e.volume) * 100, e.muted);
1307
1308            if (e.muted === true) {
1309                MiniPlayerUI.setStoredMuteState(true);
1310                News.App.MediaAnalytics.isMuted = true;
1311
1312                if (News.App.QuantumAnalytics.isplayerUnmutedBefore && !News.App.MediaAnalytics.IsPaused && !News.App.MediaAnalytics.isfullscreenStarted) {
1313                    context.MAInit.done((res, status, xhr) => {
1314                        const location = xhr.getResponseHeader('Location');
1315                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerUnmutedEnd());
1316                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playbackMuteStart());
1317                    });
1318                }
1319            } else {
1320                // Load user selected volume settings during last visit
1321                if (MiniPlayerUI.getStoredMuteState()) {
1322                    MiniPlayerUI.setStoredMuteState(false);
1323                    MiniPlayerUI.onSpectrumPlayerUnmute(playerWrapper);
1324                }
1325                if (News.App.QuantumAnalytics.isplayerUnmutedBefore && !News.App.MediaAnalytics.IsPaused && !News.App.MediaAnalytics.isfullscreenStarted) {
1326                    context.MAInit.done((res, status, xhr) => {
1327                        News.App.MediaAnalytics.triggerEvent(xhr.getResponseHeader('Location'), News.App.MediaAnalytics.playbackMuteEnd());
1328                    })
1329                }
1330
1331                if (!News.App.MediaAnalytics.IsPaused && !News.App.MediaAnalytics.isfullscreenStarted) {
1332                    context.MAInit.done((res, status, xhr) => {
1333                        News.App.MediaAnalytics.triggerEvent(xhr.getResponseHeader('Location'), News.App.MediaAnalytics.miniPlayerUnmutedStart());
1334                    })
1335                }
1336                if (!News.App.QuantumAnalytics.isplayerUnmutedBefore) {
1337
1338                    await News.App.QuantumAnalytics.playbackSelect("SpectrumLocal_playbackSelect_linear", "linear");
1339
1340                    if (playerWrapper.stream_url) {
1341                        News.App.QuantumAnalytics.uriAcquiredSuccess('SpectrumLocal_Stream_URI_Acquired_Successful_-_Linear', playerWrapper.stream_url);
1342                    } else {
1343                        News.App.QuantumAnalytics.uriAcquiredFailure('SpectrumLocal_Stream_URI_Acquired_Failure_-_Linear');
1344                    }
1345                     News.App.QuantumAnalytics.playbackStarted('SpectrumLocal_Playback_Start_Linear', context.currentBitrate);
1346
1347                    // Track audio codec after playbackStarted has fired
1348                    AnalyticsHelper.trackAudioCodecAfterPlaybackStarted(context);
1349
1350                    context.MAInit.done((res, status, xhr) => {
1351                        console.log('play event fired on unmute')
1352                        News.App.MediaAnalytics.triggerEvent(xhr.getResponseHeader('Location'), News.App.MediaAnalytics.playEvent(playerWrapper));
1353                        News.App.MediaAnalytics.streamPingInterval(playerWrapper, xhr.getResponseHeader('Location'));
1354
1355                    })
1356
1357                    News.App.QuantumAnalytics.isplayerUnmutedBefore = true;
1358                }
1359
1360
1361                News.App.MediaAnalytics.isMuted = false;
1362
1363                context.handleUnmutingForConcurrencyMonitoring();
1364            }
1365        });
1366    }
1367
1368    /**
1369     * Setup DRM authorization events
1370     * @param {Object} playerWrapper - Spectrum player instance
1371     * @param {Object} context - Player instance context (this)
1372     */
1373    function setupDRMEvents(playerWrapper, context) {
1374        const {DRM_AUTHORIZATION_EXPIRED} = playerWrapper.events;
1375
1376        playerWrapper.on(DRM_AUTHORIZATION_EXPIRED, (e) => {
1377            if (context.ipvsV5Switch === false) {
1378                const sessionId = Cookies.get("sessionID");
1379                const streamURL = Cookies.get("stream-url");
1380                const zip = Cookies.get("stream-zip");
1381                const resourceId = context.$html5Player.data('resourceid');
1382
1383                if (MiniPlayerUtils.isTokenExpired(playerWrapper)) {
1384                    News.concurrencyMonitoring.requestStream(
1385                        streamURL,
1386                        context.streamData,
1387                        zip,
1388                        context.enableDai,
1389                        resourceId,
1390                        sessionId
1391                    ).then(function (info) {
1392                        e.onNewAuthorizationHeader('Bearer ' + info.jwtToken);
1393                    });
1394                }
1395            }
1396        });
1397    }
1398
1399    /**
1400     * Setup pause toggle event (currently just logs)
1401     * @param {Object} playerWrapper - Spectrum player instance
1402     */
1403    function setupPauseEvents(playerWrapper) {
1404        const {PAUSE_TOGGLED} = playerWrapper.events;
1405
1406        playerWrapper.on(PAUSE_TOGGLED, (data) => {
1407            console.log('pause_toggled', data);
1408        });
1409    }
1410
1411    /**
1412     * Setup all event listeners for mini-live-player
1413     * @param {Object} playerWrapper - Spectrum player instance
1414     * @param {Object} context - Player instance context (this)
1415     */
1416    function setupAllEvents(playerWrapper, context) {
1417        if (context.mediaEventsSetup) {
1418            return;
1419        }
1420
1421        setupPlaybackEvents(playerWrapper, context);
1422        setupBufferingEvents(playerWrapper, context);
1423        setupBitrateEvents(playerWrapper, context);
1424        setupErrorEvents(playerWrapper, context);
1425        setupAdEvents(playerWrapper, context);
1426        setupVolumeEvents(playerWrapper, context);
1427        setupDRMEvents(playerWrapper, context);
1428        setupPauseEvents(playerWrapper);
1429
1430        context.mediaEventsSetup = true;
1431    }
1432
1433    return {
1434        setupAllEvents,
1435        setupPlaybackEvents,
1436        setupBufferingEvents,
1437        setupBitrateEvents,
1438        setupErrorEvents,
1439        setupAdEvents,
1440        setupVolumeEvents,
1441        setupDRMEvents,
1442        setupPauseEvents
1443    };
1444})();
1445
1446/**
1447 * HDCP Handler - Refactored
1448 * Manages HDCP (High-bandwidth Digital Content Protection) compliance detection and capping
1449 * 
1450 * Responsibilities:
1451 * - Detect HDCP compliance using browser APIs
1452 * - Determine appropriate resolution caps
1453 * - Apply restrictions to player
1454 * - Report capping status for analytics
1455 */
1456
1457const HDCPHandler = (function($) {
1458    'use strict';
1459
1460    // Constants
1461    const HDCP_CAP = {
1462        UNLIMITED: undefined,
1463        HD_720: 720,
1464        SD_576: 576
1465    };
1466
1467    const PLAYBACK_CAPPING_STATUS = {
1468        DETECTED_NONCOMPLIANT: false,
1469        DETECTED_COMPLIANT: true
1470    };
1471
1472    const MIN_HDCP_VERSION = '1.4';
1473    const CHROME_HDCP_VERSION = '>=73.0';
1474
1475    // Browser detection
1476    const browserParser = bowser.getParser(
1477        window?.navigator?.userAgent
1478    );
1479
1480    // Private Methods
1481
1482    function isChrome(version = ">1.0") {
1483        return browserParser.satisfies({
1484            chrome: version
1485        });
1486    }
1487
1488    function isHdcpEnabled() {
1489        return $('body').data('hdcpenabler') === true;
1490    }
1491
1492    /**
1493     * Detect HDCP compliance using browser API
1494     * Adapted from https://googlechrome.github.io/samples/hdcp-detection/
1495     * 
1496     * @returns {Promise<string>} Resolves with 'usable' if compliant
1497     */
1498    async function detectHdcpCompliance() {
1499        const config = [{
1500            videoCapabilities: [{
1501                contentType: 'video/mp4;codecs="avc1.4d401e"',
1502                robustness: 'SW_SECURE_DECODE' // Widevine L3
1503            }]
1504        }];
1505
1506        try {
1507            const mediaKeySystemAccess = await navigator.requestMediaKeySystemAccess(
1508                'com.widevine.alpha', 
1509                config
1510            );
1511            
1512            const mediaKeys = await mediaKeySystemAccess.createMediaKeys();
1513            
1514            if (!('getStatusForPolicy' in mediaKeys)) {
1515                throw new Error('HDCP Policy Check API is not available.');
1516            }
1517
1518            const status = await mediaKeys.getStatusForPolicy({
1519                minHdcpVersion: MIN_HDCP_VERSION
1520            });
1521
1522            if (status !== 'usable') {
1523                throw new Error(status);
1524            }
1525
1526            return status;
1527        } catch (error) {
1528            console.error("Detecting HDCP Compliance failed:", error);
1529            throw error;
1530        }
1531    }
1532
1533    /**
1534     * Determine maximum vertical resolution based on HDCP compliance and browser
1535     * 
1536     * @param {boolean} isHdcpCompliant - Whether HDCP is compliant
1537     * @param {boolean} isChrome73orHigher - Whether browser is Chrome 73+
1538     * @returns {number|undefined} Maximum allowed vertical resolution
1539     */
1540    function determineHdcpCapLimit(isHdcpCompliant, isChrome73orHigher) {
1541        if (isHdcpCompliant && isChrome73orHigher) {
1542            return HDCP_CAP.UNLIMITED;
1543        }
1544        if (!isHdcpCompliant && isChrome73orHigher) {
1545            return HDCP_CAP.SD_576;
1546        }
1547        return HDCP_CAP.HD_720;
1548    }
1549
1550    /**
1551     * Apply HDCP resolution cap to the player
1552     * 
1553     * @param {Object} player - Spectrum Player instance
1554     * @param {number|undefined} limit - Maximum allowed vertical resolution
1555     * @param {Error|null} error - Error if HDCP detection failed
1556     */
1557    function applyHdcpCap(player, limit, error = null) {
1558        // Report compliance status for analytics
1559        const complianceStatus = error 
1560            ? PLAYBACK_CAPPING_STATUS.DETECTED_NONCOMPLIANT 
1561            : PLAYBACK_CAPPING_STATUS.DETECTED_COMPLIANT;
1562        
1563        reportHdcpCappingStatus(limit, complianceStatus, { reason: error });
1564
1565        // Apply restriction to player
1566        const restriction = {
1567            verticalResolutionMax: limit
1568        };
1569        
1570        player.setSegmentRestriction(restriction);
1571    }
1572
1573    /**
1574     * Report HDCP capping status for analytics
1575     * 
1576     * @param {number|undefined} limit - Applied resolution limit
1577     * @param {boolean|string} status - Compliance status
1578     * @param {Object} extras - Additional data for reporting
1579     */
1580    function reportHdcpCappingStatus(limit, status, extras = {}) {
1581        console.log(`reportHdcpCappingStatus limit: ${limit} status: ${status}`);
1582        console.log(extras);
1583
1584        // TODO: Integrate with analytics system
1585        // Helix.quantum.setPlaybackCapping({ playbackCapping: status });
1586        
1587        // TODO: Send custom event
1588        // customEvent({
1589        //     customLevel: 'info',
1590        //     customName: 'HDCPCapping',
1591        //     customMessage: {
1592        //         status,
1593        //         limit: String(limit || 'unlimited'),
1594        //         ...extras
1595        //     }
1596        // });
1597    }
1598
1599    // Public Methods
1600
1601    /**
1602     * Main entry point - Enforce HDCP capping on player
1603     * 
1604     * @param {Object} player - Spectrum Player instance
1605     */
1606    function enforceHdcpCapping(player) {
1607        const enabled = isHdcpEnabled();
1608
1609        try {
1610            // Do nothing if HDCP Capping is disabled
1611            if (!enabled) {
1612                reportHdcpCappingStatus(HDCP_CAP.UNLIMITED, "disabled");
1613                return;
1614            }
1615
1616            // Detect HDCP compliance and determine cap
1617            const isChrome73orHigher = isChrome(CHROME_HDCP_VERSION);
1618            let limit = null;
1619            let errorMessage = null;
1620
1621            detectHdcpCompliance()
1622                .then(() => {
1623                    // HDCP compliant
1624                    limit = determineHdcpCapLimit(true, isChrome73orHigher);
1625                    applyHdcpCap(player, limit, null);
1626                })
1627                .catch((error) => {
1628                    // HDCP non-compliant or detection failed
1629                    errorMessage = error;
1630                    limit = determineHdcpCapLimit(false, isChrome73orHigher);
1631                    applyHdcpCap(player, limit, errorMessage);
1632                });
1633
1634        } catch (ex) {
1635            console.error('HDCP - Error enforcing capping:', ex);
1636        }
1637    }
1638
1639    /**
1640     * Get HDCP cap constants (for testing/debugging)
1641     */
1642    function getCapConstants() {
1643        return { ...HDCP_CAP };
1644    }
1645
1646    /**
1647     * Get compliance status constants (for testing/debugging)
1648     */
1649    function getStatusConstants() {
1650        return { ...PLAYBACK_CAPPING_STATUS };
1651    }
1652
1653    // Public API
1654    return {
1655        enforceHdcpCapping,
1656        detectHdcpCompliance,
1657        determineHdcpCapLimit,
1658        applyHdcpCap,
1659        reportHdcpCappingStatus,
1660        getCapConstants,
1661        getStatusConstants,
1662        
1663        // Expose constants for backward compatibility
1664        HDCP_CAP_576: HDCP_CAP.SD_576,
1665        HDCP_CAP_720: HDCP_CAP.HD_720,
1666        HDCP_CAP_UNLIMITED: HDCP_CAP.UNLIMITED,
1667        PLAYBACK_CAPPING_STATUS,
1668        MIN_HDCP_VERSION
1669    };
1670
1671})(jQuery);
1672
1673// Maintain backward compatibility with existing namespace
1674ns('News').Hdcp = HDCPHandler;
1675
1676/**
1677 * Concurrency Monitor - Refactored
1678 * Manages Adobe Primetime Concurrency Monitoring sessions
1679 * 
1680 * Responsibilities:
1681 * - Session lifecycle (create, heartbeat, terminate)
1682 * - Stream requests with authentication
1683 * - Error handling and enforcement
1684 */
1685
1686const ConcurrencyMonitor = (function($) {
1687    'use strict';
1688
1689    // Constants
1690    const CM_REASONS = {
1691        400: "Bad Request - Insufficient metadata",
1692        401: "Unauthorized - Invalid or missing application ID",
1693        409: "Conflict - Session is not allowed to start.",
1694        410: "Gone - Session no longer active."
1695    };
1696
1697    const USER_NAME = 'b381cccd-3ec4-4aee-a51b-1c2f56bce4c0';
1698    const PASSWORD = "";
1699    const HEARTBEAT_BUFFER_MS = 3;
1700
1701    // State
1702    let state = {
1703        location: null,
1704        heartbeatTimeoutId: 0,
1705        sessId: null,
1706        mvpd: null,
1707        concurrencyMonitoringEnabled: true,
1708        isConcurrencyEnforced: false,
1709        sessionRetryCount: 0
1710    };
1711    const MAX_SESSION_RETRIES = 3;
1712
1713    // Private Methods
1714
1715    function buildUrl() {
1716        return `https://streams.adobeprimetime.com/v2/sessions/${state.mvpd}/${state.sessId}`;
1717    }
1718
1719    function getAuthHeaders() {
1720        return {
1721            "Authorization": "Basic " + btoa(USER_NAME + ":" + PASSWORD),
1722            "Accept": "application/json"
1723        };
1724    }
1725
1726    async function doFetch(method, location = null) {
1727        const url = buildUrl() + (location ? "/" + location : '');
1728        const headers = getAuthHeaders();
1729        return fetch(url, { method, headers });
1730    }
1731
1732    function calculateHeartbeatTimeout(response) {
1733        let expireDate = response.headers.get('Expires');
1734        let currentTime = response.headers.get('Date');
1735        
1736        if (currentTime && currentTime.includes('GMT,')) {
1737            currentTime = currentTime.split('GMT,')[0] + 'GMT';
1738        }
1739        
1740        const expireMilliSecs = Date.parse(expireDate);
1741        const currentTimemilliSecs = Date.parse(currentTime);
1742        
1743        // Subtract buffer to ensure heartbeat arrives before expiration
1744        return expireMilliSecs - currentTimemilliSecs - HEARTBEAT_BUFFER_MS;
1745    }
1746
1747    function queueNextHeartbeat(response) {
1748        const heartBeatCallTime = calculateHeartbeatTimeout(response);
1749        state.heartbeatTimeoutId = setTimeout(() => {
1750            heartBeatSession();
1751        }, heartBeatCallTime);
1752    }
1753
1754    function clearHeartbeat() {
1755        if (state.heartbeatTimeoutId) {
1756            clearTimeout(state.heartbeatTimeoutId);
1757            state.heartbeatTimeoutId = 0;
1758        }
1759    }
1760
1761    async function handleFailureResponse(response, startTime) {
1762        if (!response.ok) {
1763            News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
1764                response.url, 
1765                response.status, 
1766                "All_Generic_API_Call_Failure", 
1767                startTime,
1768                "GET"
1769            );
1770        }
1771
1772        const message = CM_REASONS[response.status];
1773
1774        if (message) {
1775            console.error(`Error ${response.status} - ${message}`);
1776        } else {
1777            console.error(`Error ${response.status} received from Concurrency Monitoring`);
1778        }
1779
1780        // Handle specific error codes
1781        if (response.status === 410) {
1782            // Session was deleted, clear location and initiate new session
1783             if (state.sessionRetryCount < MAX_SESSION_RETRIES) {
1784                state.sessionRetryCount++;
1785                state.location = null;
1786                await initiateSession();
1787            } else {
1788                console.error('CM - Max session retries reached for 410.');
1789                state.sessionRetryCount = 0;
1790                MiniPlayerUI.showErrorContainer(true);
1791            }
1792        } else if (response.status === 409) {
1793            // Conflict - session limit exceeded
1794            if (state.concurrencyMonitoringEnabled) {
1795                News.App.QuantumAnalytics.playbackExitBeforeStartByApp(
1796                    'SpectrumLocal_playbackExitBeforeStart_linear_byApplication', 
1797                    'SLWLP-1005', 
1798                    'Concurrency Enforced'
1799                );
1800                await sessionLimitExceeded(response);
1801            } else {
1802                // Not enforcing, restart session
1803                await initiateSession();
1804            }
1805        }
1806    }
1807
1808    async function sessionLimitExceeded(response) {
1809        try {
1810            const body = await response.json();
1811            const associatedAdvice = body.associatedAdvice[0];
1812            
1813            // Redirect to limit-exceeded page
1814            News.App.Pinxt.Utils.goToPage("limit-exceeded");
1815            
1816            setWebEvent("modalView", {
1817                pageName: "concurrency monitoring block page",
1818                cmDisplayText: "you've exceeded the maximum number of streaming sessions",
1819                cmRuleName: `${associatedAdvice.policyName}|${associatedAdvice.ruleName}`,
1820                events: ["pageView,blockPageViewed"]
1821            });
1822        } catch (error) {
1823            console.error('CM - Error handling session limit exceeded:', error);
1824        }
1825    }
1826
1827    function setupUnloadHandler() {
1828        $(window).off("beforeunload", browserUnload);
1829        $(window).on("beforeunload", browserUnload);
1830    }
1831
1832    // Public Methods
1833
1834    async function initiateSession() {
1835        const url = buildUrl();
1836        const headers = getAuthHeaders();
1837        const startTime = new Date().getTime();
1838
1839        try {
1840            const response = await fetch(url, {
1841                method: "POST",
1842                headers: headers
1843            });
1844
1845            if (response.ok) {
1846                News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
1847                    response.url, 
1848                    response.status, 
1849                    "All_Generic_API_Call_Success", 
1850                    startTime, 
1851                    "POST", 
1852                    JSON.stringify(headers), 
1853                    "", 
1854                    response
1855                );
1856
1857                // Record location for heartbeat
1858                state.location = response.headers.get('Location');
1859                state.sessionRetryCount = 0;
1860
1861                // Setup unload handler
1862                setupUnloadHandler();
1863
1864                // Queue first heartbeat
1865                const heartBeatCallTime = calculateHeartbeatTimeout(response);
1866                setTimeout(() => {
1867                    heartBeatSession();
1868                }, heartBeatCallTime);
1869
1870                return true;
1871            } else if (!state.isConcurrencyEnforced) {
1872                // Not enforcing, succeed anyway
1873                return true;
1874            } else {
1875                await handleFailureResponse(response, startTime);
1876                return false;
1877            }
1878        } catch (error) {
1879            console.error('CM - Session initiation error:', error);
1880            await handleFailureResponse(error, startTime);
1881            return false;
1882        }
1883    }
1884
1885    async function heartBeatSession() {
1886        // No active session
1887        if (!state.location) {
1888            return;
1889        }
1890
1891        // Clear any existing timeout
1892        clearHeartbeat();
1893
1894        const startTime = new Date().getTime();
1895
1896        try {
1897            const response = await doFetch('POST', state.location);
1898
1899            if (response.ok) {
1900                News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
1901                    response.url, 
1902                    response.status, 
1903                    "All_Generic_API_Call_Success", 
1904                    startTime, 
1905                    "POST", 
1906                    "", 
1907                    "", 
1908                    response
1909                );
1910                queueNextHeartbeat(response);
1911            } else {
1912                await handleFailureResponse(response, startTime);
1913            }
1914        } catch (error) {
1915            console.error('CM - Heartbeat error:', error);
1916            await handleFailureResponse(error, startTime);
1917        }
1918    }
1919
1920    async function terminateSession() {
1921        if (!state.location) {
1922            return Promise.resolve();
1923        }
1924
1925        console.log('CM - terminate session ' + state.location);
1926
1927        try {
1928            await doFetch('DELETE', state.location);
1929            console.log("CM - session deleted");
1930        } catch (error) {
1931            console.error('CM - Error terminating session:', error);
1932        } finally {
1933            // Clear local state
1934            state.location = null;
1935            clearHeartbeat();
1936        }
1937    }
1938
1939    function browserUnload() {
1940        // Use sendBeacon for reliable unload
1941        if (state.location) {
1942            navigator.sendBeacon(
1943                `/services/news/v1/delete-session.servlet/${USER_NAME}/${state.mvpd}/${state.sessId}/${state.location}`
1944            );
1945        }
1946    }
1947
1948    async function getLinearStream(stream, data, zip, enableDai, resourceId, userDetails, playerContext) {
1949        const authState = News.App.Pinxt.AuthState;
1950        
1951        // Get MVPD from authState (regular MVPD or temp pass)
1952        if (authState.tempPass.isActive) {
1953            state.mvpd = authState.tempPass.mvpd;
1954        } else if (authState.isAuthenticated) {
1955            state.mvpd = authState.mvpd;
1956        } else {
1957            state.mvpd = userDetails?.mvpd || 'Spectrum';
1958        }
1959        
1960        state.concurrencyMonitoringEnabled = true;
1961        
1962        const concurrencyDataValue = $('.live-player-container').data('concurrencyenabled');
1963        state.isConcurrencyEnforced = concurrencyDataValue === true;
1964
1965        // Set session ID
1966        if (userDetails.hhid && userDetails.hhid !== 'undefined') {
1967            state.sessId = userDetails.hhid;
1968        } else {
1969            state.sessId = userDetails.userID;
1970        }
1971        Cookies.set("sessionID", state.sessId);
1972
1973        // If enforced and home-based auth
1974        if (state.isConcurrencyEnforced && userDetails.hba_status === 'true') {
1975            return requestStream(stream, data, zip, enableDai, resourceId, state.sessId);
1976        } else {
1977            // Out of home - initiate CM session
1978            if (state.location) {
1979                console.log("CM - Reusing existing CM session.");
1980                return requestStream(stream, data, zip, enableDai, resourceId, state.sessId);
1981            } else {
1982                // For mini player: defer CM session until user unmutes
1983                if (playerContext && playerContext.isMiniLivePlayer) {
1984                    console.log("CM - Mini player detected, deferring session until unmute.");
1985                    playerContext.concurrencyMonitoringRequired = true;
1986                    return requestStream(stream, data, zip, enableDai, resourceId, state.sessId);
1987                }
1988
1989                const sessionStarted = await initiateSession();
1990                if (sessionStarted) {
1991                    return requestStream(stream, data, zip, enableDai, resourceId, state.sessId);
1992                }
1993            }
1994        }
1995
1996        return Promise.reject(new Error("Could not start Concurrency Monitoring session"));
1997    }
1998
1999    function sendAuthZErrorEvent(errorDescription, statusCode) {
2000        const eventPayload = {
2001            event: "pageView, errorView, errorEvent",
2002            pageLanguage: "en",
2003            appSection: 'Authentication',
2004            pageID: "authZ_error",
2005            pageName: "Error: AuthZ Error",
2006            pageType: "error message",
2007            errorName: "noAuthZ",
2008            errorType: "authentication",
2009            errorDescription: errorDescription,
2010            errorCode: statusCode
2011        };
2012        console.log('[AuthZ] trackState - event = pageview,errorView,errorEvent', eventPayload);
2013        window._satellite && window._satellite.track('trackState', eventPayload);
2014    }
2015
2016    async function requestStream(url, data, zip, enableDai, resourceId, sessId) {
2017        const result = $.Deferred();
2018        const videoCodes = data.codec === "hevc" ? ["hevc", "avc"] : ["avc"];
2019        const isOnPlant = localStorage.getItem("onPlant") === "true";
2020        const ipvsV5Switch = $('body').data('ipvs-v5-switch');
2021
2022        const params = {
2023            'csid': data.csid,
2024            'secureTransport': true,
2025            'householdid': sessId,
2026            'use_token': true,
2027            'useOnPlantRouting': isOnPlant
2028        };
2029
2030        if (zip) {
2031            params.zipCode = zip;
2032        }
2033
2034        // Special rule for Hawaii Streams
2035        if (enableDai === "false" || data.ncsId === "17866" || data.ncsId === "18250") {
2036            params.dai = false;
2037        }
2038
2039        try {
2040            let mediaInfo = await News.App.Pinxt.AuthFlow.getAuthorizationDecisions(resourceId);
2041            let authorized = !!mediaInfo && !!mediaInfo.decisions?.[0]?.authorized;
2042
2043            // NEWS-9893: any stream that carries a temp pass is "open". If the current
2044            // identity is not authorized (e.g. an authenticated MVPD user not entitled
2045            // to this resource — including the fallback channel), fall back to the
2046            // stream's OWN temp pass and re-authorize so it can play.
2047            const streamTempPass = data.tempPass;
2048            const hasTempPass = !!streamTempPass &&
2049                String(streamTempPass).toLowerCase().indexOf('temppass') !== -1;
2050            if (!authorized && hasTempPass) {
2051                console.log('[MiniPlayer] Stream not authorized under current identity; falling back to its temp pass:', streamTempPass);
2052                const tempPassLoggedIn = await News.App.Pinxt.AuthFlow.loginTempPassUser(streamTempPass);
2053                if (tempPassLoggedIn) {
2054                    const retryInfo = await News.App.Pinxt.AuthFlow.getAuthorizationDecisions(resourceId);
2055                    if (retryInfo && retryInfo.decisions?.[0]?.authorized) {
2056                        mediaInfo = retryInfo;
2057                        authorized = true;
2058                    }
2059                }
2060            }
2061
2062            // Check if authorization failed
2063            if (!authorized) {
2064                const isAuthenticated = News.App.Pinxt.AuthState.isAuthenticated || News.App.Pinxt.AuthState.tempPass.isActive;
2065                const decision = mediaInfo?.decisions?.[0];
2066                const errorDetails = decision?.error?.details || decision?.error?.message || 'Authorization decision denied'
2067                const isSpanish = !!($('.site-header .header-ele').data('noticias-region'));
2068
2069                sendAuthZErrorEvent(
2070                    errorDetails || 'Authorization decision denied',
2071                    decision?.error?.status || (mediaInfo ? 403 : 401)
2072                );
2073
2074                if (isAuthenticated && errorDetails) {
2075                    MiniPlayerUI.showErrorContainer(true);
2076                    $('.error-container').html(`<p>${errorDetails}</p>`);
2077                } else {
2078                    MiniPlayerUI.showErrorContainer(true);
2079                    $('.error-container').html(isSpanish
2080                        ? '<p>Se ha producido un error. Por favor, inténtalo de nuevo</p>'
2081                        : '<p>Something went wrong. Please reload the stream.</p>'
2082                    );
2083                }
2084
2085                result.reject({ status: decision?.error?.status || 403, message: errorDetails || 'Authorization failed', isAuthZDenial: true });
2086                return result.promise();
2087            }
2088
2089            // Check for temp pass expiry error
2090            const decision = mediaInfo?.decisions?.[0];
2091            if (decision && !decision.authorized &&
2092                decision.error?.code === 'temporary_access_duration_limit_exceeded') {
2093                console.log('[TempPass] Temp pass has expired during playback');
2094                
2095                // Clear temp pass data
2096                News.App.Pinxt.AuthFlow.clearTempPassData();
2097                
2098                // Show expiry message
2099                if (typeof MiniPlayerUI !== 'undefined') {
2100                    MiniPlayerUI.showTempPassExpiredMessage();
2101                }
2102                
2103                result.reject(new Error('Temp pass expired'));
2104                return result.promise();
2105            }
2106            
2107            params.token = decision?.token?.serializedToken || null;
2108
2109            const body = {
2110                "deviceCapabilities": {
2111                    "drm": data.drmType,
2112                    "videoCodecs": videoCodes,
2113                    "audioCodecs": ["AAC"],
2114                    "packaging": data.streamType,
2115                    "videoFormats": ["default"]
2116                }
2117            };
2118
2119            const ajaxConfig = {
2120                url: url + '?' + $.param(params),
2121                data: JSON.stringify(body),
2122                statusCode: {
2123                    403: result.reject,
2124                    401: result.reject
2125                },
2126                method: 'POST',
2127                contentType: 'application/json'
2128            };
2129
2130            if (ipvsV5Switch === true) {
2131                ajaxConfig.headers = {
2132                    'client_id': News.App.Pinxt.AuthConfig.clientId,
2133                    'Authorization': "Bearer " + News.App.Pinxt.TokenManager.getCurrentAccessToken(),
2134                    'Accept': 'application/json',
2135                    'X-Device-Info': localStorage.getItem("deviceInfo")
2136                };
2137            }
2138
2139            const startTime = new Date().getTime();
2140
2141            $.ajax(ajaxConfig)
2142                .done(function(data, textStatus, jqXHR) {
2143                    if (ipvsV5Switch === true) {
2144                        result.resolve({
2145                            data: data,
2146                            headers: {
2147                                "x-set-ast": jqXHR.getResponseHeader("x-set-ast"),
2148                                "x-trace-id": jqXHR.getResponseHeader("x-trace-id")
2149                            }
2150                        });
2151                    } else {
2152                        result.resolve(data);
2153                    }
2154                    
2155                    News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
2156                        url, 
2157                        200, 
2158                        "All_Generic_API_Call_Success", 
2159                        startTime, 
2160                        "POST", 
2161                        JSON.stringify(body), 
2162                        "", 
2163                        jqXHR
2164                    );
2165                })
2166                .fail(function(jqXHR) {
2167                    const responseText = News.App.QuantumAnalytics.extractAjaxErrorMessage(jqXHR);
2168                    News.App.QuantumAnalytics.quantumApiSuccessOrFailure(
2169                        url, 
2170                        jqXHR.status, 
2171                        "All_Generic_API_Call_Failure", 
2172                        startTime, 
2173                        "POST", 
2174                        JSON.stringify(body), 
2175                        responseText, 
2176                        jqXHR
2177                    );
2178                    result.reject(jqXHR);
2179                });
2180        } catch (error) {
2181            result.reject(error.responseJSON || error);
2182        }
2183
2184        return result.promise();
2185    }
2186
2187    // Public API
2188    return {
2189        getLinearStream,
2190        requestStream,
2191        initiateSession,
2192        heartBeatSession,
2193        terminateSession,
2194        browserUnload,
2195        
2196        // For backward compatibility
2197        get location() { return state.location; },
2198        get sessId() { return state.sessId; }
2199    };
2200
2201})(jQuery);
2202
2203// Maintain backward compatibility with existing namespace
2204ns('News').concurrencyMonitoring = ConcurrencyMonitor;
2205
2206(function ($) {
2207    'use strict';
2208
2209    $(function () {
2210        renderTimeOnload();
2211        // NEWS-9752: Skip the onNow poll while offline or the tab is hidden;
2212        // otherwise each tick fails with response code 0 and, on re
2212connect, a
2213        // burst of these failures gets reported to Quantum.
2214        setInterval(function(){
2215            if (!navigator.onLine || document.hidden) {
2216                return;
2217            }
2218            renderTimeOnload();
2219        }, 30000);
2220
2221        // NEWS-9752: Refresh once when connectivity or visibility is restored.
2222        var resumeRenderTime = function () {
2223            if (navigator.onLine && !document.hidden) {
2224                renderTimeOnload();
2225            }
2226        };
2227        window.addEventListener('online', resumeRenderTime);
2228        document.addEventListener('visibilitychange', resumeRenderTime);
2229
2230        function showFallbackLayout() {
2231            var $container = $('.live-stream-promo--container.mini-player');
2232            var $fallback = $('.mini-player-fallback');
2233
2234            if ($fallback.length === 0) {
2235                console.warn('[MiniPlayer] .mini-player-fallback element not found in DOM');
2236                return;
2237            }
2238
2239            // Remove unauthenticated class so platform-player.less doesn't hide the player
2240            $container.removeClass('unauthenticated').addClass('authenticated');
2241            $container.removeClass('fallback-configured');
2242
2243            // Move the player inside the fallback section — between header and guide-data
2244            var $player = $container.find('#html5-player');
2245            var $header = $fallback.find('.mini-player-fallback__header');
2246            $player.insertAfter($header);
2247            $player.show();
2248
2249            // Hide the default heading and live badge (fallback section has its own)
2250            $container.find('h2.heading-title').hide();
2251            $container.find('.live-badge.show-when-authenticated').hide();
2252
2253            // Show the fallback section
2254            $fallback.show();
2255
2256            // NEWS-9894: an authenticated (regular MVPD) user is already signed in, so the
2257            // fallback layout must NOT show the Sign In CTA. Temp-pass/unauthenticated
2258            // users still see it.
2259            var authState = News.App.Pinxt && News.App.Pinxt.AuthState;
2260            if (authState && authState.isAuthenticated) {
2261                $fallback.find('.mini-player-fallback__signin').hide();
2262            }
2263
2264            // The existing live-stream-promo--block stays hidden
2265            $container.find('.live-stream-promo--block').hide();
2266
2267            // Show the container (was hidden to prevent flash)
2268            $container.show();
2269
2270            // Fetch schedule data for the fallback channel
2271            renderFallbackScheduleData();
2272            // Fetch schedule data for the default (locked) channel
2273            renderDefaultChannelScheduleData();
2274        }
2275
2276        function showAuthenticatedView() {
2277            var $container = $('.live-stream-promo--container.mini-player');
2278            $container.removeClass('unauthenticated').addClass('authenticated');
2279            $container.find('.show-when-authenticated').show();
2280            $container.find('.mini-player-fallback').hide();
2281            $container.find('.live-stream-promo--block').hide();
2282            if ($('.site-header .header-ele').data('noticias-region') || window.location.host.includes('spectrumnoticias')) {
2283                $container.find('h2').addClass('spanish-headline');
2284            }
2285            // Show the container (was hidden to prevent flash)
2286            $container.show();
2287        }
2288
2289        async function renderFallbackScheduleData() {
2290            var $player = $('#html5-player');
2291            var fallbackTmsId = $player.data('tmsid'); // After swap, this is the fallback tmsId
2292
2293            if (!fallbackTmsId) return;
2294
2295            var onNow = await getRavenData(fallbackTmsId);
2296            if (typeof onNow !== 'undefined' && onNow) {
2297                var $container = $('.live-stream-promo--container.mini-player');
2298                var $fallbackChannel = $container.find('.mini-player-fallback__channel');
2299                var $programName = $fallbackChannel.find('.fallback-program-name-dynamic');
2300                var $programTime = $fallbackChannel.find('.mini-player-fallback__program-time');
2301
2302                if ($programName.length && !$programName.text()) {
2303                    $programName.text(onNow.title || '');
2304                }
2305                if ($programTime.length) {
2306                    var startTime = moment(onNow.startTimeUtcUnixTime).format('h:mm A');
2307                    var endTime = moment(onNow.endTimeUtcUnixTime).format('h:mm A');
2308                    $programTime.text(startTime + ' - ' + endTime);
2309                }
2310            }
2311        }
2312
2313        async function renderDefaultChannelScheduleData() {
2314            var $container = $('.live-stream-promo--container.mini-player');
2315            // The container's data-tmsid is the original default channel tmsId (from server render)
2316            var defaultTmsId = $container.attr('data-tmsid');
2317
2318            if (!defaultTmsId) return;
2319
2320            var onNow = await getRavenData(defaultTmsId);
2321            if (typeof onNow !== 'undefined' && onNow) {
2322                var $defaultChannel = $container.find('.mini-player-fallback__default-channel');
2323                var $programName = $defaultChannel.find('.default-program-name-dynamic');
2324                var $programTime = $defaultChannel.find('.mini-player-fallback__default-program-time');
2325
2326                if ($programName.length && !$programName.text()) {
2327                    $programName.text(onNow.title || '');
2328                }
2329                if ($programTime.length) {
2330                    var startTime = moment(onNow.startTimeUtcUnixTime).format('h:mm A');
2331                    var endTime = moment(onNow.endTimeUtcUnixTime).format('h:mm A');
2332                    $programTime.text(startTime + ' - ' + endTime);
2333                }
2334            }
2335        }
2336
2337        // Listen for events from player-access-enabler-wrapper.js
2338        window.addEventListener('miniPlayerFallbackReady', function () {
2339            console.log('[MiniPlayer] Fallback ready event received');
2340            showFallbackLayout();
2341        });
2342
2343        window.addEventListener('miniPlayerAuthenticated', function () {
2344            console.log('[MiniPlayer] Authenticated event received');
2345            showAuthenticatedView();
2346        });
2347    });
2348
2349    function renderTimeOnload() {
2350        $('.live-stream-promo--container.mini-player').each(async function() {
2351            let tmsId = $(this).data('tmsid');
2352            let timeElem = $(this).find('.live-stream--program-time');
2353            let onNow = await getRavenData(tmsId);
2354            if (typeof onNow !== 'undefined') {
2355                let startTime = moment(onNow.startTimeUtcUnixTime).format('h:mm A');
2356                let endTime = moment(onNow.endTimeUtcUnixTime).format('h:mm A');
2357                timeElem.text(startTime + ' - ' + endTime);
2358                if ($(this).find(".live-stream--program-name").length > 0 && !($(this).find(".live-stream--program-name").text())) {
2359                    $(this).find(".live-stream--program-name").text(onNow.title);
2360                }
2361            }
2362        });
2363    }
2364
2365    function getRavenData(tmsID) {
2366        return new Promise((resolve) => {
2367            let startTime = new Date().getTime();
2368            let onNow;
2369            let ravenApi = $('#html5-player').data('ravenapi') + tmsID;
2370            $.ajax({
2371                url: ravenApi,
2372                type: 'GET',
2373                dataType: 'json',
2374            }).done(function (data, textStatus, jqXHR) {
2375                if (typeof data[0] !== 'undefined') {
2376                    onNow = data[0]['onNow'];
2377                }
2378                News.App.QuantumAnalytics.quantumApiSuccessOrFailure(ravenApi, jqXHR.status, "All_Generic_API_Call_Success", startTime, "GET", "", "", jqXHR);
2379                resolve(onNow);
2380            }).fail(function (jqXHR) {
2381                let responseText = News.App.QuantumAnalytics.extractAjaxErrorMessage(jqXHR);
2382                News.App.QuantumAnalytics.quantumApiSuccessOrFailure(ravenApi, jqXHR.status, "All_Generic_API_Call_Failure", startTime, "GET", "", responseText, jqXHR);
2383                resolve(onNow);
2384            });
2385        });
2386    }
2387
2388})(jQuery);
2389
2390/**
2391 * Mini Live Player - Main Entry Point
2392 * Refactored to use modular architecture
2393 *
2394 * Dependencies (loaded via js.txt):
2395 * - mini-player-config.js
2396 * - mini-player-utils.js
2397 * - caption-handler.js
2398 * - drm-strategy.js
2399 * - analytics-helper.js
2400 * - mini-player-ui.js
2401 * - mini-player-integration.js
2402 * - mini-player-events.js
2403 * - concurrency-monitor.js
2404 * - hdcp.js
2405 * - mini-live-player.js
2406 */
2407
2408(function ($) {
2409    'use strict';
2410
2411    class MiniLivePlayerInstance {
2412        constructor(containerId = 'html5-player', videoElementId = 'live-player') {
2413            this.isMiniLivePlayer = true;
2414            this.currentBitrate = '';
2415            this.retryCount = 0;
2416            this.adNumber = 0;
2417            this.adBreakNumber = 1;
2418            this.MAInit = null;
2419            this.errURL;
2420            this.streamData = {};
2421            this.$html5Player = $(`#${containerId}`);
2422            this.container = document.getElementById(containerId);
2423            this.videoElement = document.getElementById(videoElementId);
2424            this.playerWrapper = null;
2425
2426            this.userDetails = null;
2427            this.resourceId = null;
2428            this.isTuning = true;
2429            this.mediaEventsSetup = false;
2430
2431            this.tempPassValue = null;
2432            this.ipvsV5Switch = $('body').data('ipvs-v5-switch');
2433            this.concurrencyMonitoringSessionStarted = false;
2434            this.concurrencyMonitoringRequired = null;
2435            this.detectedAudioCodec = null;
2436
2437            // ============================
2438            // FIX: Spanish UI detection + runtime fallback label
2439            // ============================
2440            this.isSpanishUI =
2441                !!($('.site-header .header-ele').data('noticias-region')) ||
2442                (window.location.host && window.location.host.includes('spectrumnoticias'));
2443
2444            this.getFallbackCaptionLabel = () => (this.isSpanishUI ? 'Inglés' : 'English');
2445
2446            // Listen for temp pass expiry event
2447            window.addEventListener('tempPassExpired', (event) => {
2448                const tempPassId = event.detail?.tempPassId;
2449                console.log(`[MiniPlayer] Temp pass ${tempPassId} expired, reloading player`);
2450                if (this.playerWrapper) {
2451                    this.loadPlayer(this.playerWrapper);
2452                }
2453            });
2454
2455            this.init(this.container, this.videoElement);
2456
2457            // Event delegation for sign-in clicks (works for dynamically shown fallback elements)
2458            $(document).on('click', '.mini-player-signin__button, .mini-player-fallback__default-thumbnail-wrapper', function(e) {
2459                e.preventDefault();
2460                const isSpanishPage = !!($('.site-header .header-ele').data('noticias-region')) ||
2461                    (window.location.host && window.location.host.includes('spectrumnoticias'));
2462                const regionCookie = Cookies.get('region');
2463                const basePath = regionCookie ? regionCookie.replace('/content/news', '') : '';
2464                const livePath = isSpanishPage ? '/video-en-vivo' : '/live-video';
2465                const cfPath = $('.live-stream-promo--container.mini-player').data('cfpath');
2466                const channelPath = cfPath ? cfPath.split('/').pop() : '';
2467                const htmlSuffix = window.location.pathname.endsWith('.html') ? '.html' : '';
2468                const watchLiveUrl = basePath + livePath + '/watch_spectrum_news_live' + htmlSuffix + '?channelPath=' + channelPath;
2469                const loginUrl = basePath + livePath + '/login' + htmlSuffix;
2470
2471                // NEWS-9894: a fully-authenticated (regular MVPD) user is already signed in.
2472                // Clicking the locked default channel routes straight to Watch Live for that
2473                // channel (which shows the NEWS-9893 entitlement-denied screen) — no login.
2474                const authState = News.App.Pinxt && News.App.Pinxt.AuthState;
2475                if (authState && authState.isAuthenticated) {
2476                    window.location.href = watchLiveUrl;
2477                    return;
2478                }
2479
2480                localStorage.setItem('miniPlayerPostLoginReturn', watchLiveUrl);
2481                window.location.href = loginUrl;
2482            });
2483        }
2484
2485        init() {
2486            // Wait for PiNxt authentication to initialize before proceeding
2487            const waitForPinxt = () => {
2488                if (News.App.Pinxt.AuthFlow.isInitialized) {
2489                    this.proceedWithInit();
2490                } else {
2491                    setTimeout(waitForPinxt, 50);
2492                }
2493            };
2494            waitForPinxt();
2495        }
2496
2497        proceedWithInit() {
2498            console.log('[MiniPlayer] Proceeding with initialization...');
2499            const cfTempPassEnabled = this.$html5Player.data('cftemppassenabled');
2500            const cfChannel = $('#html5-player.live-player-container').data('defaultchannel');
2501            const fallbackChannel = this.$html5Player.data('fallbackchannel');
2502
2503            console.log('[MiniPlayer] Config:', {cfTempPassEnabled, cfChannel, fallbackChannel, isBot: News.App.Pinxt.Utils.isBot()});
2504
2505            if (cfTempPassEnabled === true && !News.App.Pinxt.Utils.isBot()) {
2506                // Determine which channel to auth against:
2507                // If default channel has temp pass configured (data-temppassvalue), auth against the default channel directly
2508                // Otherwise, if fallback channel is configured, auth against that (it always has temp pass)
2509                const defaultChannelTempPass = this.$html5Player.data('temppassvalue');
2510                const defaultHasTempPass = defaultChannelTempPass && defaultChannelTempPass.toString().toLowerCase().indexOf('temppass') !== -1;
2511                const authChannel = (defaultHasTempPass || !fallbackChannel) ? cfChannel : fallbackChannel;
2512                
2513                News.App.Pinxt.AuthFlow.initializeStreamAuth(authChannel).then((authResult) => {
2514                    console.log('[MiniPlayer] Authentication result:', authResult);
2515
2516                    if (authResult.success && authResult.canPlay) {
2517                        console.log('[MiniPlayer] Authentication successful, loading player...');
2518                        finishMiniPlayerLoading.apply(this, [defaultHasTempPass]);
2519                    } else if (!authResult.success) {
2520                        console.log('[MiniPlayer] Authentication failed, showing unauthenticated version');
2521                        $('.live-stream-promo--container.mini-player').removeClass('authenticated').addClass('unauthenticated');
2522                        $('.live-stream-promo--container.mini-player .show-when-authenticated').hide();
2523                        showMiniPlayerSignInButton();
2524                    }
2525                }).catch((error) => {
2526                    console.error('[MiniPlayer] Auth initialization error:', error);
2527                    finishMiniPlayerLoading.apply(this, [defaultHasTempPass]);
2528                });
2529            } else {
2530                console.log('[MiniPlayer] CF temp pass not enabled or bot detected, proceeding normally');
2531                finishMiniPlayerLoading.apply(this);
2532            }
2533
2534            function finishMiniPlayerLoading(defaultHasTempPass) {
2535                console.log('[MiniPlayer] Finishing mini player loading...');
2536                const authState = News.App.Pinxt.AuthState;
2537                const regularAuth = authState.isAuthenticated;
2538                const tempPassAuth = authState.tempPass.isActive;
2539
2540                console.log('[MiniPlayer] Final auth check:', {regularAuth, tempPassAuth, defaultHasTempPass});
2541
2542                if (!regularAuth && !tempPassAuth) {
2543                    console.log('[MiniPlayer] No authentication found, player will not load');
2544                    showMiniPlayerSignInButton();
2545                    return;
2546                }
2547
2548                console.log('[MiniPlayer] User is authenticated, loading player...');
2549                const fallbackChannel = this.$html5Player.data('fallbackchannel');
2550                const isTempPassOnly = tempPassAuth && !regularAuth;
2551
2552                // Fetch the fallback stream, swap the player onto it, and reveal the
2553                // fallback layout (fallback plays above; default channel shown locked below).
2554                const playFallback = () => {
2555                    console.log('[MiniPlayer] Fetching fallback stream details');
2556                    $.ajax({
2557                        url: '/services/api/livestreams/v1/lookup',
2558                        headers: { 'Lookup-Paths': fallbackChannel }
2559                    }).done((resp) => {
2560                        const stream = resp.streams && resp.streams[0];
2561                        if (stream) {
2562                            console.log('[MiniPlayer] Fallback stream details:', stream);
2563                            if (stream.ncsId) this.$html5Player.data('ncsid', stream.ncsId);
2564                            if (stream.tmsId) this.$html5Player.data('tmsid', stream.tmsId);
2565                            if (stream.geoFenceZip) this.$html5Player.data('zip', stream.geoFenceZip);
2566                            if (stream.displayName) this.$html5Player.data('network', stream.displayName);
2567                            if (stream.daiEnabled !== undefined) this.$html5Player.data('enabledai', stream.daiEnabled);
2568                            // NEWS-9794: authorize the fallback stream against its own resource id.
2569                            if (stream.resourceId) this.$html5Player.data('resourceid', stream.resourceId);
2570                            // NEWS-9893: use the fallback stream's own temp pass for the playback fallback.
2571                            if (stream.tempPass) this.$html5Player.data('temppassvalue', stream.tempPass);
2572                        }
2573                        this.loadMiniPlayerWhenAuthenticated();
2574                        window.dispatchEvent(new CustomEvent('miniPlayerFallbackReady'));
2575                    }).fail(() => {
2576                        console.warn('[MiniPlayer] Failed to fetch fallback stream details, loading default');
2577                        this.loadMiniPlayerWhenAuthenticated();
2578                        window.dispatchEvent(new CustomEvent('miniPlayerAuthenticated'));
2579                    });
2580                };
2581
2582                const playDefault = () => {
2583                    console.log('[MiniPlayer] Playing default channel directly');
2584                    this.loadMiniPlayerWhenAuthenticated();
2585                    window.dispatchEvent(new CustomEvent('miniPlayerAuthenticated'));
2586                };
2587
2588                // NEWS-9894: a regular-MVPD user may be authenticated but NOT entitled to
2589                // the default channel. When a fallback is configured, play the fallback and
2590                // show the default channel locked below (no Sign In CTA — already signed in).
2591                if (regularAuth && fallbackChannel) {
2592                    const defaultResourceId = this.$html5Player.data('resourceid');
2593                    News.App.Pinxt.AuthFlow.getEntitledResources([defaultResourceId])
2594                        .then((entitled) => {
2595                            if (defaultResourceId && entitled.indexOf(defaultResourceId) === -1) {
2596                                console.log('[MiniPlayer] Authenticated but not entitled to default channel; playing fallback');
2597                                playFallback();
2598                            } else {
2599                                playDefault();
2600                            }
2601                        })
2602                        .catch((err) => {
2603                            console.error('[MiniPlayer] Entitlement check failed, playing default:', err);
2604                            playDefault();
2605                        });
2606                    return;
2607                }
2608
2609                // Temp-pass-only user with a fallback and a default channel lacking temp pass.
2610                if (isTempPassOnly && fallbackChannel && !defaultHasTempPass) {
2611                    console.log('[MiniPlayer] Temp pass with fallback channel');
2612                    playFallback();
2613                } else {
2614                    playDefault();
2615                }
2616            }
2617
2618            $(window).off("beforeunload", () => {
2619                this.unloadPage();
2620            });
2621            $(window).on("beforeunload", () => {
2622                this.unloadPage();
2623            });
2624        }
2625
2626        loadMiniPlayerWhenAuthenticated() {
2627            // Register with VideoController
2628            MiniPlayerIntegration.registerLoading();
2629
2630            this.playerWrapper = new window.SpectrumPlayer.default(this.container, this.videoElement);
2631            this.zip = this.$html5Player.data('zip');
2632            this.ncsId = this.$html5Player.data('ncsid');
2633            this.tmsId = this.$html5Player.data('tmsid');
2634            this.enableDai = this.$html5Player.data('enabledai');
2635            this.ipvsApi = this.$html5Player.data('ipvsapi');
2636            this.ipvs_url = `${this.ipvsApi}/${this.ncsId}`;
2637
2638            this.playerInitialize(this.playerWrapper, this.container, this.videoElement);
2639            this.loadPlayer(this.playerWrapper);
2640
2641            // Setup PiP monitoring
2642            MiniPlayerIntegration.monitorPiP(this.videoElement);
2643
2644            // Setup Safari bfcache handling
2645            window.onpageshow = function (event) {
2646                if (event.persisted) {
2647                    console.log('navigated to page via back button... page restored from bfcache... reload page to reset encryption, auth, etc');
2648                    window.location.reload();
2649                }
2650            };
2651        }
2652
2653        playerInitialize(playerWrapper, container, videoElement) {
2654            // Apply caption fixes before overlay creation
2655            CaptionHandler.patchShakaTextTrackLabel();
2656
2657            // Use MiniPlayerConfig for initialization
2658            playerWrapper.initialize(MiniPlayerConfig.getPlayerInitConfig()).then(() => {
2659                window.News.Hdcp.enforceHdcpCapping(playerWrapper);
2660
2661                const playerUI = new window.SpectrumPlayer.shakaUi.ui.Overlay(playerWrapper.shakaPlayer, container, videoElement);
2662
2663                // Create custom play/stop button for Shaka
2664                const PlayStopButton = class extends window.SpectrumPlayer.shakaUi.ui.Element {
2665                    constructor(parent, controls) {
2666                        super(parent, controls);
2667                        this.button_ = document.createElement('button');
2668                        this.button_.textContent = 'stop';
2669                        this.button_.type = 'button';
2670                        this.button_.classList.add('material-icons-round');
2671                        this.button_.classList.add('shaka-playstop-button');
2672                        this.button_.classList.add('playing');
2673                        this.parent.appendChild(this.button_);
2674                    }
2675                };
2676
2677                PlayStopButton.Factory = class {
2678                    create(rootElement, controls) {
2679                        return new PlayStopButton(rootElement, controls);
2680                    }
2681                };
2682
2683                // Register custom button
2684                window.SpectrumPlayer.shakaUi.ui.Controls.registerElement('play_stop', new PlayStopButton.Factory());
2685
2686                // Use MiniPlayerConfig for UI configuration
2687                const config = MiniPlayerConfig.getPlayerUIConfig();
2688                playerUI.configure(config);
2689
2690                // Setup caption handling with runtime fallback label
2691                const controls = playerUI.getControls();
2692                CaptionHandler.setupLocalization(controls, this.getFallbackCaptionLabel());
2693
2694                // Setup text track change handler with fallback label
2695                playerWrapper.on(playerWrapper.events.TEXT_TRACKS_CHANGED, () => {
2696                    CaptionHandler.normalizeTextTracks(playerWrapper, this.getFallbackCaptionLabel());
2697                });
2698
2699                // Observe caption UI for changes
2700                CaptionHandler.observeCaptionUI();
2701
2702                // Spanish translations for Spectrum Noticias
2703                if (window.location.host.includes('spectrumnoticias.com')) {
2704                    const localization = playerUI.getControls().getLocalization();
2705                    localization.changeLocale(["es"]);
2706                    const translations = MiniPlayerConfig.getSpanishTranslations();
2707                    const translation_map = new Map(Object.entries(translations));
2708                    localization.insert('es', translation_map);
2709                }
2710
2711                // Setup fullscreen change handlers
2712                document.addEventListener('fullscreenchange', this.handleFullscreenChange.bind(this));
2713                document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange.bind(this));
2714                document.addEventListener('mozfullscreenchange', this.handleFullscreenChange.bind(this));
2715                document.addEventListener('MSFullscreenChange', this.handleFullscreenChange.bind(this));
2716
2717                // Initialize all UI components
2718                MiniPlayerUI.initializeUI(playerWrapper);
2719            });
2720        }
2721
2722        async loadPlayer(playerWrapper) {
2723            // Hide Error Container on load
2724            MiniPlayerUI.showErrorContainer(false);
2725
2726            this.resourceId = this.$html5Player.data('resourceid');
2727            let userDetails;
2728            const authState = News.App.Pinxt.AuthState;
2729
2730            // Get user details if authenticated (regular MVPD or temp pass)
2731            if (authState.isAuthenticated || authState.tempPass.isActive) {
2732                userDetails = await authState.getUserMetaData();
2733            }
2734
2735            this.userDetails = userDetails;
2736            this.currentBitrate = '';
2737            this.widevineUrl = this.$html5Player.data('widevine');
2738            this.playreadyUrl = this.$html5Player.data('playready');
2739            this.fairplayCertUrl = this.$html5Player.data('fairplaycerturl');
2740            this.fairplayLA = this.$html5Player.data('fairplayla');
2741
2742            Cookies.set('stream-url', this.ipvs_url);
2743            if (this.zip) {
2744                Cookies.set('stream-zip', this.zip);
2745            }
2746
2747            this.streamData.contentType = "linear";
2748            this.streamData.manifestURL = this.ipvs_url;
2749            this.streamData.streamType = MiniPlayerUtils.getStreamType();
2750            this.streamData.drmType = MiniPlayerUtils.getDrmType();
2751            this.streamData.length = 86400;
2752            this.streamData.tmsid = this.tmsId.toString();
2753            this.streamData.tmsSeriesId = this.tmsId.toString();
2754            this.streamData.network = this.$html5Player.data('network') || "Spectrum News";
2755            this.streamData.text = this.$html5Player.data('network') || "Spectrum News";
2756            this.streamData.playerState = "webMiniPlayer";
2757            this.streamData.liveStreamStartMode = "manual";
2758            this.streamData.ncsId = this.ncsId.toString();
2759            // NEWS-9893: carry the current stream's temp pass so the requestStream
2760            // fallback can use it if the user is not otherwise authorized.
2761            this.streamData.tempPass = this.$html5Player.data('temppassvalue');
2762
2763            if (this.streamData.streamType === 'hls') {
2764                this.streamData.csid = $('#html5-player').data('fpcsid');
2765            } else {
2766                this.streamData.csid = $('#html5-player').data('dashcsid');
2767            }
2768
2769            this.streamData.codec = await playerWrapper.getVideoCodecs("hevc").then(function (data) {
2770                if (data.includes("hevc")) {
2771                    return "hevc";
2772                } else {
2773                    return "avc";
2774                }
2775            });
2776
2777            this.playbackSelectEvents();
2778        }
2779
2780        playbackSelectEvents() {
2781            return new Promise(async (resolve) => {
2782                const isTargetedBotBrowserVersion = MiniPlayerUtils.isTargetedBot();
2783
2784                if (!isTargetedBotBrowserVersion && !News.App.QuantumAnalytics.isPlaybackAttemptRestart) {
2785                    this.awaitingPlaybackSelect = true;
2786                    const playbackSelectFired = new CustomEvent("mlpPlaybackSelectFired");
2787                    window.dispatchEvent(playbackSelectFired);
2788                    this.awaitingPlaybackSelect = false;
2789                    News.App.QuantumAnalytics.setContent(this.streamData);
2790                }
2791
2792                News.concurrencyMonitoring.getLinearStream(
2793                    this.ipvs_url,
2794                    this.streamData,
2795                    this.zip,
2796                    this.enableDai,
2797                    this.resourceId,
2798                    this.userDetails,
2799                    this
2800                ).then((response) => {
2801                    // Use DRMStrategy to build DRM configuration
2802                    const drmConfig = DRMStrategy.buildDRMConfig(response, {
2803                        ipvsV5Switch: this.ipvsV5Switch,
2804                        widevineUrl: this.widevineUrl,
2805                        fairplayLA: this.fairplayLA,
2806                        fairplayCertUrl: this.fairplayCertUrl
2807                    });
2808
2809                    const {info, headers, LAUrls, drmAuthHeaders} = drmConfig;
2810
2811                    this.playerWrapper.stream_url = info.stream_url;
2812                    this.isTuning = true;
2813
2814                    // Detect audio codec from manifest for Safari (native HLS)
2815                    if (MiniPlayerUtils.isSafari() && info.stream_url) {
2816                        this.detectAudioCodecFromManifest(info.stream_url);
2817                    }
2818
2819                    this.playerWrapper.setSrc({
2820                        manifestUrl: info.stream_url,
2821                        isDrm: true,
2822                        streamType: MiniPlayerUtils.getStreamType(),
2823                        LAUrls: LAUrls,
2824                        drmAuthorizationHeaders: drmAuthHeaders,
2825                        refreshDrmAuthorizationHeader: true,
2826                        drmAuthHeaderExpirationIntervalMs: 20000
2827                    }).then(() => {
2828                        // Clear Spectrum Player's filters after setSrc and register our custom filter
2829                        if (this.ipvsV5Switch === true) {
2830                            this.playerWrapper.shakaPlayer.getNetworkingEngine().clearAllRequestFilters();
2831                            this.playerWrapper.shakaPlayer.getNetworkingEngine().registerRequestFilter(async (type, request) => {
2832                                if (type === 2) {
2833                                    if (MiniPlayerUtils.isTokenExpired(this.playerWrapper)) {
2834                                        let sessionId = Cookies.get("sessionID");
2835                                        let streamURL = Cookies.get("stream-url");
2836                                        let zip = Cookies.get("stream-zip");
2837                                        let resourceId = this.$html5Player.data('resourceid');
2838                                        let playerWrapper = this.playerWrapper;
2839
2840                                        await News.concurrencyMonitoring.requestStream(
2841                                            streamURL,
2842                                            this.streamData,
2843                                            zip,
2844                                            this.enableDai,
2845                                            resourceId,
2846                                            sessionId
2847                                        ).then(function (response) {
2848                                            const newHeaders = response.headers;
2849                                            playerWrapper.drmAuthHeader.expirationTimeMs = playerWrapper.drmAuthHeader.computeExpirationTimeMs();
2850                                            playerWrapper.drmAuthHeader.authHeaderValue = 'Bearer ' + News.App.Pinxt.TokenManager.getCurrentAccessToken();
2851                                            playerWrapper.drmAuthHeader.astHeaderValue = newHeaders["x-set-ast"];
2852                                        });
2853                                    }
2854
2855                                    // Always use the latest headers from playerWrapper.drmAuthHeader
2856                                    request.headers['Authorization'] = this.playerWrapper.drmAuthHeader.authHeaderValue || 'Bearer ' + News.App.Pinxt.TokenManager.getCurrentAccessToken();
2857                                    request.headers['AST-Authorization'] = this.playerWrapper.drmAuthHeader.astHeaderValue || headers["x-set-ast"];
2858
2859                                    // Add FairPlay query params if they exist
2860                                    if (this.playerWrapper.fairplayQueryParams) {
2861                                        request.uris[0] += (request.uris[0].includes('?') ? '&' : '?') + this.playerWrapper.fairplayQueryParams;
2862                                    }
2863
2864                                    return request;
2865                                }
2866                            });
2867                        } else {
2868                            console.log('ipvsV5Switch is false, skipping filter setup');
2869                        }
2870
2871                        if (MiniPlayerUtils.isSafari()) {
2872                            // Hide the empty resolution option in Safari
2873                            $('#html5-player .shaka-resolution-button').hide();
2874                        }
2875
2876                        this.MAInit = News.App.MediaAnalytics.initMediaAnalyticsData(this.playerWrapper, this.streamData);
2877
2878                        // Setup all event listeners using MiniPlayerEvents module
2879                        MiniPlayerEvents.setupAllEvents(this.playerWrapper, this);
2880
2881                        const playbackSelectFired = new CustomEvent("mlpTuningComplete");
2882                        window.dispatchEvent(playbackSelectFired);
2883                        this.isTuning = false;
2884                        resolve(true);
2885                    }).catch((ex) => {
2886                        this.retry(this.playerWrapper, ex.status, ex.message, ex);
2887                    });
2888                }).catch((ex) => {
2889                    this.isTuning = false;
2890
2891                    // AuthZ denial already handled inline by concurrency monitor - do not redirect
2892                    if (ex && ex.isAuthZDenial) {
2893                        resolve(false);
2894                        return;
2895                    }
2896
2897                    // Handle authorization errors
2898                    if (ex && (ex.status === 401 || ex.status === 403 || ex.message === "Could not start Concurrency Monitoring session")) {
2899                        let message = ex.message || "Not Authorized";
2900                        let errorCode = message === "Not Authorized" ? "SLWLP-3304" : "SLWLP-1003";
2901
2902                        if (!News.App.QuantumAnalytics.isPlayBackExitedBeforeStartByApp) {
2903                            News.App.QuantumAnalytics.playbackExitBeforeStartByApp(
2904                                'SpectrumLocal_playbackExitBeforeStart_linear_byApplication',
2905                                errorCode,
2906                                message,
2907                                ex
2908                            );
2909                        }
2910                        if((ex.message === "Could not start Concurrency Monitoring session") && (document.querySelector('.error-container').checkVisibility())){
2911                            MiniPlayerUI.showErrorContainer(true);
2912                        } else if (ex.message === "Could not start Concurrency Monitoring session") {
2913                            MiniPlayerUI.showConcurrencyErrorMessage();
2914                        } else {
2915                            MiniPlayerUI.showErrorContainer(true);
2916                        }
2917                        
2918                    } else {
2919                        News.App.QuantumAnalytics.uriAcquiredFailure('SpectrumLocal_Stream_URI_Acquired_Failure_-_Linear');
2920                        let message = ex.status === 0 ? 'Network connection failed during getLinearStream()' : ex.message;
2921                        this.retry(this.playerWrapper, ex.status, message, ex);
2922                    }
2923
2924                    resolve(false);
2925                });
2926            });
2927        }
2928
2929        retry(Player, errorCode, errorMsg, error) {
2930            News.concurrencyMonitoring.terminateSession();
2931            let retryLimit = this.$html5Player.data('retrylimit');
2932
2933            if (this.retryCount < retryLimit) {
2934                News.App.QuantumAnalytics.playbackFailureBeforeRetry("SpectrumLocal_playbackFailureBeforeRetry_firstTime", errorMsg, errorCode, error);
2935                Player.stop().then(this.loadPlayer(Player));
2936            } else if (News.App.QuantumAnalytics.isPlaybackFailedBeforeRetry) {
2937                News.App.QuantumAnalytics.playbackFailureAfterRetry('SpectrumLocal_playbackFailure_afterRetry', errorMsg, errorCode, error);
2938                $('.live-player-container .thumbnail').hide();
2939                Player.stop().then(MiniPlayerUI.showErrorContainer(true));
2940            } else {
2941                News.App.QuantumAnalytics.playbackFailed('SpectrumLocal_Playback_Failure_Linear', errorMsg, errorCode, error);
2942            }
2943
2944            this.retryCount++;
2945        }
2946
2947        unloadPage() {
2948            AnalyticsHelper.trackUnloadPage(this.MAInit, true);
2949        }
2950
2951        handleUnmutingForConcurrencyMonitoring() {
2952            if (this.concurrencyMonitoringRequired && !this.concurrencyMonitoringSessionStarted) {
2953                News.concurrencyMonitoring.initiateSession(true).then((started) => {
2954                    if (started) {
2955                        this.concurrencyMonitoringSessionStarted = true;
2956                    } else {
2957                        this.playbackStopHandler();
2958                        MiniPlayerUI.showConcurrencyErrorMessage();
2959                    }
2960                });
2961            }
2962        }
2963
2964        addPlayStopEventHandlers() {
2965            $(this.container).find('.shaka-playstop-button:not(.event-added)').on('click', (e) => {
2966                if (!e.target.classList.contains('stopped')) {
2967                    this.playbackStopHandler();
2968                } else {
2969                    MiniPlayerIntegration.requestNewPlayback('mini-live-player');
2970                }
2971            });
2972
2973            // Only add click handler once
2974            $(this.container).find('.shaka-playstop-button:not(.event-added)').addClass('event-added');
2975        }
2976
2977        onVideoControllerStop() {
2978            console.log('mlp onVideoControllerStop');
2979            // VideoPlayerController uses this to stop a playing video when another playback is requested
2980
2981            // Ensure playback start and stop event listeners are added
2982            this.addPlayStopEventHandlers();
2983
2984            this.playbackStopHandler().then(() => {
2985                // Reset MAInit so next playbackStart starts a new playback session
2986                this.MAInit = null;
2987            });
2988        }
2989
2990        onVideoControllerPlay() {
2991            console.log('mlp onVideoControllerPlay');
2992            MiniPlayerUI.togglePlayStopButton();
2993            // VideoPlayerController uses this to play a video when another playback is requested
2994            this.playbackSelectEvents();
2995        }
2996
2997        detectAudioCodecFromManifest(manifestUrl) {
2998            var self = this;
2999            fetch(manifestUrl)
3000                .then(function(response) { return response.text(); })
3001                .then(function(manifestText) {
3002                    var availableAudioCodecs = [];
3003                    var codecsMatches = manifestText.match(/CODECS="([^"]+)"/g);
3004                    if (codecsMatches) {
3005                        codecsMatches.forEach(function(match) {
3006                            var codecs = match.replace('CODECS="', '').replace('"', '');
3007                            var parts = codecs.split(',');
3008                            parts.forEach(function(part) {
3009                                var c = part.trim().toLowerCase();
3010                                if ((c === 'ec-3' || c === 'ac-3' || c.indexOf('mp4a') === 0) && availableAudioCodecs.indexOf(c) === -1) {
3011                                    availableAudioCodecs.push(c);
3012                                }
3013                            });
3014                        });
3015                    }
3016                    var testEl = document.createElement('audio');
3017                    if (availableAudioCodecs.indexOf('ec-3') !== -1) {
3018                        var canPlayEC3 = testEl.canPlayType('audio/mp4; codecs="ec-3"');
3019                        if (canPlayEC3 === 'probably' || canPlayEC3 === 'maybe') {
3020                            self.detectedAudioCodec = 'ec-3';
3021                        } else {
3022                            self.detectedAudioCodec = availableAudioCodecs.indexOf('mp4a.40.2') !== -1 ? 'mp4a.40.2' : availableAudioCodecs[0] || null;
3023                        }
3024                    } else if (availableAudioCodecs.length > 0) {
3025                        self.detectedAudioCodec = availableAudioCodecs[0];
3026                    }
3027                })
3028                .catch(function() {
3029                    // Silently fail — codec detection is best-effort
3030                });
3031        }
3032
3033        onPlaybackStarted(data) {
3034            const playerWrapper = this.playerWrapper;
3035            this.addPlayStopEventHandlers();
3036
3037            playerWrapper.updateCCState();
3038
3039            if (data.bitrate !== "undefined") {
3040                this.currentBitrate = data.bitrate;
3041            }
3042
3043            // Store audio track labels from event data for use in deferred codec tracking
3044            // data.audioTracks contains language labels e.g. ["en"] or ["en","es"]
3045            if (data.audioTracks && data.audioTracks.length > 0) {
3046                this.availableAudioTracksFromEvent = data.audioTracks;
3047            }
3048
3049            if (window.tuningInterruptedByPlaybackRequest) {
3050                this.isTuning = false;
3051                this.playerWrapper.stop();
3052                MiniPlayerUI.togglePlayStopButton();
3053                return;
3054            }
3055
3056            // Track playback start
3057            if (!News.App.QuantumAnalytics.isPlayBackStarted && News.App.QuantumAnalytics.isPlaybackAttemptRestart) {
3058                AnalyticsHelper.trackPlaybackStart(data.bitrate);
3059            } else if (News.App.QuantumAnalytics.isPlayBackStarted && News.App.QuantumAnalytics.isPlaybackAttemptRestart && News.App.QuantumAnalytics.isPlaybackFailedBeforeRetry) {
3060                AnalyticsHelper.trackPlaybackStart(data.bitrate);
3061            } else {
3062                AnalyticsHelper.trackPlaybackStart(data.bitrate);
3063            }
3064
3065            // Audio codec tracking is now handled in mini-player-events.js
3066            // after News.App.QuantumAnalytics.playbackStarted() fires
3067
3068            $('.live-player-container .thumbnail').hide();
3069            this.retryCount = 0;
3070
3071            this.MAInit.done((res, status, xhr) => {
3072                const location = xhr.getResponseHeader('Location');
3073                News.App.MediaAnalytics.isMuted = playerWrapper.isMuted();
3074                News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerStart());
3075                if ((News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore) && !News.App.MediaAnalytics.isfullscreenStarted) {
3076                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playbackMuteStart());
3077                } else if ((!News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore) && !News.App.MediaAnalytics.isfullscreenStarted) {
3078                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerUnmutedStart());
3079                }
3080                News.App.MediaAnalytics.clearBufferPingInterval();
3081                News.App.MediaAnalytics.clearPausePingInterval();
3082                if (News.App.QuantumAnalytics.isplayerUnmutedBefore) {
3083                    News.App.MediaAnalytics.streamPingInterval(playerWrapper, location);
3084                }
3085            });
3086        }
3087
3088        playbackStopHandler() {
3089            return new Promise(async (resolve) => {
3090                console.log('playbackStopHandler');
3091                this.playerWrapper.stop();
3092                MiniPlayerUI.togglePlayStopButton();
3093
3094                const success = News.App.QuantumAnalytics.playbackStop('SpectrumLocal_Playback_Stop_Linear');
3095
3096                if (!success) {
3097                    // playbackStop failed because playbackStart had not yet fired
3098                    News.App.QuantumAnalytics.playbackExitBeforeStartByUser('SpectrumLocal_playbackExitBeforeStart_linear_byUser');
3099                }
3100
3101                this.MAInit.done((res, status, xhr) => {
3102                    const location = xhr.getResponseHeader('Location');
3103                    News.App.MediaAnalytics.clearBufferPingInterval();
3104                    News.App.MediaAnalytics.clearMediaPingInterval();
3105                    News.App.MediaAnalytics.clearStreamPlayheadPingInterval();
3106
3107                    if (!News.App.MediaAnalytics.IsPaused && !Ne
3107ws.App.MediaAnalytics.isfullscreenStarted && News.App.MediaAnalytics.isMuted) {
3108                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playbackMuteEnd());
3109                    } else if (!News.App.MediaAnalytics.IsPaused && !News.App.MediaAnalytics.isfullscreenStarted && (!News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore)) {
3110                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerUnmutedEnd());
3111                    }
3112
3113                    if (News.App.MediaAnalytics.isMiniPlayerStarted) {
3114                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playBackFinished(this.playerWrapper));
3115                    }
3116
3117                    resolve(true);
3118                });
3119
3120                // Remove from VideoController
3121                MiniPlayerIntegration.resetCurrentlyActive();
3122            });
3123        }
3124
3125        handleFullscreenChange() {
3126            if (document.fullscreenElement) {
3127                // Fullscreen is activated
3128                this.MAInit.done((res, status, xhr) => {
3129                    const location = xhr.getResponseHeader('Location');
3130                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerStop());
3131
3132                    // Stop either unmute or mute state
3133                    if (!News.App.MediaAnalytics.IsPaused && (News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore)) {
3134                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playbackMuteEnd());
3135                    } else if (!News.App.MediaAnalytics.IsPaused && (!News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore)) {
3136                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerUnmutedEnd());
3137                    }
3138
3139                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.fullscreenStart());
3140                });
3141            } else {
3142                // Fullscreen is deactivated
3143                this.MAInit.done((res, status, xhr) => {
3144                    const location = xhr.getResponseHeader('Location');
3145                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.fullscreenStop());
3146                    News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerStart());
3147
3148                    if (!News.App.MediaAnalytics.IsPaused && (News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore)) {
3149                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.playbackMuteStart());
3150                    } else if (!News.App.MediaAnalytics.IsPaused && (!News.App.MediaAnalytics.isMuted && News.App.QuantumAnalytics.isplayerUnmutedBefore)) {
3151                        News.App.MediaAnalytics.triggerEvent(location, News.App.MediaAnalytics.miniPlayerUnmutedStart());
3152                    }
3153                });
3154            }
3155        }
3156    }
3157
3158    function showMiniPlayerSignInButton() {
3159        const isSpanish = window.location.href.includes('noticias') ||
3160            window.location.host.includes('spectrumnoticias');
3161        const signInText = isSpanish ? 'Iniciar sesión para ver' : 'Sign in to watch';
3162
3163        const buttonHtml = `
3164            <div class="mini-player-signin">
3165                <button class="mini-player-signin__button">${signInText}</button>
3166            </div>
3167        `;
3168
3169        $('.live-stream-promo--block').after(buttonHtml);
3170
3171        // Replace play icon with lock icon on the thumbnail
3172        $('.live-stream-left-block .thumbnail .play-button').css({
3173            'content': 'url("/etc/designs/news/clientlibs/css/images/icons/mini-player-lock-icon.svg")',
3174            'background': 'none'
3175        });
3176         $('.live-stream-left-block .thumbnail .play-button').addClass('mobile-lock-btn');
3177    }
3178
3179    // Initialize player if browser is supported
3180    const bowserResult = MiniPlayerUtils.getBrowserInfo();
3181
3182    if ((bowserResult.os.name === "Android" && bowserResult.browser.name === "Firefox") ||
3183        (bowserResult.os.name === "iOS" && bowserResult.browser.name !== "Safari") ||
3184        bowserResult.browser.name === "Internet Explorer") {
3185        console.error("For DRM reasons, live player is not supported on Firefox for Android, IE, and iOS devices that are not using Safari.");
3186    } else {
3187        const mlpInstance = new MiniLivePlayerInstance();
3188
3189        // Register with VideoPlayerController
3190        MiniPlayerIntegration.registerWithController(mlpInstance);
3191    }
3192})(jQuery);
3193

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.