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}'>ⓘ</a></p>` : 790 `<p>Streaming session limit reached. <a target='_blank' href='${concurrencyErrorPage}'>ⓘ</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.