1/** 2 * MOM Audio Engine (Player V3) 3 * --------------------------------------------------------------------------- 4 * Ein globaler, persistenter HTML5-Audio-Player: 5 * - eine <audio>-Instanz pro Tab, Queue, Token-Handling (deliver.php), 6 * - Zustand in localStorage (überlebt Seitenwechsel â Auto-Resume), 7 * - Stats-Heartbeats 1:1 wie player-stats-1.0.js (Charts-Parität!), 8 * - Media Session API (Lockscreen-Steuerung), 9 * - BroadcastChannel: nur ein Tab spielt gleichzeitig, 10 * - Kompat-Shim: window.MOMPlayer.seekTo(min, sec) (Links in Bewertungen). 11 * 12 * API: window.MOM.audio (siehe unten, Abschnitt "public API") 13 */ 14(function (window, $) { 15 'use strict'; 16 17 if (window.MOM && window.MOM.audio) return; 18 window.MOM = window.MOM || {}; 19 20 /* Platzhalter-Cover für die JS-Fallbacks (Bild-404, fehlende imageUrl). 21 * Serverseitig macht song_no_cover() dasselbe per Datei-mtime; hier muss der 22 * Token von Hand hochgezählt werden, wenn sich das Motiv ändert â /img/* 23 * liefert Nginx mit "expires 30d". */ 24 window.MOM.noCover = function (size) { 25 return '/img/no_cover_' + size + 'x' + size + '.png?v=3'; 26 }; 27 28 var STATE_KEY = 'mom.player.state.v1'; 29 var TOKEN_MAX_AGE = 50 * 60 * 1000; // Token nach 50min erneuern (Lebensdauer 60min) 30 var HISTORY_CAP = 200; // Anti-Repeat-Liste für "ähnliche Songs" 31 var PREV_CAP = 25; // Tiefe des Zurück-Verlaufs 32 33 /* ---------------------------------------------------------------------- 34 * Stats-Kontrakt â bug-für-bug-Port von player/js/player-stats-1.0.js. 35 * Heartbeat alle 10 gespielte Sekunden, Abschlussmeldung bei 'ended'. 36 * ------------------------------------------------------------------- */ 37 var statsGuid = (function randomString(length) { 38 var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 39 var result = ''; 40 for (var i = length; i > 0; --i) result += chars[Math.round(Math.random() * (chars.length - 1))]; 41 return result; 42 })(24); 43 var lastStatCheck = null; 44 45 function statCheckSaving() { 46 var now = new Date().getTime(); 47 if (lastStatCheck) { 48 if (now - lastStatCheck > 1000) { lastStatCheck = now; return true; } 49 return false; 50 } 51 lastStatCheck = now; 52 return true; 53 } 54 55 function statSave(songId, time, ended) { 56 if (!songId) return; 57 var playedSeconds = 10, playedComplete = 0; 58 if (ended === true) { 59 playedComplete = 1; 60 playedSeconds = (Math.round(time / 10) % 10); // Rest speichern (wie Altplayer) 61 } 62 $.ajax({ 63 type: 'POST', async: true, url: '/player/stats/playtime.php', 64 data: { songid: songId, uid: statsGuid, played_seconds: playedSeconds, played_complete: playedComplete, extern: 0 } 65 }); 66 } 67 68 function statHeartbeat(songId, time) { 69 if ((Math.round(time / 1) % 10) === 0 && time > 1 && statCheckSaving()) statSave(songId, time, false); 70 } 71 72 /* ---------------------------------------------------------------------- 73 * State 74 * ------------------------------------------------------------------- */ 75 function defaultState() { 76 return { 77 songId: null, position: 0, paused: true, wasPlaying: false, 78 queue: [], queueIndex: -1, 79 autoSimilar: false, history: [], 80 prevStack: [], // zuletzt gespielte Songs (ganze Items) für den Zurück-Knopf 81 volume: 1, muted: false, updatedAt: 0, 82 dismissed: false, // Nutzer hat den Mini-Player geschlossen (übersteht Seitenwechsel) 83 tokens: {} // songId -> {token, mintedAt} 84 }; 85 } 86 87 function loadState() { 88 try { 89 var raw = window.localStorage && localStorage.getItem(STATE_KEY); 90 if (!raw) return defaultState(); 91 var s = JSON.parse(raw); 92 var st = $.extend(defaultState(), s); 93 // Queue und Zurück-Stapel überleben im localStorage â und zwar mit 94 // genau den Namen, die beim Speichern hineingingen. Ein Browser, der 95 // vor der Korrektur der Produzenten schon einmal gespielt hat, trägt 96 // also weiterhin "Black & White" mit sich herum, ohne dass es je 97 // wieder über normalizeItem liefe. Deshalb hier beim Einlesen. 98 st.queue = (st.queue || []).map(normalizeItem); 99 st.prevStack = (st.prevStack || []).map(normalizeItem); 100 return st; 101 } catch (e) { return defaultState(); } 102 } 103 104 var state = loadState(); 105 var persistTimer = null; 106 107 function persist(immediate) { 108 state.updatedAt = new Date().getTime(); 109 if (immediate) { doPersist(); return; } 110 if (persistTimer) return; 111 persistTimer = setTimeout(doPersist, 1000); 112 } 113 function doPersist() { 114 persistTimer = null; 115 try { if (window.localStorage) localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (e) {} 116 } 117 118 /* ---------------------------------------------------------------------- 119 * Events (pub/sub) 120 * ------------------------------------------------------------------- */ 121 var listeners = {}; 122 function on(evt, fn) { (listeners[evt] = listeners[evt] || []).push(fn); } 123 function emit(evt, data, meta) {
124 (listeners[evt] || []).forEach(function (fn) { try { fn(data, meta); } catch (e) { if (window.console) console.error(e); } }); 125 } 126 127 /* ---------------------------------------------------------------------- 128 * Audio-Element 129 * ------------------------------------------------------------------- */ 130 var audio = new Audio(); 131 audio.preload = 'auto'; 132 audio.volume = (typeof state.volume === 'number') ? state.volume : 1; 133 audio.muted = !!state.muted; 134 135 var pendingSeek = null; // Position, die nach loadedmetadata gesetzt wird 136 var tokenRetryDone = false; // pro Song nur EIN automatischer Token-Retry 137 138 audio.addEventListener('loadedmetadata', function () { 139 if (pendingSeek !== null) { 140 try { audio.currentTime = pendingSeek; } catch (e) {} 141 pendingSeek = null; 142 } 143 }); 144 145 audio.addEventListener('timeupdate', function () { 146 var t = audio.currentTime; 147 statHeartbeat(state.songId, t); 148 state.position = t; 149 persist(false); 150 emit('timeupdate', { position: t, duration: currentDuration() }); 151 }); 152 153 audio.addEventListener('play', function () { 154 state.paused = false; state.wasPlaying = true; 155 state.dismissed = false; // hörbare Wiedergabe hebt "geschlossen" immer auf 156 persist(true); 157 broadcastPlaying(); 158 updateMediaSessionPlaybackState(); 159 emit('play', currentItem()); 160 }); 161 162 audio.addEventListener('pause', function () { 163 state.paused = true; 164 if (!audio.ended) state.wasPlaying = false; 165 persist(true); 166 updateMediaSessionPlaybackState(); 167 emit('pause', currentItem()); 168 }); 169 170 audio.addEventListener('ended', function () { 171 statSave(state.songId, currentDuration(), true); // Abschluss VOR dem Weiterschalten (wie Altplayer) 172 emit('ended', currentItem()); 173 next(true); 174 }); 175 176 var errorSkips = 0; // aufeinanderfolgende unspielbare Songs nicht endlos überspringen 177 audio.addEventListener('error', function () { 178 if (!state.songId) return; 179 if (!tokenRetryDone) { 180 // Wahrscheinlich abgelaufener/rotierter Token â einmalig neu holen und Position wiederherstellen 181 tokenRetryDone = true; 182 var resumeAt = state.position || 0; 183 delete state.tokens[state.songId]; 184 mintToken(state.songId).then(function (token) { 185 setSource(token, resumeAt); 186 if (state.wasPlaying) tryPlay(); 187 }, function () { emit('error', { songId: state.songId }); }); 188 return; 189 } 190 // Song bleibt unspielbar (gelöschte Datei o. ä.) â überspringen, aber max. 3 in Folge 191 emit('error', { songId: state.songId }); 192 if (state.wasPlaying && errorSkips < 3) { 193 errorSkips++; 194 next(true); 195 } else { 196 state.wasPlaying = false; persist(true); 197 } 198 }); 199 audio.addEventListener('playing', function () { errorSkips = 0; }); 200 201 // Eine Quelle der Wahrheit: egal wer die Lautstärke ändert (Mini-Player, 202 // später der Maxi-Player, Media-Keys), gemerkt und weitergesagt wird sie hier 203 audio.addEventListener('volumechange', function () { 204 state.volume = audio.volume; 205 state.muted = audio.muted; 206 persist(false); 207 emit('volumechange', { volume: audio.volume, muted: audio.muted }); 208 }); 209 210 /* Lautstärke lässt sich nicht überall setzen: iOS/Safari hält 211 * HTMLMediaElement.volume schreibgeschützt und überlässt sie den 212 * Hardware-Tasten. An einem Wegwerf-Element prüfen, damit die laufende 213 * Wiedergabe dabei nichts abbekommt. */ 214 var volumeSupport = null; 215 function volumeSupported() { 216 if (volumeSupport === null) { 217 try { var t = new Audio(); t.volume = 0.5; volumeSupport = (t.volume === 0.5); } 218 catch (e) { volumeSupport = false; } 219 } 220 return volumeSupport; 221 } 222 223 function setVolume(v) { 224 v = Math.min(1, Math.max(0, parseFloat(v))); 225 if (isNaN(v)) return; 226 if (v > 0 && audio.muted) audio.muted = false; // am Regler ziehen hebt stumm auf 227 audio.volume = v; 228 } 229 230 function setMuted(onoff) { audio.muted = !!onoff; } 231 232 /* ---------------------------------------------------------------------- 233 * Token-Handling 234 * ------------------------------------------------------------------- */ 235 function mintToken(songId) { 236 var d = $.Deferred(); 237 var cached = state.tokens[songId];
238 if (cached && (new Date().getTime() - cached.mintedAt) < TOKEN_MAX_AGE) { 239 d.resolve(cached.token); 240 return d.promise(); 241 } 242 $.post('/player/get/token', { songid: songId }, null, 'json').done(function (resp) { 243 if (resp && resp.token) { 244 state.tokens = {}; // nur den aktuellen Token behalten (Speicher klein halten) 245 state.tokens[songId] = { token: resp.token, mintedAt: new Date().getTime() }; 246 persist(true); 247 d.resolve(resp.token); 248 } else d.reject(); 249 }).fail(function () { d.reject(); }); 250 return d.promise(); 251 } 252 253 function setSource(token, position) { 254 pendingSeek = (position && position > 0.5) ? position : null; 255 audio.src = '/player/deliver.php?token=' + encodeURIComponent(token); 256 } 257 258 /* ---------------------------------------------------------------------- 259 * Queue 260 * ------------------------------------------------------------------- */ 261 /* Klartext aus Altbestand â das Gegenstück zu mom_textout() auf dem Server. 262 * In der Datenbank stecken HTML-Entities in den Songnamen ("Black & 263 * White"), teils mehrfach kodiert. Die Anzeige setzt die Namen per .text(), 264 * erwartet also Klartext; kam etwas Kodiertes an, stand es wörtlich auf dem 265 * Schirm. 266 * Die Produzenten liefern das inzwischen aufgelöst, aber hier ist der eine 267 * Punkt, durch den JEDES Queue-Item läuft â gespeicherter Zustand von 268 * früher, künftige Aufrufer, alles. Deshalb zur Sicherheit auch hier. 269 * <textarea> ist dafür der sichere Weg: dessen Inhalt wird als reiner Text 270 * gelesen (RCDATA), es entsteht also kein Markup und nichts wird 271 * ausgeführt â anders als bei innerHTML an einem <div>. 272 * Wie auf dem Server abwechselnd auflösen, bis sich nichts mehr ändert 273 * (Deckel bei 4), damit auch mehrfach kodierte Altdaten sauber werden. */ 274 var decoder = null; 275 function decodeEntities(s) { 276 s = (s == null) ? '' : String(s); 277 if (s.indexOf('&') === -1) return s; // der Normalfall, ohne DOM-Umweg 278 if (!decoder) decoder = document.createElement('textarea'); 279 for (var i = 0; i < 4; i++) { 280 decoder.innerHTML = s; 281 var out = decoder.value; 282 if (out === s) break; 283 s = out; 284 } 285 return s; 286 } 287 288 function normalizeItem(it) { 289 return { 290 songId: String(it.songId), songName: decodeEntities(it.songName), 291 artistNick: decodeEntities(it.artistNick), artistUrlNick: it.artistUrlNick || '', 292 imageUrl: it.imageUrl || '', timeSec: parseInt(it.timeSec, 10) || 0 293 }; 294 } 295 296 function findIndex(songId) { 297 songId = String(songId); 298 for (var i = 0; i < state.queue.length; i++) if (state.queue[i].songId === songId) return i; 299 return -1; 300 } 301 302 var queue = { 303 seed: function (items, startIndex) { 304 state.queue = (items || []).map(normalizeItem); 305 state.queueIndex = (typeof startIndex === 'number') ? startIndex : (state.queue.length ? 0 : -1); 306 persist(true); 307 emit('queuechange', queue.list()); 308 }, 309 add: function (item) { 310 item = normalizeItem(item); 311 if (findIndex(item.songId) !== -1) return false; 312 state.queue.push(item); 313 if (state.queueIndex === -1) state.queueIndex = 0; 314 persist(true); 315 emit('queuechange', queue.list()); 316 return true; 317 }, 318 remove: function (songId) { 319 var idx = findIndex(songId); 320 if (idx === -1) return; 321 state.queue.splice(idx, 1); 322 if (idx < state.queueIndex) state.queueIndex--; 323 else if (idx === state.queueIndex) state.queueIndex = Math.min(state.queueIndex, state.queue.length - 1); 324 persist(true); 325 emit('queuechange', queue.list()); 326 }, 327 move: function (from, to) { 328 if (from < 0 || from >= state.queue.length || to < 0 || to >= state.queue.length) return; 329 var it = state.queue.splice(from, 1)[0]; 330 state.queue.splice(to, 0, it); 331 if (state.queueIndex === from) state.queueIndex = to; 332 else if (from < state.queueIndex && to >= state.queueIndex) state.queueIndex--; 333 else if (from > state.queueIndex && to <= state.queueIndex) state.queueIndex++; 334 persist(true); 335 emit('queuechange', queue.list()); 336 }, 337 clear: function () { 338 state.queue = []; state.queueIndex = -1; persist(true); 339 emit('queuechange', []); 340 }, 341 has: function (songId) { return findIndex(songId) !== -1; }, 342 list: function () { return state.queue.slice(); }, 343 index: function () { return state.queueIndex; } 344 }; 345 346 function currentItem() { 347 return (state.queueIndex >= 0 && state.queue[state.queueIndex]) ? state.queue[state.queueIndex] : null; 348 } 349 350 function currentDuration() { 351 if (audio.duration && isFinite(audio.duration)) return audio.duration; 352 var it = currentItem(); 353 return it ? it.timeSec : 0; 354 } 355 356 /* ---------------------------------------------------------------------- 357 * Playback 358 * ------------------------------------------------------------------- */ 359 /* Zurück-Knopf: Die Queue taugt dafür nicht â playIndex() schneidet alles vor 360 * dem laufenden Song weg ("Up Next"), es gibt also kein Davor mehr. Deshalb ein 361 * eigener Stapel mit vollständigen Items. Er wandert in den localStorage-State, 362 * bleibt aber klein: 25 Einträge decken jede realistische Rückwärts-Navigation 363 * ab und kosten nur wenige Kilobyte. (state.history bleibt davon unberührt â 364 * das ist die Anti-Wiederholungs-Liste für "ähnliche Songs".) */ 365 function pushPrev(item) { 366 if (!item || !item.songId) return; 367 var st = state.prevStack || (state.prevStack = []); 368 if (st.length && String(st[st.length - 1].songId) === String(item.songId)) return; 369 st.push(normalizeItem(item)); 370 if (st.length > PREV_CAP) state.prevStack = st.slice(-PREV_CAP); 371 } 372 373 function rememberHistory(songId) { 374 songId = String(songId); 375 var h = state.history; 376 if (h[h.length - 1] !== songId) h.push(songId); 377 if (h.length > HISTORY_CAP) state.history = h.slice(-HISTORY_CAP); 378 } 379 380 function playIndex(idx, position, meta) { 381 if (idx < 0 || idx >= state.queue.length) return; 382 var item = state.queue[idx]; 383
384 // Bisherigen Song merken, damit der Zurück-Knopf ihn wiederfindet â muss VOR 385 // dem Beschneiden der Queue passieren, danach zeigt queueIndex ins Leere 386 var leaving = currentItem(); 387 if (leaving && String(leaving.songId) !== String(item.songId) && !(meta && meta.back)) 388 pushPrev(leaving); 389 390 // "Up Next"-Queue: bereits gespielte/übersprungene Einträge fliegen raus, 391 // der aktuelle Song steht immer oben (Index 0). Die Song-Seite reseedet 392 // ihre Tracklist bei Interaktion ohnehin (seedIfForeign). 393 if (idx > 0) { 394 state.queue.splice(0, idx); 395 idx = 0; 396 } 397 state.queueIndex = idx; 398 state.songId = item.songId; 399 state.position = position || 0; 400 state.dismissed = false; // bewusste Songwahl macht den Player wieder sichtbar 401 tokenRetryDone = false; 402 rememberHistory(item.songId); 403 persist(true); 404 updateMediaSessionMetadata(item); 405 emit('songchange', item, meta || {}); 406 emit('queuechange', queue.list()); 407 mintToken(item.songId).then(function (token) { 408 setSource(token, position || 0); 409 tryPlay(); 410 }, function () { emit('error', { songId: item.songId }); }); 411 } 412 413 function tryPlay() { 414 var p = audio.play(); 415 if (p && p.then) { 416 p.then(function () { emit('resumed'); }, function () { 417 // Autoplay-Policy (iOS/Safari): Nutzer muss einmal tippen 418 state.paused = true; persist(true); 419 emit('resume-blocked', currentItem()); 420 }); 421 } 422 } 423 424 function play(songId, opts) { 425 opts = opts || {}; 426 if (songId === undefined || songId === null) { // einfach fortsetzen 427 if (audio.src) { if (audio.ended) seek(0); tryPlay(); return; } 428 if (currentItem()) { 429 // gespeicherte Position am/nach Songende â von vorne beginnen 430 var it = currentItem(), pos = state.position || 0; 431 if (it.timeSec && pos >= it.timeSec - 1) pos = 0; 432 playIndex(state.queueIndex, pos); 433 return; 434 } 435 return; 436 } 437 var idx = findIndex(songId); 438 if (idx === -1 && opts.item) { // unbekannt â hinter aktueller Position einreihen 439 var insertAt = state.queueIndex + 1; 440 state.queue.splice(insertAt, 0, normalizeItem(opts.item)); 441 emit('queuechange', queue.list()); 442 idx = insertAt; 443 } 444 if (idx === -1) return; 445 playIndex(idx, opts.position || 0); 446 } 447 448 function pause() { audio.pause(); } 449 450 function toggle() { 451 if (audio.paused) play(); else pause(); 452 } 453 454 function seek(seconds) { 455 seconds = Math.max(0, parseFloat(seconds) || 0); 456 if (audio.readyState >= 1) { 457 try { audio.currentTime = seconds; } catch (e) { pendingSeek = seconds; } 458 } else { 459 pendingSeek = seconds; 460 } 461 state.position = seconds; persist(false); 462 } 463 464 function next(auto) { 465 if (state.queueIndex + 1 < state.queue.length) { 466 playIndex(state.queueIndex + 1, 0, { auto: !!auto }); 467 return; 468 } 469 // Queue zu Ende 470 if (state.autoSimilar && state.songId) { 471 fetchSimilarAndPlay(!!auto); 472 } else { 473 // Position zurücksetzen, sonst startet ein späteres Play am Songende 474 state.wasPlaying = false; state.position = 0; persist(true); 475 emit('queue-ended'); 476 } 477 } 478 479 function prev() { 480 if (audio.currentTime > 3) { seek(0); return; } // wie üblich: erst an Songanfang 481 482 var back = (state.prevStack && state.prevStack.length) ? state.prevStack.pop() : null; 483 if (!back) { seek(0); return; } // nichts davor -> an den Anfang 484 485 // Den vorherigen Song vor den aktuellen setzen: so spielt er sofort und 486 // "weiter" führt anschlieÃend wieder zum Song, von dem man kam. 487 state.queue.splice(0, 0, normalizeItem(back)); 488 emit('queuechange', queue.list()); 489 playIndex(0, 0, { back: true }); 490 } 491 492 /* ---------------------------------------------------------------------- 493 * Autoplay "ähnliche Songs" 494 * ------------------------------------------------------------------- */ 495 var similarPending = false;
496 function fetchSimilarAndPlay(auto) { 497 if (similarPending) return; 498 similarPending = true; 499 var exclude = state.history.slice(-HISTORY_CAP); 500 state.queue.forEach(function (it) { exclude.push(it.songId); }); 501 $.ajax({ 502 type: 'POST', url: '/player/get/similar', dataType: 'json', 503 data: { songid: state.songId, exclude: JSON.stringify(exclude) } 504 }).done(function (items) { 505 similarPending = false; 506 if (items && items.length) { 507 var pick = items[Math.floor(Math.random() * Math.min(items.length, 15))]; 508 queue.add(pick); 509 playIndex(state.queue.length - 1, 0, { auto: auto }); 510 emit('autosimilar', pick); 511 } else { 512 emit('queue-ended'); 513 } 514 }).fail(function () { similarPending = false; emit('queue-ended'); }); 515 } 516 517 function setAutoSimilar(onoff) { 518 state.autoSimilar = !!onoff; persist(true); 519 emit('autosimilar-toggle', state.autoSimilar); 520 } 521 522 /* ---------------------------------------------------------------------- 523 * Media Session (Lockscreen) 524 * ------------------------------------------------------------------- */ 525 function absoluteUrl(u) { 526 if (!u) return ''; 527 if (u.indexOf('//') === 0) return window.location.protocol + u; 528 if (u.indexOf('/') === 0) return window.location.origin + u; 529 return u; 530 } 531 532 function updateMediaSessionMetadata(item) { 533 if (!('mediaSession' in navigator) || !item) return; 534 try { 535 var art = absoluteUrl(item.imageUrl); 536 navigator.mediaSession.metadata = new MediaMetadata({ 537 title: item.songName, 538 artist: item.artistNick, 539 album: 'MyOwnMusic', 540 artwork: art ? [ 541 { src: art, sizes: '512x512', type: 'image/jpeg' }, 542 { src: art, sizes: '96x96', type: 'image/jpeg' } 543 ] : [] 544 }); 545 } catch (e) {} 546 } 547 548 function updateMediaSessionPlaybackState() { 549 if (!('mediaSession' in navigator)) return; 550 try { navigator.mediaSession.playbackState = audio.paused ? 'paused' : 'playing'; } catch (e) {} 551 } 552 553 function setupMediaSessionHandlers() { 554 if (!('mediaSession' in navigator)) return; 555 var ms = navigator.mediaSession; 556 var handlers = { 557 play: function () { play(); }, 558 pause: function () { pause(); }, 559 previoustrack: function () { prev(); }, 560 nexttrack: function () { next(); }, 561 seekto: function (d) { if (d && typeof d.seekTime === 'number') seek(d.seekTime); }, 562 seekbackward: function (d) { seek(Math.max(0, audio.currentTime - ((d && d.seekOffset) || 10))); }, 563 seekforward: function (d) { seek(audio.currentTime + ((d && d.seekOffset) || 10)); } 564 }; 565 for (var k in handlers) { 566 try { ms.setActionHandler(k, handlers[k]); } catch (e) {} 567 } 568 // Positionsanzeige auf dem Lockscreen aktuell halten 569 if ('setPositionState' in ms) { 570 audio.addEventListener('timeupdate', function () { 571 try { 572 if (audio.duration && isFinite(audio.duration)) { 573 ms.setPositionState({ duration: audio.duration, playbackRate: audio.playbackRate, position: audio.currentTime }); 574 } 575 } catch (e) {} 576 }); 577 } 578 } 579 580 /* ---------------------------------------------------------------------- 581 * Multi-Tab-Schutz: nur ein Tab spielt 582 * ------------------------------------------------------------------- */ 583 var tabId = statsGuid; 584 var channel = null; 585 try { channel = ('BroadcastChannel' in window) ? new BroadcastChannel('mom-player') : null; } catch (e) {} 586 587 function broadcastPlaying() { 588 if (channel) { try { channel.postMessage({ type: 'playing', tab: tabId }); } catch (e) {} } 589 } 590 var otherTabPlaying = false; 591 if (channel) { 592 channel.onmessage = function (msg) { 593 var d = msg && msg.data; 594 if (!d || d.tab === tabId) return; 595 if (d.type === 'playing') { 596 otherTabPlaying = true; 597 if (!audio.paused) pause(); 598 } else if (d.type === 'anyone-playing') { 599 if (!audio.paused) { try { channel.postMessage({ type: 'playing-here', tab: tabId }); } catch (e) {} } 600 } else if (d.type === 'playing-here') { 601 otherTabPlaying = true; 602 } 603 }; 604 } 605 606 /* ---------------------------------------------------------------------- 607 * Auto-Resume nach Seitenwechsel 608 * ------------------------------------------------------------------- */ 609 function resumeFromState() { 610 if (!state.songId || state.queueIndex < 0 || !state.queue[state.queueIndex]) { 611 emit('idle'); 612 return; 613 } 614 var item = currentItem(); 615 updateMediaSessionMetadata(item); 616 emit('songchange', item); 617 emit('queuechange', queue.list()); 618 if (state.wasPlaying && (new Date().getTime() - state.updatedAt) < 60000) { 619 // Erst fragen, ob ein anderer Tab bereits spielt (sonst Ping-Pong-Pause) ⦠620 if (channel) { try { channel.postMessage({ type: 'anyone-playing', tab: tabId }); } catch (e) {} } 621 setTimeout(function () { 622 if (otherTabPlaying) { emit('paused-restore', item); return; } 623 // ⦠frisch & allein â weiterspielen (Autoplay-Policy kann blocken â resume-blocked) 624 mintToken(item.songId).then(function (token) { 625 setSource(token, state.position || 0); 626 tryPlay(); 627 }, function () { emit('error', { songId: item.songId }); }); 628 }, channel ? 200 : 0); 629 } else { 630 emit('paused-restore', item); 631 } 632 } 633 634 window.addEventListener('pagehide', function () { doPersist(); }); 635 636 /* ---------------------------------------------------------------------- 637 * public API 638 * ------------------------------------------------------------------- */ 639 window.MOM.audio = { 640 play: play, pause: pause, toggle: toggle, seek: seek, next: next, prev: prev, 641 queue: queue, 642 setAutoSimilar: setAutoSimilar, 643 setVolume: setVolume, setMuted: setMuted, volumeSupported: volumeSupported, 644 setDismissed: function (onoff) { state.dismissed = !!onoff; persist(true); }, 645 token: function (songId) { return mintToken(songId); }, 646 on: on, 647 element: function () { return audio; }, 648 state: function () { 649 return { 650 songId: state.songId, position: audio.currentTime || state.position, 651 paused: audio.paused, queue: queue.list(), queueIndex: state.queueIndex, 652 autoSimilar: state.autoSimilar, duration: currentDuration(), 653 volume: audio.volume, muted: audio.muted, 654 prevCount: (state.prevStack || []).length,
655 dismissed: state.dismissed 656 }; 657 }, 658 current: currentItem 659 }; 660 661 /* Kompat-Shim: Timestamps in Bewertungen linken auf MOMPlayer.seekTo('m','ss') */ 662 if (!window.MOMPlayer) { 663 window.MOMPlayer = { 664 seekTo: function (min, sec) { 665 var t = (parseInt(min, 10) || 0) * 60 + (parseInt(sec, 10) || 0); 666 window.MOM.audio.seek(t); 667 window.MOM.audio.play(); 668 }, 669 pause: function () { window.MOM.audio.pause(); } 670 }; 671 } 672 673 setupMediaSessionHandlers(); 674 $(function () { resumeFromState(); }); 675 676})(window, jQuery);
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.