1class VideoLang { 2 constructor() { 3 this.currentLang = this.detectLanguage(); 4 this.popup = null; 5 this.currentPlayer = null; 6 } 7 8 detectLanguage() { 9 const htmlLang = (document.documentElement.lang || '').toLowerCase(); 10 return htmlLang.startsWith('es') ? 'es' : 'en'; 11 } 12 13 choosePopup(selector1, selector2) { 14 return document.querySelector(selector1) || document.querySelector(selector2); 15 } 16 17 init(popupEl, popupElAlt) { 18 this.popup = this.choosePopup(popupEl, popupElAlt); 19 this.createToggleButton(); 20 this.bindEvents(); 21 } 22 23 createToggleButton() { 24 if (!this.popup) return; 25 const actionsContainers = this.popup.querySelectorAll('.course__actions'); 26 // Limpia botones previos 27 this.popup.querySelectorAll('.course__actions .course__actions__item--lang').forEach(btn => btn.remove()); 28 // Añade botón en cada contenedor 29 actionsContainers.forEach((actionsContainer) => { 30 const toggleButton = document.createElement('button'); 31 toggleButton.className = 'course__actions__item course__actions__item--lang'; 32 toggleButton.innerHTML = this.getToggleButtonHTML(); 33 actionsContainer.appendChild(toggleButton); 34 }); 35 } 36 37 getToggleButtonHTML() { 38 const flagData = this.currentLang === 'es' 39 ? { src: '/wp-content/themes/miniatureart/assets/img/common/flag_en_US.png', text: 'ENG', alt: 'flag-en' } 40 : { src: '/wp-content/themes/miniatureart/assets/img/common/flag_es_ES.png', text: 'ESP', alt: 'flag-es' }; 41 return `<img src="${flagData.src}" alt="${flagData.alt}"><span>${flagData.text}</span>`; 42 } 43 44 bindEvents() { 45 if (!this.popup) return; 46 this.popup.querySelectorAll('.course__actions .course__actions__item--lang').forEach((button) => { 47 button.addEventListener('click', () => this.toggleLanguage()); 48 }); 49 } 50 51 // --- helpers DOM/maCourse --- 52 getActiveContainerEl() { 53 // Preferente: el activo por clase 54 let el = document.querySelector('.course__lessons__item--active .mavideo-container'); 55 if (el) return el; 56 // Fallback: visible 57 const visibles = Array.from(document.querySelectorAll('.course__lessons__item .mavideo-container')) 58 .filter(n => n.closest('.course__lessons__item')?.offsetParent !== null); 59 if (visibles.length) return visibles[0]; 60 return null; 61 } 62 63 getVideoByIdFromCourse(lessonId) { 64 if (!window.maCourse || !Array.isArray(maCourse.videos)) return null; 65 return maCourse.videos.find(v => String(v.id) === String(lessonId)) || null; 66 } 67 68 getActiveVideoInstance() { 69 // Si hay uno reproduciendo, priorÃzalo 70 const candidates = (window.maCourse?.videos || []).filter(v => v?.player && v?.vdom); 71 const playing = candidates.find(v => v.player.media && !v.player.media.paused); 72 if (playing) return playing; 73 if (candidates.length) return candidates[0]; 74 75 // Si ninguno está embebido aún, intenta con el contenedor visible y embébelo 76 const container = this.getActiveContainerEl(); 77 if (!container) return null; 78 const did = container.dataset.did; 79 const inst = this.getVideoByIdFromCourse(did); 80 if (inst && !inst.player) { 81 try { inst.embed(); } catch(_) {} 82 } 83 return inst || null; 84 } 85 86 replaceLanguagePattern(src, newLang) { 87 if (!src) return src; 88 return newLang === 'en' ? src.replace(/\/es\//g, '/en/') : src.replace(/\/en\//g, '/es/'); 89 } 90// Cambia el src manteniendo tiempo/estado y reenviando cabeceras de auth 91switchVideoSource(vid, { newSrc, newVtt, newPoster }) { 92 return new Promise((resolve) => { 93 if (!vid || !vid.player || !vid.vdom) return resolve(false); 94 95 const player = vid.player; 96 const media = player.media; 97 const wasPlaying = media && !media.paused; 98 const savedTime = player.currentTime || 0; 99 100 // Opcional UI 101 if (newPoster) { 102 try { player.poster = newPoster; } catch(_) { vid.vdom.setAttribute('data-poster', newPoster); } 103 } 104 if (newVtt && player.previewThumbnails?.load) { 105 try { player.previewThumbnails.load(newVtt); } catch(_) {} 106 } 107 108 const restorePlayback = () => { 109 const onLoaded = () => { 110 const t = (savedTime || 0) + 0.05;
110 // nudge para invalidar frames obsoletos 111 media.currentTime = t; 112 player.currentTime = t; 113 media.removeEventListener('loadedmetadata', onLoaded); 114 if (wasPlaying) setTimeout(() => player.play().catch(()=>{}), 50); 115 resolve(true); 116 }; 117 media.addEventListener('loadedmetadata', onLoaded); 118 }; 119 120 // --- Ruta HLS.js (con auth por cabeceras) --- 121 if (vid.hls) { 122 try { player.pause(); } catch(_) {} 123 124 // 1) Recupera config y tokens actuales (si existen) 125 const cfgRef = vid.hls.config || {}; 126 const tokens = this._extractTokensFromExistingHls(vid); 127 128 // 2) Destruye para purgar buffers del manifiesto anterior 129 try { vid.hls.destroy(); } catch(_) {} 130 131 // 3) Vuelve a crear hls con xhrSetup garantizado 132 const cfg = Object.assign({}, cfgRef); 133 if (typeof cfg.xhrSetup !== 'function') { 134 // Fallback: si la config previa no llevaba xhrSetup enumerable, lo recreamos 135 // (si no pudo extraer tokens, usa los que tenÃas en tu embed() como último recurso) 136 const fallback = { m3u8: tokens.m3u8 || 'TMA123456', ts: tokens.ts || 'TMATS2024' }; 137 cfg.xhrSetup = this._xhrSetupFromTokens(fallback); 138 } else { 139 // Aun existiendo, reforzamos que ignore querystring 140 const existing = cfg.xhrSetup; 141 cfg.xhrSetup = (xhr, url) => { 142 const path = this._urlPath(url); 143 existing(xhr, path); // le pasamos el path limpio 144 }; 145 } 146 147 vid.hls = new Hls(cfg); 148 149 // 4) Adjunta y carga la nueva fuente 150 const onAttached = () => { 151 vid.hls.off(Hls.Events.MEDIA_ATTACHED, onAttached); 152 vid.hls.loadSource(newSrc); // sin cache-buster, coincide con tu htaccess 153 }; 154 vid.hls.on(Hls.Events.MEDIA_ATTACHED, onAttached); 155 156 const onParsed = () => { 157 vid.hls.off(Hls.Events.MANIFEST_PARSED, onParsed); 158 restorePlayback(); 159 vid.hls.startLoad?.(); 160 }; 161 vid.hls.on(Hls.Events.MANIFEST_PARSED, onParsed); 162 163 vid.hls.attachMedia(media); 164 vid.url = newSrc; 165 return; 166 } 167 168 // --- Ruta HLS nativo (Safari) --- 169 try { player.pause(); } catch(_) {} 170 try { media.removeAttribute('src'); media.load(); } catch(_) {} 171 media.src = newSrc; 172 try { media.load?.(); } catch(_) {} 173 restorePlayback(); 174 vid.url = newSrc; 175 }); 176} 177 178// --- Helpers para autenticación y URLs --- 179_urlPath(url) { 180 try { return new URL(url, window.location.href).pathname; } 181 catch { return (url || '').split('?')[0]; } 182} 183 184// Extrae los tokens que ya usa la instancia actual de hls (si existen) 185_extractTokensFromExistingHls(vid) { 186 const tok = { m3u8: null, ts: null }; 187 const fn = vid?.hls?.config?.xhrSetup; 188 if (typeof fn !== 'function') return tok; 189 190 const mock = { _last: null, setRequestHeader: (k, v) => { if (k === 'X-Miniatureart') mock._last = v; } }; 191 try { fn(mock, 'x.m3u8'); tok.m3u8 = mock._last || null; } catch {} 192 mock._last = null; 193 try { fn(mock, 'x.ts'); tok.ts = mock._last || null; } catch {} 194 return tok; 195} 196 197// Crea un xhrSetup robusto (ignora querystring) con los tokens dados 198_xhrSetupFromTokens(tokens) { 199 const pathFn = this._urlPath.bind(this); 200 const m3u8Token = tokens?.m3u8; 201 const tsToken = tokens?.ts; 202 return (xhr, url) => { 203 const path = pathFn(url); 204 if ((/\.m3u8$|\.martpl$/).test(path) && m3u8Token) { 205 xhr.setRequestHeader('X-Miniatureart', m3u8Token); 206 } else if (/\.ts$/.test(path) && tsToken) { 207 xhr.setRequestHeader('X-Miniatureart', tsToken); 208 } 209 }; 210} 211 212 213 214 // --- botón de cambio por vÃdeo (no toca el sistema global de idioma) --- 215 async toggleLanguage() { 216 const newLang = this.currentLang === 'es' ? 'en' : 'es'; 217 console.log(`Cambiando idioma SOLO para este vÃdeo: ${this.currentLang} -> ${newLang}`); 218 219 const container = this.getActiveContainerEl(); 220 if (!container) { 221 console.warn('No se encontró un contenedor activo de vÃdeo'); 222 return; 223 } 224 225 // Lee las URLs especÃficas por idioma desde data-attrs; si faltan, intenta derivarlas 226 let newSrc = container.dataset['vsrc' + newLang] || this.replaceLanguagePattern(container.dataset.vsrc || '', newLang); 227 let newVtt = container.dataset['vtt' + newLang] || (container.dataset.vtt ? this.replaceLanguagePattern(container.dataset.vtt, newLang) : undefined); 228 let newPoster = container.dataset['poster' + newLang] || undefined; 229 230 if (!newSrc) { 231 console.error('No se pudo determinar la URL del nuevo idioma para este vÃdeo'); 232 return; 233 } 234 235 const did = container.dataset.did;
236 const vid = this.getVideoByIdFromCourse(did) || this.getActiveVideoInstance(); 237 if (!vid) { 238 console.error('No hay instancia de vÃdeo disponible en maCourse'); 239 return; 240 } 241 242 // === TODO-CHANGE-LANG (implementado) === 243 await this.switchVideoSource(vid, { newSrc, newVtt, newPoster }); 244 245 // Actualiza SOLO el aspecto del botón (estado local del toggle) 246 this.currentLang = newLang; 247 this.updateToggleButton(); 248 } 249 250 updateToggleButton() { 251 if (!this.popup) return; 252 this.popup.querySelectorAll('.course__actions .course__actions__item--lang').forEach((button) => { 253 button.innerHTML = this.getToggleButtonHTML(); 254 }); 255 } 256} 257 258
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.