import type {Episode,Segment} from './types'; import {BroadcastSound} from './sound'; import {caption,activeCue} from './editorial'; import {Observatory} from './stage'; export class Broadcast{ episode?:Episode;index=0;time=0;playing=false;muted=false;silent=false;audio=new Audio();sound=new BroadcastSound(this.audio);private start=0;private raf=0;onChange:()=>void=()=>{};onError:(message:string)=>void=()=>{};caption='';onFrame:()=>void=()=>{}; constructor(public stage:Observatory){this.audio.crossOrigin='anonymous';this.audio.addEventListener('ended',()=>this.next());this.audio.addEventListener('error',()=>{this.pause();this.onError('A saved audio line could not load. The show is paused.');});this.tick();} get segment(){return this.episode?.segments[this.index];} get elapsed(){return (this.episode?.segments.slice(0,this.index).reduce((n,s)=>n+s.duration,0)??0)+this.time;} get duration(){return this.episode?.segments.reduce((n,s)=>n+s.duration,0)??0;} async load(ep:Episode,silent=false){this.stop();if(ep.sceneVersion!==this.stage.manifest?.version)await this.stage.load(ep.sceneVersion);this.episode=structuredClone(ep);this.index=0;this.time=0;this.silent=silent;this.caption='';this.stage.setSegment(this.segment);this.onChange();} async play(){if(this.episode&&this.index===this.episode.segments.length-1&&this.time>=(this.segment?.duration??0)-.05){await this.jump(0);}const s=this.segment;if(!s)return;if(!this.silent&&!s.audio?.url){this.onError('Prepare Walter’s saved voice before playback, or choose a silent rehearsal.');return;} try{if(!this.silent){await this.sound.start(this.episode?.sceneVersion?.startsWith('2.')??false);const u=new URL(s.audio!.url,location.origin).href;if(this.audio.src!==u){this.audio.src=u;this.audio.currentTime=this.time;}this.audio.muted=this.muted;await this.audio.play();}this.playing=true;this.start=performance.now()-this.time*1000;this.onChange();}catch{this.onError('Press Play again to allow audio in this browser.');}} pause(){this.sound.pause();this.playing=false;this.audio.pause();this.stage.setSegment(this.segment,this.time,false);this.onChange();} stop(){this.pause();this.audio.removeAttribute('src');this.audio.load();this.time=0;this.index=0;this.caption='';this.stage.setSegment(undefined);} async jump(index:number){const resume=this.playing;this.pause();this.index=Math.max(0,Math.min(index,(this.episode?.segments.length??1)-1));this.time=0;this.audio.removeAttribute('src');this.stage.setSegment(this.segment,0,false);this.onChange();if(resume)await this.play();} async next(){if(!this.episode)return;if(this.index>=this.episode.segments.length-1){this.pause();this.time=this.segment?.duration??0;this.onChange();return;}const resume=this.playing;await this.jump(this.index+1);if(resume&&!this.playing)await this.play();} async previous(){await this.jump(Math.max(0,this.index-1));} setMuted(m:boolean){this.muted=m;this.audio.muted=m;this.sound.mute(m);this.onChange();} private tick=()=>{this.raf=requestAnimationFrame(this.tick);if(this.playing&&this.segment){this.time=this.silent?(performance.now()-this.start)/1000:this.audio.currentTime;this.caption=caption(this.segment,this.time);const cue=activeCue(this.segment,this.time);if(cue&&!this.silent)this.sound.cue(this.segment.id+':'+cue.id);this.stage.setSegment(this.segment,this.time,true);this.onFrame();if(this.silent&&this.time>=this.segment.duration)this.next();}}; dispose(){cancelAnimationFrame(this.raf);this.stop();this.sound.dispose();} }