1'use strict'; 2var player=false; 3var player_window=false; 4var pDataTransfer="OPENER"; 5var killswitch=false; 6var player_state=""; 7var wurl="20210406"; 8// Set by PHP (inc.audioplayer / index): live = https://media.holysoft.de/, local = same origin or WEBSTREAM_BASE_LOCAL 9var webstream_base = (typeof window.HolyShopWebstreamBase !== 'undefined' ? window.HolyShopWebstreamBase : 'https://media.holysoft.de/'); 10 11function isSafariMacOS() { 12 const ua = navigator.userAgent; 13 14 const isSafari = ua.includes('Safari') && 15 !ua.includes('Chrome') && 16 !ua.includes('Chromium') && 17 !ua.includes('CriOS') && 18 !ua.includes('Edg') && 19 !ua.includes('OPR'); 20 21 const isMac = ua.includes('Macintosh'); 22 23 return isSafari && isMac; 24} 25 26$.when($.ready).then( ()=> { 27 var resumedata=false; 28 if(resumedata=readCookieData('holyplayer')) { 29 if(resumedata.pid>0) { 30 31 } 32 else { 33 resumedata=false; 34 Cookies.remove('holyplayer'); 35 } 36 } 37 if(typeof show_inline_player !== 'undefined' && show_inline_player) { 38 $('.hsp_play_button').on('click keydown', (e)=> { 39 if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; 40 if (e.type === 'keydown') e.preventDefault(); 41 playStream(e.currentTarget); 42 }); 43 if(resumedata) { 44 playStream(false,resumedata); 45 } 46 } 47 else { 48 $('.hsp_play_button').on('click keydown', (e)=> { 49 if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; 50 if (e.type === 'keydown') e.preventDefault(); 51 var element=e.currentTarget; 52 var pData=createPData(element); 53 pDataTransfer=pData; 54 openPlayer(); 55 }); 56 if(window.opener && window.opener.pDataTransfer!==false) { 57 var pData=window.opener.pDataTransfer; 58 window.opener.pDataTransfer=false; 59 playStream(pData,resumedata); 60 } 61 else if(document.body.id=="windowplayer" && resumedata) { 62 playStream(false,resumedata); 63 } 64 } 65}); 66 67function createPData(element,resumedata=false) { 68 var pData={}; 69 var pDataResume={}; 70 if(element!==false) { 71 var pData={ 72 title:$(element).data('title'), 73 serie:$(element).data('serie'), 74 episode:$(element).data('episode'), 75 cover:$(element).data('cover'), 76 prod_id:$(element).data('fileid'), 77 preview:$(element).data('preview'), 78 start_playing:true, 79 progress:false, 80 targetTime:-1, 81 curTime:0, 82 lastTime:0, 83 } 84 if(pData.preview==1) { 85 pData.title+=' Hörprobe'; 86 } 87 pData= { 88 ...pData, 89 full_title:pData.serie+' '+pData.episode+' '+pData.title, 90 } 91 } 92 if(resumedata!==false && isObjectHS(resumedata)) { 93 var pDataResume={ 94 title:resumedata.t, 95 serie:resumedata.s, 96 episode:resumedata.e, 97 cover:resumedata.c, 98 start_playing:resumedata.pl, 99 prod_id:resumedata.pid, 100 preview:resumedata.prvw, 101 curTime:0, 102 targetTime:resumedata.ct, 103 lastTime:0, 104 } 105 pDataResume= { 106 ...pDataResume, 107 full_title:pDataResume.serie+' '+pDataResume.episode+' '+pDataResume.title, 108 } 109 pData= { 110 ...pData, 111 full_title:pData.serie+' '+pData.episode+' '+pData.title, 112 } 113 if(element===false || pData.prod_id<1 || pDataResume.prod_id==pData.prod_id) { 114 pData=pDataResume; 115 } 116 } 117 pData= { 118 ...pData, 119 saveTimeInterval:1, 120 } 121 return pData; 122} 123 124function unregisterPlayer(player,clear_cookie=false,hideactions=true,wait_for_close=false) { 125 var player=player; 126 var grace_timer=800; 127 return new Promise(async (resolve,reject)=> { 128 if(player) { 129 try { 130 player.stop(); 131 } catch (e) { 132 // ignore if already stopped/destroyed 133 } 134 if(hideactions) { 135 $('#topPlayer').hide(); 136 } 137 try { 138 await player.destroy(); 139 } catch (e) { 140 // ignore if already destroyed 141 } 142 if(clear_cookie) { 143 Cookies.remove('holyplayer'); 144 } 145 player_state="destroyed"; 146 player=false; 147 } 148 if(wait_for_close) { 149 setTimeout(function(){ 150 resolve(true); 151 }, grace_timer); 152 } 153 else { 154 resolve(true); 155 } 156 }); 157} 158 159const getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null); 160const isNullOrUndefined = (input) => input === null || typeof input === 'undefined'; 161const isString = (input) => getConstructor(input) === String; 162const isArray = (input) => Array.isArray(input); 163const isNodeList = (input) => input instanceof NodeList; 164const isObject = (input) => getConstructor(input) === Object; 165 166function urlexists(url, cb) { 167 $.ajax({ 168 url: url, 169 dataType: 'text', 170 type: 'GET', 171 xhrFields: { 172 withCredentials: true 173 }, 174 timeout: 3000, 175 complete: function (xhr) { 176 if (typeof cb === 'function') 177 cb.apply(this, [xhr.status]); 178 }, 179 error: function (xhr) { 180 cb.apply(this, [408]); 181 } 182 }); 183} 184 185function isEmpty(input) { 186 return isNullOrUndefined(input) || 187 ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) || 188 (isObject(input) && !Object.keys(input).length); 189} 190function debounce(func, wait, immediate) { 191 var timeout; 192 return function() { 193 var context = this, 194 args = arguments; 195 var callNow = immediate && !timeout; 196 clearTimeout(timeout); 197 timeout = setTimeout(function() { 198 timeout = null; 199 if (!immediate) {
200 func.apply(context, args); 201 } 202 }, wait); 203 if (callNow) func.apply(context, args); 204 } 205} 206function throttle(func, timeFrame) { 207 var lastTime = new Date(); 208 return function () { 209 var context = this, args = arguments; 210 var now = new Date(); 211 if (now - lastTime >= timeFrame) { 212 func.apply(context, args); 213 lastTime = now; 214 } 215 }; 216} 217 218function plyrSeekAction(target,player) { 219 // Seek Code 220 const seek = target; 221 // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) 222 let seekTo = seek.getAttribute('seek-value'); 223 if (isEmpty(seekTo)) { 224 seekTo = seek.value; 225 } 226 seek.removeAttribute('seek-value'); 227 player.currentTime = (seekTo / seek.max) * player.duration; 228} 229 230var debouncedPlyrSeekAction = debounce(function (e,player) { 231 plyrSeekAction(e,player,true); 232},250); 233 234function readCookieData(cookieName) { 235 236 let data=Cookies.get(cookieName); 237 if(data && data.length>0) { 238 return JSON.parse(data); 239 } 240 return false; 241} 242function writeCookieData(cookieName,data) { 243 let expireIn5Minutes=1/24/12; 244 let json_data=JSON.stringify(data); 245 Cookies.set(cookieName,json_data,{expires:expireIn5Minutes,sameSite: 'Strict'}); 246 return true; 247} 248function isObjectHS(objValue,test_constructor=true) { 249 if(test_constructor) { 250 return objValue && typeof objValue === 'object' && objValue.constructor === Object; 251 } 252 else { 253 return objValue && typeof objValue === 'object'; 254 } 255} 256 257 258function openPlayer() { 259 let width = 376; 260 let height = 392; 261 if(isSafariMacOS()) { 262 const chromeHeight = 28; 263 const chromeWidth = 20; 264 width += chromeWidth; 265 height += chromeHeight; 266 } 267 268 var player_window=window.open(base+"audioplayer/", "holyshopplayer", "toolbar=no,scrollbars=no,resizable=no,top=200,left=200,width="+width+",height="+height); 269} 270 271function initPlayer(pData) { 272 unregisterPlayer(player,true,true); 273 274 var initTime = Date.now(); 275 276 // BFSG/WCAG 2.1 AA Compliance: Announce player initialization to screen readers 277 announceToScreenReader('Player wird initialisiert...'); 278 279 player = new Plyr('#hspplayer', { 280 debug: false, 281 controls: [ 282 // 'rewind', // Rewind by the seek time (default 10 seconds) 283 'play', // Play/pause playback 284 // 'fast-forward', // Fast forward by the seek time (default 10 seconds) 285 'progress', // The progress bar and scrubber for playback and buffering 286 'current-time', // The current time of playback 287 'duration', // The full duration of the media 288 'mute', // Toggle mute 289 'volume', // Volume control 290 ], 291 listeners: { 292 seek: function (e) { 293 e.preventDefault() // required on v2 294 295 var target=e.currentTarget; 296 297 debouncedPlyrSeekAction(target,player); 298 299 return false // required on v3 300 } 301 }, 302 }); 303 player_state="init"; 304 305 player.source={ 306 type: 'audio', 307 title: pData.full_title, 308 sources: [ 309 { 310 src: webstream_base+'webstream.php?1='+wurl+'&2='+pData.prod_id+'&3='+pData.preview, 311 type: 'audio/mp4', 312 config: { 313 credentials: 'include' 314 } 315 }, 316 ], 317 }; 318 319 // BFSG/WCAG 2.1 AA Compliance: Enhanced player event handling with accessibility 320 player.on('loadedmetadata', event => { 321 if(pData.targetTime>-1) { 322 event.detail.plyr.currentTime=pData.targetTime; 323 } 324 // Announce track loaded to screen readers 325 announceToScreenReader('Track geladen: ' + pData.full_title); 326 }); 327 328 player.on('ready', event => { 329 player_state="ready"; 330 $('#topPlayer').show(); 331 event.detail.plyr.pData=pData; 332 $('#plyr_close').off('click keydown'); 333 $('#plyr_close').on('click keydown', async (e)=> { 334 if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; 335 if (e.type === 'keydown') e.preventDefault(); 336 announceToScreenReader('Player wird beendet'); 337 await unregisterPlayer(event.detail.plyr,true,true); 338 window.close(); 339 }); 340 if(pData.start_playing) { 341 event.detail.plyr.play().then( () => { 342 announceToScreenReader('Wiedergabe gestartet'); 343 }, (e) => { 344 // Autoplay blocked (popup not user-activated): stay paused, no alarming message 345 if (e && e.name === 'NotAllowedError') { 346 console.info('Autoplay nicht erlaubt â Benutzer kann Play drücken.'); 347 return; 348 } 349 console.info(e); 350 announceToScreenReader('Wiedergabe konnte nicht starten. Bitte überprüfen Sie die Browser-Einstellungen für automatische Wiedergabe.'); 351 }); 352 } 353 // Announce player ready to screen readers 354 // announceToScreenReader('Player bereit. ' + pData.full_title); 355 }); 356 357 player.on('playing', event => { 358 if(window.opener) { 359 let pDataL=pData; 360 document.title="âº"+" "+pDataL.full_title+" - HolyShop Hörspiel Player"; 361 } 362 }); 363 364 player.on('pause', event => { 365 if(window.opener) { 366 let pDataL=pData; 367 document.title="ââ"+" "+pDataL.full_title+" - HolyShop Hörspiel Player"; 368 } 369 // Announce pause state to screen readers 370 announceToScreenReader('Pausiert: ' + pData.full_title); 371 }); 372
373 // BFSG/WCAG 2.1 AA Compliance: Error handling with screen reader announcements 374 player.on('error', event => { 375 const plyrInstance = event.detail?.plyr ?? player; 376 const media = plyrInstance?.media ?? plyrInstance?.elements?.media; 377 const src = (media && media.currentSrc) ? media.currentSrc : ''; 378 const errMsg = (media && media.error && media.error.message) ? media.error.message : ''; 379 // Ignore Plyr internal blank.mp4 (decode often fails in audio-only context); harmless 380 if (/blank\.mp4/i.test(src) || /blank\.mp4/i.test(errMsg)) { 381 return; 382 } 383 // Ignore errors in first 3s (autoplay rejection, Plyr internal blank.mp4 from other element) 384 if (typeof initTime === 'number' && (Date.now() - initTime) < 3000) { 385 return; 386 } 387 if (player_state === 'ready') { 388 // When message is blank (e.g. privacy.resistFingerprinting or 403/5xx), show generic message 389 const displayMsg = (errMsg && errMsg.length > 0) ? errMsg : 'Netzwerk- oder Serverfehler. Bitte erneut versuchen.'; 390 console.error('Player error:', displayMsg, event); 391 announceToScreenReader('Fehler beim Laden des Audio-Tracks. ' + (errMsg && errMsg.length > 0 ? '' : 'Bitte versuchen Sie es erneut.')); 392 } else { 393 console.info('Player error (before ready):', event.detail?.plyr?.media?.error?.message || event); 394 } 395 }); 396 397 // BFSG/WCAG 2.1 AA Compliance: Loading state announcements 398 player.on('loadstart', event => { 399 announceToScreenReader('Lade Audio-Track...'); 400 }); 401 402 player.on('canplay', event => { 403 announceToScreenReader('Audio-Track bereit zur Wiedergabe'); 404 }); 405 406 // BFSG/WCAG 2.1 AA Compliance: Volume change announcements 407 player.on('volumechange', event => { 408 const volume = Math.round(event.detail.plyr.volume * 100); 409 const muted = event.detail.plyr.muted; 410 if (muted) { 411 announceToScreenReader('Audio stummgeschaltet'); 412 } else { 413 announceToScreenReader('Lautstärke: ' + volume + ' Prozent'); 414 } 415 }); 416 417 window.addEventListener('unload', () => { 418 if(player_state=="destroyed") { 419 return; 420 } 421 // store Playback for resume after page change (guard: pData only set after 'ready') 422 if (!player || !player.pData) { 423 return; 424 } 425 let pData=player.pData; 426 if(player.currentTime) { 427 let curTime=player.currentTime || 0; 428 var cData={ 429 pid:pData.prod_id, 430 prvw:pData.preview, 431 ct:player.currentTime, 432 pl:player.playing, 433 t:pData.title, 434 s:pData.serie, 435 e:pData.episode, 436 c:pData.cover, 437 }; 438 writeCookieData('holyplayer',cData); 439 unregisterPlayer(player,false,false); 440 } 441 else { 442 console.error('could not save playback, player_state:'+player_state); 443 } 444 if(this) this.removeEventListener('unload',arguments.callee); 445 }); 446} 447 448// BFSG/WCAG 2.1 AA Compliance: Screen reader announcement function 449function announceToScreenReader(message) { 450 // Create or get existing live region 451 let liveRegion = document.getElementById('player-status'); 452 if (!liveRegion) { 453 liveRegion = document.createElement('div'); 454 liveRegion.id = 'player-status'; 455 liveRegion.className = 'sr-only'; 456 liveRegion.setAttribute('aria-live', 'polite'); 457 liveRegion.setAttribute('aria-atomic', 'true'); 458 document.body.appendChild(liveRegion); 459 } 460 461 // Update the live region with the message 462 liveRegion.textContent = message; 463 464 // Clear the message after a delay to allow for multiple announcements 465 setTimeout(() => { 466 liveRegion.textContent = ''; 467 }, 3000); 468} 469 470async function playStream(element,resumedata=false) { 471 if(element===false) { 472 var pData=createPData(element,resumedata); 473 } 474 else { 475 var pData=element; 476 } 477 478 await unregisterPlayer(player,false,true,true); 479 480 // BFSG/WCAG 2.1 AA Compliance: Announce stream loading to screen readers 481 announceToScreenReader('Lade Hörspiel: ' + pData.full_title); 482 483 urlexists(webstream_base+'webstream.php?0=0&1='+wurl+'&2='+pData.prod_id+'&3='+pData.preview, function (status) { 484 if (status === 200) { 485 // Execute code if successful 486 if(window.opener) { 487 document.title=pData.full_title; 488 } 489 $('#tp_serie').html(pData.serie); 490 $('#tp_episode').html(pData.episode+':'); 491 $('#tp_title').html(pData.title); 492 $('#tp_cover').html('<img src="'+pData.cover+'" alt="Cover '+pData.full_title+'">'); 493
494 // BFSG/WCAG 2.1 AA Compliance: Announce successful load to screen readers 495 announceToScreenReader('Hörspiel erfolgreich geladen: ' + pData.full_title); 496 497 initPlayer(pData); 498 } 499 else { 500 // BFSG/WCAG 2.1 AA Compliance: Announce error to screen readers 501 announceToScreenReader('Fehler beim Laden des Hörspiels. Bitte versuchen Sie es erneut.'); 502 console.error('Stream loading failed with status:', status); 503 return; 504 } 505 }); 506}
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.