declare let MediaRecorder , webkitAudioContext ; import * as config from "./config" ; import * as log from "./log" ; import * as net from "./net" ; import { prot } from "./protocol" ; import * as capture from "./capture" ; import * as ui from "./ui" ; import * as util from "./util" ; import { dce } from "./util" ; import * as vad from "./vad" ; export let userMedia = null ; export let userMediaRTC = null ; export function setUserMediaRTC ( to ) { userMediaRTC = to ; } export let ac = null ; type Packet = [ number , DataView ] ; let packets = [ ] ; let zeroPacket = new Uint8Array ( [ 0xF8 , 0xFF , 0xFE ] ) ; export let timeOffset = null ; let targetTimeOffset = null ; const timeOffsetAdjPerFrame = 0.0002 ; const pongs = [ ] ; let sendSilence = 400 ; let sentZeroes = 999 ; export let lastSentTime = 0 ; export function setLastSentTime ( to ) { lastSentTime = to ; } let recordingTimerInterval = null ; let recordingTimerBaseST = 0 ; let recordingTimerBase = 0 ; let recordingTimerTicking = false ; util . netEvent ( "ping" , "pong" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . pong ; const sent = msg . getFloat64 ( p . clientTime , true ) ; const recvd = performance . now ( ) ; pongs . push ( recvd - sent ) ; while ( pongs . length > 5 ) pongs . shift ( ) ; if ( pongs . length < 5 ) { setTimeout ( net . ping , 150 ) ; } else { setTimeout ( net . ping , 10000 ) ; const latency = pongs . reduce ( function ( a , b ) { return a + b ; } ) / 10 ; const remoteTime = msg . getFloat64 ( p . serverTime , true ) + latency ; targetTimeOffset = remoteTime - recvd ; if ( timeOffset === null ) timeOffset = targetTimeOffset ; } } ) ; export function getAudioPerms ( mkAudioUI ) { return navigator . mediaDevices . getUserMedia ( { audio : true } ) . catch ( ( ) => null ) . then ( function ( userMediaIn ) { userMedia = userMediaIn ; return getMic ( mkAudioUI ( ) ) ; } ) . catch ( function ( err ) { config . disconnect ( ) ; log . pushStatus ( "fail" , "Cannot get microphone: " + err ) ; log . popStatus ( "getmic" ) ; } ) ; } export function getMic ( deviceId ? ) { if ( ! net . connected ) return ; log . pushStatus ( "getmic" , "Asking for microphone permission..." ) ; log . popStatus ( "conn" ) ; if ( userMedia ) { userMedia . getTracks ( ) . forEach ( function ( track ) { track . stop ( ) ; } ) ; userMedia = null ; if ( userMediaRTC ) { userMediaRTC . getTracks ( ) . forEach ( function ( track ) { track . stop ( ) ; } ) ; userMediaRTC = null ; } util . dispatchEvent ( "usermediastopped" , { } ) ; } return navigator . mediaDevices . getUserMedia ( { audio : < any > { deviceId : deviceId , autoGainControl : { ideal : ui . ui . panels . inputConfig . agc . checked } , echoCancellation : { ideal : getEchoCancel ( ) } , noiseSuppression : { ideal : false } , sampleRate : { ideal : 48000 } , sampleSize : { ideal : 24 } } } ) . catch ( ( ) => null ) . then ( function ( userMediaIn ) { userMedia = userMediaIn ; return userMediaSet ( ) ; } ) . catch ( function ( err ) { config . disconnect ( ) ; log . pushStatus ( "fail" , "Cannot get microphone: " + err ) ; log . popStatus ( "getmic" ) ; } ) ; } function userMediaSet ( ) { if ( ! net . connected ) return ; if ( ! ac ) { try { ac = new AudioContext ( { latencyHint : "playback" } ) ; } catch ( ex ) { ac = new webkitAudioContext ( ) ; } const msd = ( < any > ac ) . ecDestination = ac . createMediaStreamDestination ( ) ; util . events . addEventListener ( "usermediartcready" , function ( ) { if ( ! ui . ui . audioOutput ) { const a = ui . ui . audioOutput = dce ( "audio" ) ; a . style . display = "none" ; document . body . appendChild ( a ) ; } ui . ui . audioOutput . srcObject = msd . stream ; ui . ui . audioOutput . play ( ) . catch ( console . error ) ; } ) ; } let noUserMedia = false ; if ( ! userMedia ) { noUserMedia = true ; const cs = ac . createConstantSource ( ) ; const msd = ac . createMediaStreamDestination ( ) ; cs . connect ( msd ) ; userMedia = msd . stream ; } return Promise . all ( [ ] ) . then ( function ( ) { if ( ac . state !== "running" ) { ui . showPanel ( ui . ui . panels . mobile , ui . ui . panels . mobile . button , true ) ; return new Promise ( function ( res ) { ui . ui . panels . mobile . button . onclick = function ( ) { ui . unsetModal ( ) ; ui . showPanel ( null , ui . ui . persistent . main ) ; ac . resume ( ) . catch ( res ) . then ( res ) ; } ; } ) ; } } ) . then ( < any > function ( ) { if ( ac . state !== "running" ) log . pushStatus ( "audiocontext" , "Cannot capture audio! State: " + ac . state ) ; window . addEventListener ( "error" , function ( error ) { try { let msg = "" ; if ( error . error ) msg = error . error + "\n\n" + error . error . stack ; else msg = error . message + "\n\n" + error . filename + ":" + error . lineno ; net . errorHandler ( msg ) ; } catch ( ex ) { } } ) ; window . addEventListener ( "unhandledrejection" , function ( error ) { error = error . reason ; if ( error instanceof Error ) { net . errorHandler ( error + "\n\n" + error . stack ) ; } else { let msg ; try { msg = JSON . stringify ( error ) ; } catch ( ex ) { msg = error + "" ; } msg += "\n\n" + new Error ( ) . stack ; net . errorHandler ( msg ) ; } } ) ; if ( noUserMedia ) { log . pushStatus ( "usermedia" , "Failed to capture audio!" ) ; setTimeout ( function ( ) { log . popStatus ( "usermedia" ) ; } , 10000 ) ; } util . dispatchEvent ( "audio.mute" ) ; log . pushStatus ( "initenc" , "Initializing encoder..." ) ; log . popStatus ( "getmic" ) ; util . dispatchEvent ( "usermediaready" , { } ) ; } ) . catch ( net . promiseFail ( ) ) . then ( encoderLoaded ) ; } function encoderLoaded ( ) { if ( ! net . connected ) return ; log . pushStatus ( "startenc" , "Starting encoder..." ) ; log . popStatus ( "initenc" ) ; return encoderStart ( ) ; } function encoderStart ( ) { let sampleRate = 48000 ; if ( config . useFlac && ac . sampleRate === 44100 ) sampleRate = 44100 ; if ( config . useFlac ) { const p = prot . parts . info ; const info = new DataView ( new ArrayBuffer ( p . length ) ) ; info . setUint32 ( 0 , prot . ids . info , true ) ; info . setUint32 ( p . key , prot . info . sampleRate , true ) ; info . setUint32 ( p . value , sampleRate , true ) ; net . flacInfo ( info . buffer ) ; } if ( config . useFlac ) { switch ( sampleRate ) { case 44100 : zeroPacket = new Uint8Array ( [ 0xFF , 0xF8 , 0x79 , 0x0C , 0x00 , 0x03 , 0x71 , 0x56 , 0x00 , 0x00 , 0x00 , 0x00 , 0x63 , 0xC5 ] ) ; break ; default : zeroPacket = new Uint8Array ( [ 0xFF , 0xF8 , 0x7A , 0x0C , 0x00 , 0x03 , 0xBF , 0x94 , 0x00 , 0x00 , 0x00 , 0x00 , 0xB1 , 0xCA ] ) ; } } let channelLayout = 4 ; const channelCount = ~ ~ ( ( < any > userMedia . getAudioTracks ( ) [ 0 ] . getSettings ( ) ) . channelCount ) ; if ( channelCount > 1 ) channelLayout = Math . pow ( 2 , channelCount ) - 1 ; return capture . createCapture ( ac , { ms : userMedia , matchSampleRate : true , bufferSize : 16384 , outStream : true , sampleRate : "inSampleRate" , workerCommand : { c : "encoder" , outSampleRate : sampleRate , format : config . useFlac ? "flac" : "opus" , channelLayout : channelLayout , channelCount : channelCount } } ) . then ( capture => { let last = 0 ; capture . worker . onmessage = function ( ev ) { const msg = ev . data ; if ( msg . c !== "packets" ) return ; const p = msg . d ; const now = msg . ts + performance . now ( ) - Date . now ( ) ; let pktTime = Math . round ( now * 48 - p . length * 960 ) ; for ( let pi = 0 ; pi < p . length ; pi ++ ) { packets . push ( [ pktTime , new DataView ( p [ pi ] . buffer ) ] ) ; pktTime += 960 ; } if ( msg . s > last ) net . errorHandler ( "Sequence error! " + msg . s + " " + last ) ; last = msg . s + p . length ; handlePackets ( ) ; } ; util . events . addEventListener ( "usermediastopped" , capture . disconnect , { once : true } ) ; } ) . catch ( net . promiseFail ( ) ) ; } function handlePackets ( ) { if ( ! packets . length || timeOffset === null ) return ; const curGranulePos = packets [ packets . length - 1 ] [ 0 ] ; net . setTransmitting ( true ) ; lastSentTime = performance . now ( ) ; log . popStatus ( "startenc" ) ; if ( net . mode !== prot . mode . rec ) { while ( packets . length ) packets . pop ( ) ; return ; } const ba = net . bufferedAmount ( ) ; if ( ba > 1024 * 1024 ) log . pushStatus ( "buffering" , util . bytesToRepr ( ba ) + " audio data buffered" ) ; else log . popStatus ( "buffering" ) ; if ( ! vad . vadOn ) { const old = curGranulePos - vad . vadExtension * 48 ; while ( packets [ 0 ] [ 0 ] < old ) { const packet = packets . shift ( ) ; const granulePos = adjustTime ( packet ) ; if ( granulePos < 0 ) continue ; if ( config . useContinuous || sendSilence > 0 ) { sendPacket ( granulePos , packet [ 1 ] , 0 ) ; sendSilence -- ; } else if ( sentZeroes < 3 ) { if ( granulePos < 0 ) continue ; sendPacket ( granulePos , zeroPacket , 0 ) ; sentZeroes ++ ; } } } else { const vadVal = ( vad . rawVadOn ? 2 : 1 ) ; packets . forEach ( function ( packet ) { const data = packet [ 1 ] ; const granulePos = adjustTime ( packet ) ; if ( granulePos < 0 ) return ; sendPacket ( granulePos , data , vadVal ) ; } ) ; sentZeroes = 0 ; packets = [ ] ; } } function sendPacket ( granulePos , data , vadVal ) { const p = prot . parts . data ; const msg = new DataView ( new ArrayBuffer ( p . length + ( config . useContinuous ? 1 : 0 ) + data . buffer . byteLength ) ) ; msg . setUint32 ( 0 , prot . ids . data , true ) ; msg . setUint32 ( p . granulePos , granulePos & 0xFFFFFFFF , true ) ; msg . setUint16 ( p . granulePos + 4 , ( granulePos / 0x100000000 ) & 0xFFFF , true ) ; if ( config . useContinuous ) msg . setUint8 ( p . packet , vadVal ) ; const data8 = new Uint8Array ( data . buffer ) ; ( new Uint8Array ( msg . buffer ) ) . set ( data8 , p . packet + ( config . useContinuous ? 1 : 0 ) ) ; net . dataSock . send ( msg . buffer ) ; } function adjustTime ( packet ) { if ( targetTimeOffset > timeOffset ) { if ( targetTimeOffset > timeOffset + timeOffsetAdjPerFrame ) timeOffset += timeOffsetAdjPerFrame ; else timeOffset = targetTimeOffset ; } else if ( targetTimeOffset < timeOffset ) { if ( targetTimeOffset < timeOffset - timeOffsetAdjPerFrame ) timeOffset -= timeOffsetAdjPerFrame ; else timeOffset = targetTimeOffset ; } return Math . round ( packet [ 0 ] + timeOffset * 48 ) ; } function getMute ( ) { const track = userMedia . getAudioTracks ( ) [ 0 ] ; return ! track . enabled ; } export function getEchoCancel ( ) { return ui . ui . panels . inputConfig . echo . checked ; } export function toggleMute ( to ? ) { if ( ! userMedia ) return ; const track = userMedia . getAudioTracks ( ) [ 0 ] ; if ( typeof to === "undefined" ) to = ! track . enabled ; track . enabled = to ; util . dispatchEvent ( "audio.mute" ) ; net . updateAdminPerm ( { mute : ! to } ) ; } export function setEchoCancel ( to ) { ui . ui . panels . inputConfig . echo . checked = to ; net . updateAdminPerm ( { echo : to } ) ; return getMic ( ui . ui . panels . inputConfig . device . value ) ; } function playStopSound ( url , status , time ) { let sound = ui . ui . sounds . soundboard [ url ] ; if ( ! sound ) { sound = ui . ui . sounds . soundboard [ url ] = { el : document . createElement ( "audio" ) } ; let format = "m4a" ; if ( typeof MediaSource !== "undefined" && MediaSource . isTypeSupported ( "audio/webm; codecs=opus" ) ) format = "webm" ; sound . el . src = url + "." + format ; if ( ui . ui . panels . outputConfig ) { sound . el . volume = ( + ui . ui . panels . outputConfig . sfxVolume . value ) / 100 ; ui . ui . panels . outputConfig . sfxVolumeHider . style . display = "" ; } } const el = sound . el ; el . pause ( ) ; el . playbackRate = 1 ; el . ecStartTime = time ; if ( status ) { el . currentTime = 0 ; el . play ( ) . then ( catchup ) ; } function catchup ( ) { if ( el . duration < 10 || el . ended || el . ecStartTime !== time ) { return ; } if ( ! timeOffset ) { setTimeout ( catchup , 1000 ) ; return ; } const elCurTime = el . ecStartTime + el . currentTime * 1000 ; const realCurTime = performance . now ( ) + timeOffset ; if ( elCurTime < realCurTime - 100 || elCurTime > realCurTime + 100 ) { let rate ; if ( elCurTime < realCurTime ) rate = Math . min ( 1 + ( realCurTime - elCurTime ) / 1000 , 16 ) ; else rate = Math . max ( 1 / ( 1 + ( elCurTime - realCurTime ) / 1000 ) , 0.75 ) ; if ( rate < 0.75 || rate > 4 ) el . muted = true ; el . playbackRate = rate ; setTimeout ( function ( ) { if ( el . ecStartTime !== time ) return ; el . playbackRate = 1 ; catchup ( ) ; } , 1000 ) ; } else { el . muted = false ; } } util . dispatchEvent ( "audio.sound" , { url : url , status : status , el : el } ) ; } util . netEvent ( "data" , "sound" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . sound . sc ; const time = msg . getFloat64 ( p . time , true ) ; const status = msg . getUint8 ( p . status ) ; const url = util . decodeText ( msg . buffer . slice ( p . url ) ) ; playStopSound ( url , status , time ) ; } ) ; export function setRecordingTimer ( serverTime , recTime , ticking ) { recordingTimerBaseST = serverTime ; recordingTimerBase = recTime ; recordingTimerTicking = ticking ; if ( ! recordingTimerInterval ) recordingTimerInterval = setInterval ( tickRecordingTimer , 500 ) ; } util . events . addEventListener ( "net.info." + prot . info . mode , function ( ev ) { const val = ev . detail . val ; const msg = ev . detail . msg ; const p = prot . parts . info ; if ( msg . byteLength >= p . length + 16 ) { const sTime = msg . getFloat64 ( p . value + 4 , true ) ; const recTime = msg . getFloat64 ( p . value + 12 , true ) ; setRecordingTimer ( sTime , recTime , ( val === prot . mode . rec ) ) ; } } ) ; function tickRecordingTimer ( ) { let time = recordingTimerBase ; if ( recordingTimerTicking && timeOffset !== null ) time += performance . now ( ) + timeOffset - recordingTimerBaseST ; time = ~ ~ ( time / 1000 ) ; let s = "" + ~ ~ ( time % 60 ) ; if ( s . length < 2 ) s = "0" + s ; time = ~ ~ ( time / 60 ) ; let m = "" + ~ ~ ( time % 60 ) ; time = ~ ~ ( time / 60 ) ; const h = ~ ~ time ; if ( h && m . length < 2 ) m = "0" + m ; const timer = ui . ui . log . timer ; timer . style . color = recordingTimerTicking ? "#080" : "#800" ; timer . innerText = ( h ? ( h + ":" ) : "" ) + m + ":" + s ; } export function deviceInfo ( allowVideo ) { return navigator . mediaDevices . enumerateDevices ( ) . then ( ( devices ) => { const audio = [ ] ; const video = allowVideo ? [ ] : null ; let ctr = 1 , ctrv = 1 ; devices . forEach ( ( dev ) => { if ( dev . kind === "audioinput" ) { audio . push ( { id : dev . deviceId , label : dev . label || ( "Mic " + ctr ++ ) } ) ; } else if ( dev . kind === "videoinput" && allowVideo ) { video . push ( { id : dev . deviceId , label : dev . label || ( "Camera " + ctrv ++ ) } ) ; } } ) ; return { audioDevices : audio , audioDevice : ui . ui . panels . inputConfig . device . value , videoDevices : video , videoDevice : allowVideo ? ui . ui . panels . videoConfig . device . value : null , videoRes : allowVideo ? + ui . ui . panels . videoConfig . res . value : null , videoRec : ( typeof MediaRecorder !== "undefined" ) , mute : getMute ( ) , echo : getEchoCancel ( ) , vadSensitivity : + ui . ui . panels . inputConfig . vadSensitivity . value , vadNoiseGate : + ui . ui . panels . inputConfig . vadNoiseGate . value } ; } ) ; } util . netEvent ( "data" , "admin" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . admin ; const acts = prot . flags . admin . actions ; const action = msg . getUint32 ( p . action , true ) ; if ( action === acts . mute ) { toggleMute ( false ) ; } else if ( action === acts . echoCancel ) { setEchoCancel ( true ) ; } else if ( action === acts . request ) { const reqNick = util . decodeText ( msg . buffer . slice ( p . argument ) ) ; const target = msg . getUint32 ( p . target , true ) ; ui . ui . panels . userAdminReq . user = target ; ui . ui . panels . userAdminReq . name . innerText = reqNick ; ui . showPanel ( "userAdminReq" , "audio" ) ; } else { const src = msg . getUint32 ( p . target , true ) ; const acc = net . adminAccess [ src ] ; if ( ! acc || ! acc . audio ) return ; if ( action >= acts . videoInput && ! acc . video ) return ; switch ( action ) { case acts . unmute : toggleMute ( true ) ; break ; case acts . unechoCancel : setEchoCancel ( false ) ; break ; case acts . audioInput : { const arg = util . decodeText ( msg . buffer . slice ( p . argument ) ) ; ui . ui . panels . inputConfig . device . value = arg ; net . updateAdminPerm ( { audioDevice : arg } ) ; getMic ( arg ) ; break ; } case acts . vadSensitivity : { const arg = msg . getInt32 ( p . argument , true ) ; const inp = ui . ui . panels . inputConfig . vadSensitivity ; inp . value = "" + arg ; inp . oninput ( null ) ; inp . onchange ( null ) ; break ; } case acts . vadNoiseGate : { const arg = msg . getInt32 ( p . argument , true ) ; const inp = ui . ui . panels . inputConfig . vadNoiseGate ; inp . value = "" + arg ; inp . oninput ( null ) ; inp . onchange ( null ) ; break ; } default : util . dispatchEvent ( "net.admin.video" , { action : action , arg : util . decodeText ( msg . buffer . slice ( p . argument ) ) } ) ; } } } ) ;