/* One scene owner. Handoffs happen only at a real ended event and a ready first frame. */ (function(){ 'use strict'; const stage=document.querySelector('#stage'); if(!stage||document.documentElement.classList.contains('content-reader'))return; const slots={idle:null,thinking:null,broadcast:null},entries=new Set(); let current=null,target=null,pageActive=true,scheduled=false; const visible=()=>pageActive&&!document.hidden; const wanted=()=>[slots.broadcast,slots.thinking,slots.idle].find(d=>d&&!d.failed&&!d.blocked)||null; function schedule(){if(!scheduled){scheduled=true;queueMicrotask(()=>{scheduled=false;sync();});}} function clearTimer(d){clearTimeout(d?.timer);if(d)d.timer=0;} function arm(d){clearTimer(d);if(visible())d.timer=setTimeout(()=>fail(d,new Error('Avatar media stalled')),12000);} function retained(d){return d===current||Object.values(slots).includes(d);} function collect(d){ if(!d||retained(d)||(!d.retired&&d.keep))return; clearTimer(d);d.disposed=true;entries.delete(d); d.video.pause();for(const [name,fn] of d.listeners)d.video.removeEventListener(name,fn); d.dispose?.(); } function fail(d,error){ if(d.disposed||d.failed)return; d.failed=true;clearTimer(d);d.video.pause();d.onError?.(error); if(current===d){current=null;d.onLeave?.();} schedule(); } function play(d){ if(!d||d.disposed||d.failed||d.blocked||d.playingPromise||!visible())return; if(d===current&&d===wanted()&&d.userPaused)return; if(!d.video.paused&&!d.video.ended)return; d.playingPromise=true; Promise.resolve(d.video.play()).then(()=>{ if(current!==d||!visible()||(d.userPaused&&wanted()===d))d.video.pause(); }).catch(error=>{ if(d.disposed||current!==d||!visible()||error.name==='AbortError')return; if(error.name==='NotAllowedError'){d.blocked=true;clearTimer(d);if(current===d){current=null;d.preparing=false;d.onLeave?.();}d.onBlocked?.();schedule();}else fail(d,error); }).finally(()=>{d.playingPromise=false;}); } function prepare(d){ if(!d||d===current||d.preparing||d.failed)return; d.preparing=true;d.ready=false;d.video.preload='auto';d.video.autoplay=false;d.video.loop=false; d.prepare?.(); if(d.video.readyState>=1&&d.video.currentTime!==0){try{d.video.currentTime=0;}catch{}} arm(d);ready(d); } function ready(d){ if(d.disposed||d===current||!d.preparing||d.failed)return; if(d.video.readyState>=1&&d.video.currentTime!==0){if(!d.video.seeking)try{d.video.currentTime=0;}catch{}return;} if(d.video.readyState>=2&&!d.video.seeking){d.ready=true;clearTimer(d);schedule();} } function notify(){window.dispatchEvent(new CustomEvent('zhiwei-avatar-scene',{detail:{state:current?.slot||'idle',pending:target?.slot||null}}));} function enter(d){ if(!d||d!==wanted()||!d.ready||!visible())return; const previous=current; if(previous){previous.video.pause();clearTimer(previous);previous.onLeave?.();previous.ready=false;previous.preparing=false; if(document.fullscreenElement===previous.video)document.exitFullscreen?.().catch(()=>{}); if(previous.video.webkitDisplayingFullscreen)previous.video.webkitExitFullscreen?.(); } current=d;target=null;d.preparing=false;d.ready=false;d.atEnd=false;d.blocked=false; d.video.muted=d.muted;d.onEnter?.();notify();play(d);collect(previous); } function ended(d){ if(d!==current||d.disposed)return; d.atEnd=true;clearTimer(d); if(wanted()!==d){schedule();return;} if(d.onEnded)d.onEnded(); else{d.atEnd=false;try{d.video.currentTime=0;}catch{}play(d);} schedule(); } function sync(){ const next=wanted();target=next!==current?next:null; for(const d of entries)if(d!==current&&d!==target){clearTimer(d);collect(d);} if(!visible()){ if(current)current.video.pause(); for(const d of entries)clearTimer(d); return; } if(target){ prepare(target); // Retain the outgoing frame until the incoming first frame is decoded. if(!current||current.failed||current.atEnd||current.video.ended){if(target.ready)enter(target);else arm(target);} else{ // A stopped/replaced narration finishes its visual motion silently. if(current.slot==='broadcast')current.video.muted=true; current.video.loop=false;play(current); } }else if(current){ current.video.muted=current.muted; if(current.atEnd||current.video.ended){ if(!current.onEnded){current.atEnd=false;current.video.currentTime=0;play(current);} }else play(current); } } function create(options){ const d={...options,slot:options.slot,muted:!!options.muted,keep:!!options.keep,ready:false,preparing:false,atEnd:false,failed:false,blocked:false,userPaused:false,listeners:[]}; const v=d.video;v.loop=false;v.autoplay=false;v.muted=d.muted; function on(name,fn){v.addEventListener(name,fn);d.listeners.push([name,fn]);} for(const event of ['loadedmetadata','loadeddata','canplay','seeked'])on(event,()=>ready(d)); on('ended',()=>ended(d)); on('pause',()=>{if(current===d&&wanted()===d&&visible()&&v.controls&&!v.ended&&!d.failed)d.userPaused=true;}); on('play',()=>{if(current===d&&wanted()===d&&visible()&&v.controls)d.userPaused=false;}); on('error',()=>fail(d,v.error||new Error('Avatar media unavailable'))); on('playing',()=>{ if(current!==d||!visible()||(d.userPaused&&wanted()===d)){v.pause();return;} clearTimer(d);d.onPlaying?.(); }); on('timeupdate',()=>{if(current===d)clearTimer(d);}); for(const event of ['waiting','stalled'])on(event,()=>{if(current===d&&!d.userPaused)arm(d);}); entries.add(d);return d; } function set(slot,d){ const previous=slots[slot];slots[slot]=d; if(d){d.slot=slot;d.userPaused=false;} if(previous!==d)collect(previous); schedule(); } function pause(d){if(!d)return;d.userPaused=true;clearTimer(d);if(d===current)d.video.pause();} function resume(d){if(!d||d.disposed)return;d.userPaused=false;d.blocked=false;if(d===current&&(d.atEnd||d.video.ended)){d.atEnd=false;d.video.currentTime=0;}if(d.failed){d.failed=false;d.video.load();d.preparing=false;}set(d.slot,d);} function retire(d){if(!d)return;d.retired=true;if(slots[d.slot]===d)slots[d.slot]=null;collect(d);schedule();} function recover(){ if(!visible())return; let changed=false; for(const d of entries)if(d.blocked){d.blocked=false;changed=true;} if(changed)schedule(); } document.addEventListener('visibilitychange',schedule); window.addEventListener('pagehide',()=>{pageActive=false;sync();}); window.addEventListener('pageshow',()=>{pageActive=true;recover();schedule();}); document.addEventListener('pointerdown',recover,{passive:true}); document.addEventListener('keydown',recover); window.addEventListener('online',recover); window.AvatarScene={create,set,pause,resume,retire,recover,getCurrent:()=>current,getPending:()=>target}; })();