PageSourceSearch

https://www.miniatureartacademy.com/wp-content/themes/miniatureart/assets/js/videolang.js?ver=6ac99931c8caa

js miniatureartacademy.com collected 2026-10-10 01:49:13 UTC 9,676 bytes, 258 lines download raw bytes

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.