import * as config from "./config" ; import * as log from "./log" ; import { prot } from "./protocol" ; import * as util from "./util" ; export let pingSock = null ; export let dataSock = null ; export let masterSock = null ; export let connected = false ; export let transmitting = false ; export function setTransmitting ( to ) { transmitting = to ; } export let selfId = 0 ; export let recName = null ; export let mode = prot . mode . init ; let flacInfoBuf = null ; export const iceServers = [ { urls : "stun:stun.l.google.com:19302" } ] ; export let remoteBeginTime = null ; let flushTimeout = null ; class ReconnectableWebSocket { sock : WebSocket ; url : string ; connecter : ( arg0 : WebSocket ) => Promise < unknown > ; closeHandler : ( arg0 : CloseEvent ) => unknown ; promise : Promise < unknown > ; keepaliveTimeout : null | number ; constructor ( url , closeHandler , connecter ) { this . url = url ; this . closeHandler = closeHandler ; this . connecter = connecter ; this . promise = Promise . all ( [ ] ) ; this . keepaliveTimeout = null ; } connect ( ) { let sock ; let connectTimeout = null ; this . promise = this . promise . then ( ( ) => { sock = this . sock = new WebSocket ( this . url ) ; sock . binaryType = "arraybuffer" ; sock . onerror = this . closeHandler ; sock . onclose = this . closeHandler ; return new Promise ( ( res , rej ) => { sock . onopen = ( ) => { this . connecter ( sock ) . then ( res ) . catch ( rej ) ; } ; connectTimeout = setTimeout ( rej , 10000 ) ; } ) ; } ) . then ( ( ) => { clearTimeout ( connectTimeout ) ; setTimeout ( ( ) => { if ( sock !== this . sock ) return ; sock . onclose = ( ) => { this . connect ( ) . catch ( this . closeHandler ) ; } ; } , 1000 ) ; } ) ; return this . promise ; } send ( data ) { this . promise = this . promise . then ( ( ) => { this . sock . send ( data ) ; } ) ; return this . promise ; } close ( ) { this . promise = this . promise . then ( ( ) => { this . sock . onclose = this . closeHandler ; this . sock . close ( ) ; } ) ; return this . promise ; } keepalive ( duration ? ) { duration = duration || 30000 ; if ( this . keepaliveTimeout !== null ) clearTimeout ( this . keepaliveTimeout ) ; this . keepaliveTimeout = setTimeout ( ( ) => { this . promise = this . promise . then ( ( ) => { this . sock . onclose = function ( ) { } ; this . sock . close ( ) ; this . connect ( ) . catch ( this . closeHandler ) ; } ) ; } , duration ) ; } } interface AdminAccess { audio : boolean ; video : boolean ; } export const adminAccess = { } ; export function connect ( ) { const p = prot . parts . login ; const f = prot . flags ; const nickBuf = util . encodeText ( config . username ) ; const connMsg = new DataView ( new ArrayBuffer ( p . length + nickBuf . length ) ) ; connMsg . setUint32 ( 0 , prot . ids . login , true ) ; const flags = ( config . useFlac ? f . dataType . flac : 0 ) | ( config . useContinuous ? f . features . continuous : 0 ) ; connMsg . setUint32 ( p . id , config . config . id , true ) ; connMsg . setUint32 ( p . key , config . config . key , true ) ; new Uint8Array ( connMsg . buffer ) . set ( nickBuf , p . length ) ; return Promise . all ( [ ] ) . then ( ( ) => { connected = true ; log . pushStatus ( "conn" , "Connecting..." ) ; pingSock = new ReconnectableWebSocket ( config . wsUrl ( ) , config . disconnect , connecter ) ; const out = new DataView ( connMsg . buffer . slice ( 0 ) ) ; out . setUint32 ( p . flags , f . connectionType . ping | flags , true ) ; function connecter ( sock ) { sock . send ( out . buffer ) ; sock . addEventListener ( "message" , pingSockMsg ) ; return Promise . all ( [ ] ) ; } return pingSock . connect ( ) ; } ) . then ( ( ) => { dataSock = new ReconnectableWebSocket ( config . wsUrl ( ) , config . disconnect , connecter ) ; const out = new DataView ( connMsg . buffer . slice ( 0 ) ) ; out . setUint32 ( p . flags , f . connectionType . data | flags , true ) ; function connecter ( sock ) { sock . send ( out . buffer ) ; if ( flacInfoBuf ) sock . send ( flacInfoBuf ) ; sock . addEventListener ( "message" , dataSockMsg ) ; return Promise . all ( [ ] ) ; } return dataSock . connect ( ) ; } ) . then ( function ( ) { let out ; if ( "master" in config . config ) { masterSock = new ReconnectableWebSocket ( config . wsUrl ( ) , config . disconnect , connecter ) ; out = new DataView ( connMsg . buffer . slice ( 0 ) ) ; out . setUint32 ( p . key , config . config . master , true ) ; out . setUint32 ( p . flags , f . connectionType . master | flags , true ) ; return masterSock . connect ( ) ; } function connecter ( sock ) { sock . send ( out . buffer ) ; sock . addEventListener ( "message" , masterSockMsg ) ; return Promise . all ( [ ] ) ; } } ) ; } export function ping ( ) { const p = prot . parts . ping ; const msg = new DataView ( new ArrayBuffer ( p . length ) ) ; msg . setUint32 ( 0 , prot . ids . ping , true ) ; msg . setFloat64 ( p . clientTime , performance . now ( ) , true ) ; pingSock . send ( msg ) ; } function pingSockMsg ( ev ) { const msg = new DataView ( ev . data ) ; const cmd = msg . getUint32 ( 0 , true ) ; pingSock . keepalive ( ) ; switch ( cmd ) { case prot . ids . ack : { const ackd = msg . getUint32 ( prot . parts . ack . ackd , true ) ; if ( ackd === prot . ids . login ) { ping ( ) ; } break ; } default : util . dispatchEvent ( "net.pingSock." + cmd , msg ) ; } } function dataSockMsg ( ev ) { const msg = new DataView ( ev . data ) ; const cmd = msg . getUint32 ( 0 , true ) ; dataSock . keepalive ( ) ; switch ( cmd ) { case prot . ids . nack : { const p = prot . parts . nack ; const text = util . decodeText ( msg . buffer . slice ( p . msg ) ) ; alert ( text ) ; log . pushStatus ( "nack" , text ) ; break ; } case prot . ids . info : { const p = prot . parts . info ; const key = msg . getUint32 ( p . key , true ) ; let val = 0 ; if ( msg . byteLength >= p . length ) val = msg . getUint32 ( p . value , true ) ; switch ( key ) { case prot . info . id : selfId = val ; break ; case prot . info . mode : { mode = val ; const wvms = ( ( val === prot . mode . rec ) ? "r" : "s" ) + ( config . useContinuous ? "c" : "v" ) ; config . setWaveVADColors ( wvms ) ; log . popStatus ( "mode" ) ; if ( mode < prot . mode . rec ) log . pushStatus ( "mode" , "Not yet recording" ) ; else if ( mode === prot . mode . paused ) log . pushStatus ( "mode" , "Recording paused" ) ; else if ( mode > prot . mode . rec ) log . pushStatus ( "mode" , "Not recording" ) ; if ( mode === prot . mode . buffering ) { flushBuffers ( ) ; } else if ( flushTimeout ) { clearTimeout ( flushTimeout ) ; flushTimeout = null ; } break ; } case prot . info . startTime : remoteBeginTime = msg . getFloat64 ( p . value , true ) ; break ; case prot . info . recName : recName = util . decodeText ( msg . buffer . slice ( p . value ) ) ; document . title = recName + " — Ennuicastr" ; break ; case prot . info . ice : { const iceServer = JSON . parse ( util . decodeText ( msg . buffer . slice ( p . value ) ) ) ; iceServers . push ( iceServer ) ; break ; } } util . dispatchEvent ( "net.info." + key , { val : val , msg : msg } ) ; break ; } default : util . dispatchEvent ( "net.dataSock." + cmd , msg ) ; } } function masterSockMsg ( ev ) { const msg = new DataView ( ev . data ) ; const cmd = msg . getUint32 ( 0 , true ) ; masterSock . keepalive ( ) ; util . dispatchEvent ( "net.masterSock." + cmd , msg ) ; } export function flacInfo ( to ) { flacInfoBuf = to ; dataSock . send ( to ) ; } function flushBuffers ( ) { if ( flushTimeout ) { clearTimeout ( flushTimeout ) ; flushTimeout = null ; } if ( ! dataSock ) return ; const ba = bufferedAmount ( ) ; if ( ba ) log . pushStatus ( "buffering" , "Sending audio to server (" + util . bytesToRepr ( ba ) + ")..." ) ; else log . popStatus ( "buffering" ) ; flushTimeout = setTimeout ( function ( ) { flushTimeout = null ; flushBuffers ( ) ; } , 1000 ) ; } export function bufferedAmount ( ) { return dataSock ? dataSock . sock . bufferedAmount : 0 ; } export function setAdminPerm ( target , deviceInfo , allowAudio , allowVideo ) { if ( allowAudio ) adminAccess [ target ] = { audio : true , video : allowVideo } ; else delete adminAccess [ target ] ; let permMessage ; return Promise . all ( [ ] ) . then ( ( ) => { if ( allowAudio ) return deviceInfo ( allowVideo ) ; else return null ; } ) . then ( ret => { if ( ret ) permMessage = JSON . stringify ( ret ) ; else permMessage = "" ; const permBuf = util . encodeText ( permMessage ) ; const p = prot . parts . info ; const out = new DataView ( new ArrayBuffer ( p . length + 1 + permBuf . length ) ) ; out . setUint32 ( 0 , prot . ids . info , true ) ; out . setUint32 ( p . key , prot . info . allowAdmin , true ) ; out . setUint32 ( p . value , target , true ) ; out . setUint8 ( p . length , allowAudio ? 1 : 0 ) ; new Uint8Array ( out . buffer ) . set ( permBuf , p . length + 1 ) ; dataSock . send ( out . buffer ) ; } ) . catch ( promiseFail ( ) ) ; } export function updateAdminPerm ( val , video ? ) { const permBuf = util . encodeText ( JSON . stringify ( val ) ) ; const p = prot . parts . info ; const out = new DataView ( new ArrayBuffer ( p . length + permBuf . length ) ) ; out . setUint32 ( 0 , prot . ids . info , true ) ; out . setUint32 ( p . key , prot . info . adminState , true ) ; new Uint8Array ( out . buffer ) . set ( permBuf , p . length ) ; for ( const target in adminAccess ) { const access = adminAccess [ target ] ; if ( video && ! access . video ) continue ; out . setUint32 ( p . value , + target , true ) ; dataSock . send ( out . buffer . slice ( 0 ) ) ; } } export function errorHandler ( error ) { const errBuf = util . encodeText ( error + "\n\n" + navigator . userAgent ) ; const out = new DataView ( new ArrayBuffer ( 4 + errBuf . length ) ) ; out . setUint32 ( 0 , prot . ids . error , true ) ; new Uint8Array ( out . buffer ) . set ( errBuf , 4 ) ; dataSock . send ( out . buffer ) ; } export function promiseFail ( ) { const loc = ( new Error ( ) . stack ) + "" ; return function ( ex ) { let exStack = "(Unknown stack)" ; try { exStack = ex . stack ; } catch ( ex ) { } errorHandler ( "Promise failure\n\n" + ex + "\n\n" + exStack + "\n\n" + loc ) ; } ; }