declare let JitsiMeetJS ; import * as audio from "./audio" ; import * as config from "./config" ; import * as net from "./net" ; import * as outproc from "./outproc" ; import { prot } from "./protocol" ; import * as ui from "./ui" ; import * as util from "./util" ; import * as vad from "./vad" ; import * as video from "./video" ; import * as videoRecord from "./video-record" ; import * as wsp from "web-streams-polyfill/ponyfill" ; export let videoRecHost = - 1 ; const jitsiFeatures = { disableSimulcast : false , disableP2P : false } ; let connection ; let room ; let jPromise = Promise . all ( [ ] ) ; let jCounter = 0 ; let jAudio ; let jVideo ; interface JitsiPeer { id : string ; audio : any ; video : any ; rtc : RTCPeerConnection ; data : RTCDataChannel ; signal : ( msg : any ) => unknown ; rtcReady : boolean ; } const jitsiPeers = { } ; interface VideoRecIncoming { nextIdx : number ; buf : { idx : number ; buf : Uint8Array ; } [ ] ; notify : ( ) => void ; hardStop : boolean ; softStop : boolean ; } const videoRecIncoming = { } ; function assertJitsiPeer ( id , jid ) { if ( jitsiPeers [ id ] ) return jitsiPeers [ id ] ; const ret = jitsiPeers [ id ] = { id : jid , audio : < any > null , video : < any > null , rtc : < RTCPeerConnection > null , data : < RTCDataChannel > null , signal : < ( msg : any ) => unknown > null , rtcReady : false } ; startRTC ( id , ret ) ; return ret ; } function initJitsi ( ) { if ( ! audio . userMediaRTC ) { util . events . addEventListener ( "usermediartcready" , initJitsi , { once : true } ) ; return ; } let timeout = null ; jPromise = jPromise . then ( ( ) => { if ( typeof JitsiMeetJS === "undefined" ) return util . loadLibrary ( "libs/jquery.min.js" ) ; } ) . then ( ( ) => { if ( typeof JitsiMeetJS === "undefined" ) return util . loadLibrary ( "libs/lib-jitsi-meet.min.js" ) ; } ) . then ( ( ) => { for ( const id of Object . keys ( jitsiPeers ) ) { const inc = ( < any > jitsiPeers ) [ id ] ; if ( inc . video ) jitsiTrackRemoved ( inc . video ) ; if ( inc . audio ) jitsiTrackRemoved ( inc . audio ) ; if ( ! inc . rtcReady ) { try { inc . rtc . close ( ) ; } catch ( ex ) { } delete ( < any > jitsiPeers ) [ id ] ; } } if ( room ) { room . removeEventListener ( JitsiMeetJS . events . conference . CONFERENCE_LEFT , config . disconnect ) ; return room . leave ( ) ; } } ) . then ( ( ) => { room = null ; if ( connection ) { connection . removeEventListener ( JitsiMeetJS . events . connection . CONNECTION_DISCONNECTED , config . disconnect ) ; connection . disconnect ( ) ; connection = null ; } JitsiMeetJS . setLogLevel ( JitsiMeetJS . logLevels . ERROR ) ; const initDict = { disableAudioLevels : true , disableSimulcast : jitsiFeatures . disableSimulcast } ; if ( jitsiFeatures . disableSimulcast ) initDict . preferredCodec = "h264" ; JitsiMeetJS . init ( initDict ) ; return new Promise ( function ( res , rej ) { connection = new JitsiMeetJS . JitsiConnection ( null , null , { hosts : { domain : "jitsi." + config . url . host , muc : "conference.jitsi." + config . url . host } , bosh : config . jitsiUrl , clientNode : "https://ennuicastr.com/" } ) ; connection . addEventListener ( JitsiMeetJS . events . connection . CONNECTION_ESTABLISHED , res ) ; connection . addEventListener ( JitsiMeetJS . events . connection . CONNECTION_FAILED , ( ) => { rej ( new Error ( "Connection failed" ) ) ; } ) ; connection . addEventListener ( JitsiMeetJS . events . connection . CONNECTION_DISCONNECTED , config . disconnect ) ; timeout = setTimeout ( ( ) => { rej ( new Error ( "Connection timeout" ) ) ; } , 30000 ) ; connection . connect ( ) ; } ) ; } ) . then ( ( ) => { clearTimeout ( timeout ) ; return new Promise ( function ( res , rej ) { const roomNm = config . config . id . toString ( 36 ) + "_" + config . config . key . toString ( 36 ) + ( jitsiFeatures . disableSimulcast ? "_nosc" : "" ) + ( jitsiFeatures . disableP2P ? "_nop2p" : "" ) ; room = connection . initJitsiConference ( roomNm , { openBridgeChannel : true , p2p : { enabled : ! jitsiFeatures . disableP2P } } ) ; room . addEventListener ( JitsiMeetJS . events . conference . CONFERENCE_JOINED , res ) ; room . addEventListener ( JitsiMeetJS . events . conference . CONFERENCE_LEFT , config . disconnect ) ; room . addEventListener ( JitsiMeetJS . events . conference . CONFERENCE_FAILED , ( ) => { rej ( new Error ( "Conference failed" ) ) ; } ) ; room . addEventListener ( JitsiMeetJS . events . conference . CONFERENCE_ERROR , config . disconnect ) ; room . addEventListener ( JitsiMeetJS . events . conference . TRACK_ADDED , jitsiTrackAdded ) ; room . addEventListener ( JitsiMeetJS . events . conference . TRACK_REMOVED , jitsiTrackRemoved ) ; room . addEventListener ( JitsiMeetJS . events . conference . USER_JOINED , jitsiUserJoined ) ; room . addEventListener ( JitsiMeetJS . events . conference . ENDPOINT_MESSAGE_RECEIVED , jitsiMessage ) ; if ( ! config . useRTEnnui . audio ) { util . events . addEventListener ( "usermediartcready" , ( ) => { jitsiSetUserMediaRTC ( ) ; } ) ; jitsiSetUserMediaRTC ( ) ; } util . events . addEventListener ( "usermediavideoready" , ( ) => { jitsiSetUserMediaVideo ( ) ; } ) ; jitsiSetUserMediaVideo ( ) ; timeout = setTimeout ( ( ) => { rej ( new Error ( "Conference timeout" ) ) ; } , 30000 ) ; room . setDisplayName ( "" + net . selfId ) ; room . join ( ) ; } ) ; } ) . then ( ( ) => { clearTimeout ( timeout ) ; } ) . catch ( ex => { initJitsi ( ) ; throw ex ; } ) . catch ( net . promiseFail ( ) ) ; return jPromise ; } util . events . addEventListener ( "net.info." + prot . info . id , function ( ) { if ( config . useRTC ) initJitsi ( ) ; } ) ; util . events . addEventListener ( "net.info." + prot . info . jitsi , function ( ev ) { if ( config . useRTC ) { const msg = new Uint8Array ( ev . detail . msg . buffer ) ; const p = prot . parts . info ; const jitsiStr = util . decodeText ( msg . slice ( p . value ) ) ; const jitsiF = JSON . parse ( jitsiStr ) ; if ( ! ! jitsiF . disableSimulcast !== jitsiFeatures . disableSimulcast || ! ! jitsiF . disableP2P !== jitsiFeatures . disableP2P ) { jitsiFeatures . disableSimulcast = ! ! jitsiF . disableSimulcast ; jitsiFeatures . disableP2P = ! ! jitsiF . disableP2P ; jPromise = jPromise . then ( ( ) => { if ( room ) initJitsi ( ) ; } ) ; } } } ) ; function jitsiSetUserMediaRTC ( retries ? ) { if ( typeof retries === "undefined" ) retries = 2 ; if ( ! audio . userMediaRTC ) return Promise . all ( [ ] ) ; jitsiUnsetUserMediaRTC ( ) ; jPromise = jPromise . then ( ( ) => { jAudio = new JitsiMeetJS . JitsiLocalTrack ( { deviceId : audio . userMediaRTC . id , rtcId : audio . userMediaRTC . id + ":" + ( jCounter ++ ) , mediaType : "audio" , stream : audio . userMediaRTC , track : audio . userMediaRTC . getAudioTracks ( ) [ 0 ] } ) ; return room . addTrack ( jAudio ) ; } ) . then ( ( ) => { util . events . addEventListener ( "usermediastopped" , jitsiUnsetUserMediaRTC , { once : true } ) ; } ) . catch ( ( ) => { if ( retries ) { setTimeout ( ( ) => { jitsiSetUserMediaRTC ( retries - 1 ) ; } , 1000 ) ; } else { net . promiseFail ( ) ; } } ) ; return jPromise ; } function jitsiUnsetUserMediaRTC ( ) { jPromise = jPromise . then ( ( ) => { if ( ! jAudio ) return ; return room . removeTrack ( jAudio ) ; } ) . then ( ( ) => { jAudio = null ; } ) . catch ( ex => { initJitsi ( ) ; throw ex ; } ) . catch ( net . promiseFail ( ) ) ; return jPromise ; } function jitsiSetUserMediaVideo ( retries ? ) { if ( config . useRecordOnly ) { return ; } if ( typeof retries === "undefined" ) retries = 2 ; if ( ! video . userMediaVideo ) return ; jitsiUnsetUserMediaVideo ( ) ; jPromise = jPromise . then ( ( ) => { jVideo = new JitsiMeetJS . JitsiLocalTrack ( { deviceId : video . userMediaVideo . id , rtcId : video . userMediaVideo . id + ":" + ( jCounter ++ ) , mediaType : "video" , stream : video . userMediaVideo , track : video . userMediaVideo . getVideoTracks ( ) [ 0 ] } ) ; return room . addTrack ( jVideo ) ; } ) . then ( ( ) => { util . events . addEventListener ( "usermediavideostopped" , jitsiUnsetUserMediaVideo , { once : true } ) ; } ) . catch ( ( ) => { if ( retries ) { setTimeout ( ( ) => { jitsiSetUserMediaVideo ( retries - 1 ) ; } , 1000 ) ; } else { net . promiseFail ( ) ; } } ) ; return jPromise ; } function jitsiUnsetUserMediaVideo ( ) { jPromise = jPromise . then ( ( ) => { if ( ! jVideo ) return ; return room . removeTrack ( jVideo ) ; } ) . then ( ( ) => { jVideo = null ; } ) . catch ( net . promiseFail ( ) ) ; return jPromise ; } function getUserIdFromTrack ( track ) { return getUserIdFromJid ( track . getParticipantId ( ) ) ; } function getUserIdFromJid ( jid ) { let user = null ; const parts = room . getParticipants ( ) ; for ( let i = 0 ; i < parts . length ; i ++ ) { if ( parts [ i ] . getId ( ) === jid ) { user = parts [ i ] ; break ; } } if ( ! user ) return null ; const id = + user . getDisplayName ( ) ; if ( Number . isNaN ( id ) ) return null ; return id ; } function jitsiTrackAdded ( track ) { if ( track . isLocal ( ) ) return ; const stream = track . getOriginalStream ( ) ; const type = track . getType ( ) ; const id = getUserIdFromTrack ( track ) ; const jid = track . getParticipantId ( ) ; if ( ! ( id in jitsiPeers ) ) assertJitsiPeer ( id , jid ) ; const inc = jitsiPeers [ id ] ; ( < any > inc ) [ type ] = track ; ui . videoAdd ( id , null ) ; const el = ( < any > ui . ui . video . users [ id ] ) [ type ] ; el . srcObject = stream ; if ( el . paused ) el . play ( ) . catch ( net . promiseFail ( ) ) ; if ( type === "video" ) ui . ui . video . users [ id ] . standin . style . display = "none" ; if ( type === "audio" ) outproc . createCompressor ( id , audio . ac , stream , ui . ui . video . users [ id ] . waveformWrapper ) ; if ( ui . ui . video . major === id ) setMajor ( id ) ; } function jitsiTrackRemoved ( track ) { if ( track . isLocal ( ) ) return ; const type = track . getType ( ) ; const id = getUserIdFromTrack ( track ) ; if ( ! ( id in jitsiPeers ) ) return ; const inc = jitsiPeers [ id ] ; if ( ( < any > inc ) [ type ] !== track ) return ; ( < any > inc ) [ type ] = null ; if ( type === "video" && videoRecIncoming [ id ] ) { const vr = videoRecIncoming [ id ] ; vr . hardStop = true ; if ( vr . notify ) vr . notify ( ) ; delete videoRecIncoming [ id ] ; } if ( ! inc . audio && ! inc . video && ! inc . rtcReady ) { try { inc . rtc . close ( ) ; } catch ( ex ) { } delete jitsiPeers [ id ] ; } if ( ui . ui . video . users [ id ] ) { const el = ( < any > ui . ui . video . users [ id ] ) [ type ] ; el . srcObject = null ; if ( type === "video" ) ui . ui . video . users [ id ] . standin . style . display = "" ; } if ( type === "audio" ) outproc . destroyCompressor ( id ) ; } function jitsiUserJoined ( jid ) { const id = getUserIdFromJid ( jid ) ; if ( ! ( id in jitsiPeers ) ) assertJitsiPeer ( id , jid ) ; speech ( vad . vadOn , id ) ; if ( "master" in config . config ) { videoRecSend ( id , prot . videoRec . videoRecHost , ~ ~ ui . ui . panels . master . acceptRemoteVideo . checked ) ; } } function jitsiMessage ( user , jmsg ) { if ( jmsg . type !== "ennuicastr" ) return ; const jid = user . getId ( ) ; const peer = + user . getDisplayName ( ) ; if ( Number . isNaN ( peer ) ) return ; assertJitsiPeer ( peer , jid ) ; if ( typeof jmsg . ec !== "string" ) return ; const ec = jmsg . ec ; const buf = new Uint8Array ( ec . length ) ; for ( let i = 0 ; i < buf . length ; i ++ ) buf [ i ] = ec . charCodeAt ( i ) ; const msg = new DataView ( buf . buffer ) ; return peerMessage ( peer , msg ) ; } util . events . addEventListener ( "net.dataSock." + prot . ids . ctcp , function ( ev ) { if ( config . useRTC ) { const p = prot . parts . ctcp ; const peer = ev . detail . getUint32 ( p . peer , true ) ; const u8 = new Uint8Array ( ev . detail . buffer ) ; const msg = new DataView ( u8 . slice ( p . msg ) . buffer ) ; peerMessage ( peer , msg ) ; } } ) ; function peerMessage ( peer , msg ) { if ( msg . byteLength < 4 ) return ; const cmd = msg . getUint32 ( 0 , true ) ; switch ( cmd ) { case prot . ids . data : try { const vr = videoRecIncoming [ peer ] ; const idx = msg . getFloat64 ( 4 , true ) ; const buf = new Uint8Array ( msg . buffer ) . subarray ( 12 ) ; vr . buf . push ( { idx , buf } ) ; if ( vr . buf . length >= 1024 ) { vr . hardStop = true ; delete videoRecIncoming [ peer ] ; } if ( vr . notify ) vr . notify ( ) ; } catch ( ex ) { } break ; case prot . ids . caption : { const p = prot . parts . caption . cc ; if ( msg . byteLength < p . length ) return ; const append = ! ! msg . getUint8 ( p . append ) ; const complete = ! ! msg . getUint8 ( p . complete ) ; try { const text = util . decodeText ( new Uint8Array ( msg . buffer ) . subarray ( p . text ) ) ; ui . caption ( peer , text , append , complete ) ; } catch ( ex ) { } break ; } case prot . ids . speech : { const p = prot . parts . speech ; if ( msg . byteLength < p . length ) return ; const status = ! ! msg . getUint32 ( p . indexStatus , true ) ; util . dispatchEvent ( "ui.speech" , { user : peer , status : status } ) ; break ; } case prot . ids . videoRec : { const p = prot . parts . videoRec ; const pv = prot . videoRec ; if ( msg . byteLength < p . length ) return ; const cmd = msg . getUint32 ( p . cmd , true ) ; switch ( cmd ) { case pv . videoRecHost : { let accept = 0 ; try { accept = msg . getUint32 ( p . length , true ) ; } catch ( ex ) { } if ( accept ) videoRecHost = peer ; else if ( videoRecHost === peer ) videoRecHost = - 1 ; break ; } case pv . startVideoRecReq : if ( "master" in config . config && ui . ui . panels . master . acceptRemoteVideo . checked ) { let opts = { } ; if ( msg . byteLength > p . length ) { try { opts = JSON . parse ( util . decodeText ( new Uint8Array ( msg . buffer ) . subarray ( p . length ) ) ) ; } catch ( ex ) { } } const vri = videoRecIncoming [ peer ] = { nextIdx : 0 , buf : < { idx : number ; buf : Uint8Array ; } [ ] > [ ] , notify : < ( ) => void > null , hardStop : false , softStop : false } ; const stream = < ReadableStream < Uint8Array > > < unknown > new wsp . ReadableStream ( { async pull ( controller ) { while ( true ) { if ( vri . hardStop ) { controller . close ( ) ; break ; } let found = false ; for ( let i = 0 ; i < vri . buf . length ; i ++ ) { const buf = vri . buf [ i ] ; if ( buf . idx === vri . nextIdx ) { found = true ; vri . buf . splice ( i , 1 ) ; if ( buf . buf ) { controller . enqueue ( buf . buf ) ; vri . nextIdx += buf . buf . length ; } else { controller . close ( ) ; } break ; } } if ( found ) break ; if ( ! found && vri . softStop ) { controller . close ( ) ; break ; } await new Promise < void > ( res => vri . notify = res ) ; vri . notify = null ; } } } ) ; videoRecord . recordVideoRemoteIncoming ( peer , stream , opts ) ; videoRecSend ( peer , prot . videoRec . startVideoRecRes , 1 ) ; } else { videoRecSend ( peer , prot . videoRec . startVideoRecRes , 0 ) ; } break ; case pv . startVideoRecRes : if ( videoRecord . recordVideoRemoteOK && peer === videoRecHost ) videoRecord . recordVideoRemoteOK ( peer ) ; break ; case pv . endVideoRec : try { const vr = videoRecIncoming [ peer ] ; vr . softStop = true ; if ( vr . notify ) vr . notify ( ) ; delete videoRecIncoming [ peer ] ; } catch ( ex ) { } break ; } break ; } } } function sendJitsiMsg ( jid , msg , retries ? ) { if ( typeof retries === "undefined" ) retries = 10 ; try { if ( jid === null ) { if ( Object . keys ( jitsiPeers ) . length > 0 ) room . broadcastEndpointMessage ( msg ) ; } else { room . sendEndpointMessage ( jid , msg ) ; } } catch ( ex ) { if ( retries ) { setTimeout ( ( ) => { sendJitsiMsg ( jid , msg , retries - 1 ) ; } , 1000 ) ; } else { initJitsi ( ) ; throw ex ; } } } function sendMsg ( msg , peer ? ) { if ( ! room ) return ; let inc = null ; let jid = null ; if ( typeof peer !== "undefined" ) { if ( ! ( peer in jitsiPeers ) ) return ; inc = jitsiPeers [ peer ] ; jid = inc . id ; } if ( inc && inc . rtcReady ) { inc . data . send ( msg . buffer ) ; return ; } if ( typeof peer !== "undefined" ) { const p = prot . parts . ctcp ; const cmsg = new DataView ( new ArrayBuffer ( p . length + msg . length ) ) ; cmsg . setUint32 ( 0 , prot . ids . ctcp , true ) ; cmsg . setUint32 ( p . peer , peer , true ) ; ( new Uint8Array ( cmsg . buffer ) ) . set ( msg , p . msg ) ; net . dataSock . send ( cmsg . buffer ) ; } else { const msga = [ ] ; for ( let i = 0 ; i < msg . length ; i ++ ) msga . push ( String . fromCharCode ( msg [ i ] ) ) ; const msgs = msga . join ( "" ) ; const msgj = { type : "ennuicastr" , ec : msgs } ; sendJitsiMsg ( jid , msgj ) ; } } export function videoRecSend ( peer , cmd , payloadData ? ) { if ( ! config . useRTC ) return ; let payload ; if ( typeof payloadData === "number" ) { payload = new Uint8Array ( 4 ) ; const dv = new DataView ( payload . buffer ) ; dv . setUint32 ( 0 , payloadData , true ) ; } else if ( typeof payloadData === "object" ) { payload = util . encodeText ( JSON . stringify ( payloadData ) ) ; } else { payload = new Uint8Array ( 0 ) ; } const p = prot . parts . videoRec ; const msg = new DataView ( new ArrayBuffer ( p . length + payload . length ) ) ; msg . setUint32 ( 0 , prot . ids . videoRec , true ) ; msg . setUint32 ( p . cmd , cmd , true ) ; new Uint8Array ( msg . buffer ) . set ( new Uint8Array ( payload . buffer ) , p . length ) ; sendMsg ( new Uint8Array ( msg . buffer ) , peer ) ; } function closeRTC ( peer ) { if ( ! ( peer in jitsiPeers ) ) return ; const inc = jitsiPeers [ peer ] ; if ( inc . video ) jitsiTrackRemoved ( inc . video ) ; if ( inc . audio ) jitsiTrackRemoved ( inc . audio ) ; delete jitsiPeers [ peer ] ; } util . events . addEventListener ( "net.info." + prot . info . peerLost , function ( ev ) { if ( config . useRTC ) closeRTC ( ev . detail . val ) ; } ) ; export function speech ( status , peer ? ) { if ( ! config . useRTC ) return ; const p = prot . parts . speech ; const msgv = new DataView ( new ArrayBuffer ( p . length ) ) ; msgv . setUint32 ( 0 , prot . ids . speech , true ) ; msgv . setUint32 ( p . indexStatus , status ? 1 : 0 , true ) ; const msg = new Uint8Array ( msgv . buffer ) ; sendMsg ( msg , peer ) ; } util . events . addEventListener ( "ui.speech" , function ( ev ) { if ( ev . detail . user === null ) speech ( ev . detail . status ) ; } ) ; let lastCaption = "" ; function caption ( complete , text ) { if ( ! config . useRTC ) return ; let append = false ; if ( lastCaption && text . slice ( 0 , lastCaption . length ) === lastCaption ) { append = true ; const newText = text . slice ( lastCaption . length ) ; lastCaption = text ; text = newText ; } else { lastCaption = complete ? "" : text ; } if ( text === "" ) return ; const textBuf = util . encodeText ( text ) ; const p = prot . parts . caption . cc ; const msg = new DataView ( new ArrayBuffer ( p . length + textBuf . length ) ) ; msg . setUint32 ( 0 , prot . ids . caption , true ) ; msg . setUint8 ( p . append , + append ) ; msg . setUint8 ( p . complete , + complete ) ; ( new Uint8Array ( msg . buffer ) ) . set ( textBuf , p . text ) ; sendMsg ( new Uint8Array ( msg . buffer ) ) ; } util . events . addEventListener ( "proc.caption" , function ( ev ) { caption ( ev . detail . complete , ev . detail . result . text || ev . detail . result . partial ) ; } ) ; export function videoDataSend ( peer , idx , buf ) { for ( let start = 0 ; start < buf . length ; start += 16380 ) { const part = buf . subarray ( start , start + 16380 ) ; const msg = new DataView ( new ArrayBuffer ( 12 + part . length ) ) ; msg . setUint32 ( 0 , prot . ids . data , true ) ; msg . setFloat64 ( 4 , idx + start , true ) ; new Uint8Array ( msg . buffer ) . set ( part , 12 ) ; sendMsg ( new Uint8Array ( msg . buffer ) , peer ) ; } } function setMajor ( peer ) { if ( ! ( peer in jitsiPeers ) || ! room ) return ; const jid = jitsiPeers [ peer ] . id ; if ( peer < 0 ) { room . setReceiverConstraints ( { lastN : 64 , selectedEndpoints : [ ] , onStageEndpoints : [ ] , defaultConstraints : { maxHeight : 1080 } , constraints : { } } ) ; } else { const constraints = { } ; constraints [ jid ] = { maxHeight : 1080 } ; room . setReceiverConstraints ( { lastN : 64 , selectedEndpoints : [ jid ] , onStageEndpoints : [ jid ] , defaultConstraints : { maxHeight : 360 } , constraints : constraints } ) ; } } util . events . addEventListener ( "ui.video.major" , function ( ) { setMajor ( ui . ui . video . major ) ; } ) ; function sendRTCNegotiation ( peer , cont ) { const p = prot . parts . rtc ; const contU8 = util . encodeText ( JSON . stringify ( cont ) ) ; const msg = new DataView ( new ArrayBuffer ( p . length + contU8 . length ) ) ; msg . setUint32 ( 0 , prot . ids . rtc , true ) ; msg . setUint32 ( p . peer , peer , true ) ; ( new Uint8Array ( msg . buffer ) ) . set ( contU8 , p . value ) ; net . dataSock . send ( msg . buffer ) ; } function startRTC ( id , j ) { const polite = ( net . selfId > id ) ; j . rtc = new RTCPeerConnection ( { iceServers : net . iceServers } ) ; j . rtc . ondatachannel = function ( ev ) { const data = ev . channel ; data . binaryType = "arraybuffer" ; data . onmessage = function ( ev ) { const msg = new DataView ( ev . data ) ; peerMessage ( id , msg ) ; } ; } ; j . rtc . onnegotiationneeded = onnegotiationneeded ; function onnegotiationneeded ( ) { j . rtc . createOffer ( ) . then ( offer => { return j . rtc . setLocalDescription ( offer ) ; } ) . then ( ( ) => { sendRTCNegotiation ( id , { desc : j . rtc . localDescription } ) ; } ) . catch ( ( ) => { } ) ; } j . rtc . onicecandidate = function ( ev ) { sendRTCNegotiation ( id , { cand : ev . candidate } ) ; } ; j . signal = function ( msg ) { if ( msg . desc ) { const desc = msg . desc ; let rollbackLocal = false ; if ( desc . type === "offer" && j . rtc . signalingState !== "stable" ) { if ( ! polite ) return ; rollbackLocal = true ; } return Promise . all ( [ ] ) . then ( ( ) => { if ( rollbackLocal ) return j . rtc . setLocalDescription ( { type : "rollback" } ) ; } ) . then ( ( ) => { return j . rtc . setRemoteDescription ( desc ) ; } ) . then ( ( ) => { if ( desc . type === "offer" ) { return j . rtc . createAnswer ( ) . then ( answer => { return j . rtc . setLocalDescription ( answer ) ; } ) . then ( ( ) => { sendRTCNegotiation ( id , { desc : j . rtc . localDescription } ) ; } ) ; } } ) . catch ( console . error ) ; } else if ( msg . cand ) { j . rtc . addIceCandidate ( msg . cand ) . catch ( ( ) => { } ) ; } } ; j . data = j . rtc . createDataChannel ( "ennuicastr" ) ; j . data . onopen = function ( ) { j . rtcReady = true ; } ; j . data . onclose = j . data . onerror = function ( ) { j . rtcReady = false ; if ( jitsiPeers [ id ] === j && ! j . audio && ! j . video ) { delete jitsiPeers [ id ] ; } } ; onnegotiationneeded ( ) ; } util . events . addEventListener ( "net.dataSock." + prot . ids . rtc , function ( ev ) { if ( config . useRTC ) { const p = prot . parts . rtc ; const peer = ev . detail . getUint32 ( p . peer , true ) ; if ( ! ( peer in jitsiPeers ) ) return ; const j = jitsiPeers [ peer ] ; if ( ! j . signal ) return ; try { const tmsg = util . decodeText ( ( new Uint8Array ( ev . detail . buffer ) ) . subarray ( p . value ) ) ; const msg = JSON . parse ( tmsg ) ; j . signal ( msg ) ; } catch ( ex ) { } } } ) ;