Back to Drone Zone — deep field
SOURCE / PINNED RELEASE

Made of little things.

Drone Zone — deep field

Release
578f277a8be1
Author-recorded commit
a96742b428de…
License
LICENSE
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-9753e6cf3ec

Archive hash verified: f7f3bf8ff3482551. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/radio.ts
import { common, outbox, media, link, type Subscription, type MediaAction } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { STATION_NADDR, STATION_PAGE, STATION_ADDRESS, STATION_RELAYS, isStationEvent, parseStation, type Station } from './station.js';

const element=<T extends HTMLElement>(id:string)=>document.getElementById(id) as T;
export class Radio {
  private station:Station|null=null;
  private session:string|null=null;
  private subscriptions:Subscription[]=[];
  private busy=false;private dead=false;private generation=0;private playing=false;
  private play=element<HTMLButtonElement>('play');
  private status=element<HTMLOutputElement>('radio-status');
  private volume=element<HTMLInputElement>('volume');
  private capabilities:MediaAction[]=[];
  private watchdog=0;
  private events=new AbortController();
  constructor() {
    const options={signal:this.events.signal};
    this.play.addEventListener('click',()=>void this.toggle(),options);
    element('retry').addEventListener('click',()=>void this.resolve(),options);
    element('station-link').hidden=!runtimeHasDomain('link');
    element('station-link').addEventListener('click',()=>void this.openStation(),options);
    this.volume.addEventListener('input',()=>{
      if(!this.session||!this.capabilities.includes('volume'))return;
      try{media.sendCommand(this.session,'volume',Number(this.volume.value));}catch{this.message('Volume control unavailable');}
    },options);
    void this.resolve();
  }
  private message(text:string) {if(this.dead)return;this.status.textContent=text;this.status.title=text;}
  private async limited<T>(promise:Promise<T>,message:string):Promise<T> {
    let timeout=0;try{return await Promise.race([promise,new Promise<never>((_,reject)=>{timeout=window.setTimeout(()=>reject(new Error(message)),11000);})]);}finally{clearTimeout(timeout);}
  }
  async resolve() {
    if(this.busy||this.dead)return;
    this.busy=true;this.play.disabled=true;const current=++this.generation;let found=false;
    this.message('Finding the signal…');
    try {
      if(!runtimeHasDomain('outbox'))throw new Error('Station lookup unavailable in this host');
      if(runtimeHasDomain('common')) {
        // Decoding is an enhancement: the immutable, independently decoded coordinate is bundled.
        try{const decoded=await this.limited(common.decodeNip19(STATION_NADDR),'Address lookup timed out');if(decoded.ok&&(decoded.kind!==STATION_ADDRESS.kind||decoded.pubkey!==STATION_ADDRESS.pubkey||decoded.identifier!==STATION_ADDRESS.identifier))throw new Error('Unexpected station address');}catch(error){if(error instanceof Error&&error.message==='Unexpected station address')throw error;}
      }
      const result=await this.limited(outbox.query([{kinds:[STATION_ADDRESS.kind],authors:[STATION_ADDRESS.pubkey],'#d':[STATION_ADDRESS.identifier],limit:10}],{authors:[STATION_ADDRESS.pubkey],relays:STATION_RELAYS,timeoutMs:8000,limit:10}),'Station lookup timed out — retry');
      if(this.dead||current!==this.generation)return;
      const events=result.events.map(r=>r.event).filter(isStationEvent).sort((a,b)=>b.created_at-a.created_at||a.id.localeCompare(b.id));
      if(!events.length)throw new Error(result.error?'Station lookup failed — retry':'Signal not found — retry');
      this.station=parseStation(events[0]);found=true;element('station-name').textContent=this.station.name.toUpperCase();
      if(this.session)this.message(this.playing?'Playing · live transmission':'Paused');
      else if(runtimeHasDomain('media'))this.message('Signal found · connecting…');
      else this.message(runtimeHasDomain('link')?'Listen in Wavefunc ↗':'Audio unavailable in this host');
    } catch(error) {this.message(error instanceof Error?error.message:'Station lookup failed — retry');}
    finally {if(!this.dead&&current===this.generation){this.busy=false;this.play.disabled=!runtimeHasDomain('media')&&!runtimeHasDomain('link');this.updatePlayLabel();}}
    // A resolved station starts once. Refreshing an existing session preserves
    // the listener's choice to pause; unavailable media never opens a link itself.
    if(found&&!this.dead&&current===this.generation&&!this.session&&runtimeHasDomain('media'))await this.toggle();
  }
  private updatePlayLabel() {
    const action=!runtimeHasDomain('media')&&runtimeHasDomain('link')?'Listen to Drone Zone in Wavefunc':this.playing?'Pause Drone Zone':'Play Drone Zone';
    this.play.setAttribute('aria-label',action);this.play.title=action;
    element('play-symbol').setAttribute('d',this.playing?'M6 5H10V19H6ZM14 5H18V19H14Z':'M8 5L19 12L8 19Z');
    element('app').dataset.playing=String(this.playing);
  }
  private async openStation() {
    if(!runtimeHasDomain('link')){this.message('External links unavailable');return;}
    try{const result=await this.limited(link.open(STATION_PAGE),'Link request timed out');this.message(result.status==='opened'?'Opened in Wavefunc':'Station link was not opened');}catch{this.message('Could not open Wavefunc');}
  }
  private armWatchdog() {
    clearTimeout(this.watchdog);this.watchdog=window.setTimeout(()=>{this.busy=false;this.play.disabled=false;this.message('No playback response — retry or open ↗');},10000);
  }
  private async toggle() {
    if(this.busy||this.dead)return;
    if(!runtimeHasDomain('media')){await this.openStation();return;}
    if(!this.station){await this.resolve();return;}
    this.busy=true;this.play.disabled=true;
    try {
      if(this.session) {
        const action=this.playing?'pause':'play';
        if(this.capabilities.length&&!this.capabilities.includes(action))throw new Error(`${action==='pause'?'Pause':'Playback'} is unavailable`);
        this.message(action==='play'?'Connecting to stream…':'Pausing…');this.armWatchdog();media.sendCommand(this.session,action);return;
      }
      const create=media.createSession({owner:'shell',source:{url:this.station.url,mimeType:this.station.mimeType,nostr:{eventId:this.station.eventId,address:`${STATION_ADDRESS.kind}:${STATION_ADDRESS.pubkey}:${STATION_ADDRESS.identifier}`,relays:STATION_RELAYS}},metadata:{title:this.station.name,artist:'Live radio',mediaType:'audio'},live:true,autoplay:true});
      // A late create response must not leak a session after timeout or teardown.
      let accepted=true;
      create.then(result=>{if((!accepted||this.dead)&&result.sessionId)media.destroySession(result.sessionId);}).catch(()=>{});
      let result;try{result=await this.limited(create,'Audio host did not respond — open ↗');}catch(error){accepted=false;throw error;}
      if(this.dead)return;
      if(result.error||!result.sessionId)throw new Error(result.error||'Audio session unavailable');
      if(result.owner!=='shell'){media.destroySession(result.sessionId);throw new Error('This host cannot own radio playback');}
      this.session=result.sessionId;
      this.subscriptions.push(media.onState(this.session,state=>{
        if(this.dead)return;clearTimeout(this.watchdog);this.busy=false;this.play.disabled=false;
        this.playing=state.status==='playing';this.updatePlayLabel();
        if(typeof state.volume==='number')this.volume.value=String(state.volume);
        const bitrate=this.station?.bitrate?` · ${Math.round(this.station.bitrate/1000)} kbps`:'';
        this.message(state.status==='playing'?`Live transmission${bitrate}`:state.status==='buffering'?'Buffering signal…':state.status==='paused'?'Paused · the field continues':'Transmission stopped · retry');
      }));
      this.subscriptions.push(media.onCapabilities(this.session,actions=>{this.capabilities=actions;this.volume.disabled=!actions.includes('volume');if(actions.includes('volume')&&this.session)media.sendCommand(this.session,'volume',Number(this.volume.value));}));
      // The host starts playback after attaching the session. Sending a second
      // play command here can cancel its pending browser-activation prompt.
      this.message('Connecting to stream…');this.armWatchdog();
    } catch(error) {
      this.message(error instanceof Error?error.message:'Playback failed — retry or open ↗');this.busy=false;this.play.disabled=false;
      if(this.session&&!this.playing){this.subscriptions.forEach(s=>s.close());this.subscriptions=[];try{media.destroySession(this.session);}catch{}this.session=null;}
    }
  }
  destroy() {this.dead=true;++this.generation;clearTimeout(this.watchdog);this.events.abort();this.subscriptions.forEach(s=>s.close());if(this.session){try{media.destroySession(this.session);}catch{}}}
}