import * as config from "./config" ; import * as net from "./net" ; import { prot } from "./protocol" ; import * as util from "./util" ; import { dce } from "./util" ; export enum ViewMode { Normal = 0 , Small , Gallery , Studio } interface Panel { wrapper : HTMLElement ; onshow ? : ( ) => void ; onhide ? : ( ) => void ; } let curPanel = null ; let modal = null ; export const ui = { manualSize : false , resizing : < null | number > null , resized : false , outerInnerHeightDiff : 0 , outprocSetPerUserGain : < ( target : number , gain : number ) => unknown > null , masterUserAdmin : < ( target : number ) => unknown > null , wrapper : < HTMLElement > null , colors : < Record < string , string > > { } , video : < { wrapper : HTMLElement ; window : WindowProxy ; sideWrapper : HTMLElement ; side : HTMLElement ; sideFS : HTMLButtonElement ; mainWrapper : HTMLElement ; main : HTMLElement ; mainFS : HTMLButtonElement ; users : { boxA : HTMLElement ; boxB : HTMLElement ; video : HTMLVideoElement ; audio : HTMLAudioElement ; standin : HTMLElement ; name : HTMLElement ; caption : HTMLElement ; captions : { div : HTMLElement ; span : HTMLElement ; complete : boolean ; timeout : number ; } [ ] ; popout : HTMLButtonElement ; admin : HTMLButtonElement ; waveformWrapper : HTMLElement ; } [ ] ; selected : number ; major : number ; mode : ViewMode ; css : HTMLStyleElement ; } > null , chat : < { wrapper : HTMLElement ; incoming : HTMLElement ; outgoing : HTMLInputElement ; outgoingB : HTMLButtonElement ; } > null , dock : < HTMLElement > null , wave : < { wrapper : HTMLElement ; watcher : HTMLImageElement ; } > null , log : < { wrapper : HTMLElement ; logWrapper : HTMLElement ; log : HTMLElement ; timer : HTMLElement ; } > null , persistent : < { master : HTMLButtonElement ; userAdmin : HTMLButtonElement ; sounds : HTMLButtonElement ; masterSpacer : HTMLElement ; mute : HTMLButtonElement ; camera : HTMLButtonElement ; shareScreen : HTMLButtonElement ; main : HTMLButtonElement ; chat : HTMLButtonElement ; videoPopout : HTMLButtonElement ; } > null , layerSeparator : < HTMLElement > null , panels : { mobile : < { wrapper : HTMLElement ; button : HTMLButtonElement ; } > null , main : < { wrapper : HTMLElement ; modeHider : HTMLElement ; modeS : HTMLSelectElement ; captionHider : HTMLElement ; captionC : HTMLInputElement ; inputB : HTMLButtonElement ; outputB : HTMLButtonElement ; videoB : HTMLButtonElement ; userListB : HTMLButtonElement ; debug : HTMLButtonElement ; } > null , master : < { wrapper : HTMLElement ; mainPopout : HTMLButtonElement ; mainPopoutWrapper : HTMLElement ; mainPopoutDock : HTMLElement ; mainDock : HTMLElement ; pauseResumeB : HTMLButtonElement ; startStopB : HTMLButtonElement ; yesNo : HTMLElement ; yesB : HTMLButtonElement ; noB : HTMLButtonElement ; inviteLink : HTMLInputElement ; inviteCopyB : HTMLButtonElement ; inviteFLACHider : HTMLElement ; inviteFLAC : HTMLInputElement ; inviteContinuousHider : HTMLElement ; inviteContinuous : HTMLInputElement ; acceptRemoteVideo : HTMLInputElement ; saveVideoInBrowser : HTMLInputElement ; downloadVideoLive : HTMLInputElement ; videoStatus : HTMLElement ; recordingCostPopout : HTMLButtonElement ; recordingCostPopoutWrapper : HTMLElement ; recordingCostDock : HTMLElement ; recordingCost : HTMLInputElement ; recordingRate : HTMLInputElement ; } > null , userAdmin : < { wrapper : HTMLElement ; allB : HTMLButtonElement ; buttons : HTMLButtonElement [ ] ; } > null , userAdminUser : < { wrapper : HTMLElement ; user : number ; name : HTMLElement ; kick : HTMLButtonElement ; mute : HTMLButtonElement ; echo : HTMLButtonElement ; reqFull : HTMLButtonElement ; } > null , userAdminFull : < { wrapper : HTMLElement ; user : number ; name : HTMLElement ; kick : HTMLButtonElement ; mute : HTMLInputElement ; echo : HTMLInputElement ; vadSensitivity : HTMLInputElement ; vadSensitivityStatus : HTMLElement ; vadNoiseGate : HTMLInputElement ; vadNoiseGateStatus : HTMLElement ; audioInput : HTMLSelectElement ; videoHider : HTMLElement ; videoInput : HTMLSelectElement ; videoRes : HTMLSelectElement ; videoBR : HTMLInputElement ; videoRec : HTMLButtonElement ; } > null , userAdminReq : < { wrapper : HTMLElement ; user : number ; name : HTMLElement ; yes : HTMLButtonElement ; audio : HTMLButtonElement ; no : HTMLButtonElement ; } > null , soundboard : < { wrapper : HTMLElement ; popout : HTMLButtonElement ; popoutWrapper : HTMLElement ; dock : HTMLElement ; soundsWrapper : HTMLElement ; sounds : Record < string , { b : HTMLButtonElement ; i : HTMLElement ; n : HTMLElement ; } > ; } > null , inputConfig : < { wrapper : HTMLElement ; device : HTMLSelectElement ; ptt : HTMLButtonElement ; noiserHider : HTMLElement ; noiser : HTMLInputElement ; echo : HTMLInputElement ; agcHider : HTMLElement ; agc : HTMLInputElement ; vadSensitivity : HTMLInputElement ; vadSensitivityStatus : HTMLElement ; vadNoiseGate : HTMLInputElement ; vadNoiseGateStatus : HTMLElement ; } > null , outputConfig : < { wrapper : HTMLElement ; deviceHider : HTMLElement ; device : HTMLSelectElement ; volumeHider : HTMLElement ; volume : HTMLInputElement ; volumeStatus : HTMLElement ; sfxVolumeHider : HTMLElement ; sfxVolume : HTMLInputElement ; sfxVolumeStatus : HTMLElement ; compressionHider : HTMLElement ; compression : HTMLInputElement ; muteInterface : HTMLInputElement ; } > null , videoConfig : < { wrapper : HTMLElement ; visible : boolean ; onshow : ( ) => void ; onhide : ( ) => void ; preview : HTMLElement ; previewS : MediaStream ; previewV : HTMLVideoElement ; device : HTMLSelectElement ; shareB : HTMLButtonElement ; res : HTMLSelectElement ; outputHider : HTMLElement ; recording : { hider : HTMLElement ; record : HTMLInputElement ; optHider : HTMLElement ; remote : HTMLInputElement ; local : HTMLInputElement ; manualBitrate : HTMLInputElement ; bitrateHider : HTMLInputElement ; bitrate : HTMLInputElement ; } ; streamerModeHider : HTMLElement ; streamerMode : HTMLInputElement ; } > null , userList : < { wrapper : HTMLElement ; popout : HTMLButtonElement ; popoutWrapper : HTMLElement ; dock : HTMLElement ; userList : HTMLElement ; users : { wrapper : HTMLElement ; name : HTMLElement ; volume : HTMLInputElement ; volumeStatus : HTMLElement ; } [ ] ; } > null , debug : < { wrapper : HTMLElement ; input : HTMLInputElement ; output : HTMLElement ; } > null } , sounds : < { chimeUp : HTMLAudioElement ; chimeDown : HTMLAudioElement ; soundboard : Record < string , { el : HTMLAudioElement & { ecStartTime ? : number ; } ; } > ; } > null , audioOutput : < HTMLAudioElement > null } ; const lastSpeech = [ ] ; const standinSVG = [ '<svg viewBox="0 0 512 512" style="width:100%;height:100%"><g transform="translate(0,215)"><rect ry="128" rx="128" y="-215" x="0" height="512" width="512" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66518928px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' , '<svg viewBox="0 0 640 512" style="width:100%;height:100%"><g transform="translate(64,215)"><path transform="matrix(1.3149081,0,0,1.1060609,-80.616476,-4.348493)" d="m 256.00001,-190.45201 243.36301,176.81359 -92.95641,286.09039 -300.81323,-1e-5 -92.956399,-286.090393 z" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66519165px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' , '<svg viewBox="0 0 512 512" style="width:100%;height:100%"><g transform="translate(0,215)"><path transform="matrix(1.1552103,0,0,1.0004415,-39.733837,-24.463922)" d="m 256.00001,-190.45201 221.60466,127.943517 -10e-6,255.887023 -221.60467,127.94351 -221.604657,-127.94352 7e-6,-255.887025 z" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66518928px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' , '<svg viewBox="0 0 576 512" style="width:100%;height:100%"><g transform="translate(32,215)"><path transform="matrix(1.1544409,0,0,1.0525596,-39.536869,-14.537931)" d="M 256.00001,-190.45201 456.06054,-94.107932 505.4714,122.37524 367.02521,295.98126 144.97478,295.98125 6.5285965,122.37523 55.939473,-94.107942 Z" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66519165px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' , '<svg viewBox="0 0 640 512" style="width:100%;height:100%"><g transform="translate(64,215)"><path transform="matrix(1.25,0,0,1,-64,0)" d="M 256.00001,-215.00002 437.01934,-140.01935 512,40.999988 437.01933,222.01932 255.99999,296.99998 74.980659,222.01931 0,40.999974 74.980669,-140.01936 Z" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66519165px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' , '<svg viewBox="0 0 576 512" style="width:100%;height:100%"><g transform="translate(32,215)"><path transform="matrix(1.1423549,0,0,1.0310912,-36.442856,6.6846075)" d="m 256.00001,-215.00002 164.55362,59.89263 87.55716,151.6534442 -30.40829,172.4539358 -134.14535,112.5613 -175.11431,0 L 34.297493,168.99997 3.8892164,-3.4539593 91.446377,-155.1074 Z" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66519165px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' , '<svg viewBox="0 0 544 512" style="width:100%;height:100%"><g transform="translate(16,215)"><path transform="matrix(1.1171786,0,0,1,-29.997722,0)" d="m 256.00001,-215.00002 150.47302,48.89165 92.99744,128.000007 0,158.216703 -92.99745,128 -150.47303,48.89164 -150.47302,-48.89165 -92.99744,-128.00001 4e-6,-158.216696 92.997446,-127.999994 z" style="opacity:0.5;fill:#ffffff" /><text style="font-size:298.66519165px;font-family:\'Noto Sans\',sans-serif;text-align:center;text-anchor:middle" y="149.86458" x="256">##</text></g></svg>' ] ; export function showPanel ( panelName , autoFocusName , makeModal ? ) { let panel ; let autoFocus = null ; if ( typeof autoFocusName === "string" ) autoFocus = ( < any > ui . panels ) [ < string > panelName ] [ autoFocusName ] ; else if ( typeof autoFocus !== "undefined" ) autoFocus = autoFocusName ; if ( typeof panelName === "string" ) panel = ( < any > ui . panels ) [ panelName ] ; else panel = panelName ; if ( modal && panel !== modal ) return ; modal = makeModal ? panel : null ; for ( const o in ui . panels ) { ( < any > ui . panels ) [ o ] . wrapper . style . display = "none" ; } if ( curPanel ) { if ( curPanel . onhide ) curPanel . onhide ( ) ; curPanel = null ; } if ( panel ) { ui . layerSeparator . style . display = "" ; panel . wrapper . style . display = "block" ; document . body . setAttribute ( "data-interface" , "none" ) ; if ( autoFocus ) autoFocus . focus ( ) ; else ( < HTMLElement > panel . wrapper . childNodes [ 0 ] ) . focus ( ) ; curPanel = panel ; if ( panel . onshow ) panel . onshow ( ) ; } else { ui . layerSeparator . style . display = "none" ; mouseenter ( ) ; if ( autoFocus ) autoFocus . focus ( ) ; } resizeUI ( ) ; } export function unsetModal ( ) { modal = null ; } let metimeout = null ; export function mouseenter ( ) { if ( metimeout ) clearTimeout ( metimeout ) ; document . body . setAttribute ( "data-interface" , "show" ) ; metimeout = setTimeout ( function ( ) { if ( document . body . getAttribute ( "data-interface" ) === "show" ) document . body . setAttribute ( "data-interface" , "hide" ) ; } , 2000 ) ; } export function saveConfigValue ( sel , name , onchange ? ) { const cur = localStorage . getItem ( name ) ; if ( cur !== null ) sel . value = cur ; sel . onchange = function ( ev ) { localStorage . setItem ( name , sel . value ) ; if ( onchange ) return onchange ( ev ) ; } ; } export function saveConfigCheckbox ( cb , name , onchange ? ) { const cur = localStorage . getItem ( name ) ; if ( cur !== null ) cb . checked = ! ! ~ ~ cur ; cb . onchange = function ( ev ) { localStorage . setItem ( name , cb . checked ? "1" : "0" ) ; if ( onchange ) return onchange ( ev ) ; } ; } export function saveConfigSlider ( sl , name , onchange ? , oninput ? ) { const cur = localStorage . getItem ( name ) ; if ( cur !== null ) sl . value = "" + ( + cur ) ; if ( oninput ) sl . oninput = oninput ; sl . onchange = function ( ev ) { let ret ; if ( onchange ) ret = onchange ( ev ) ; localStorage . setItem ( name , "" + sl . value ) ; return ret ; } ; } export function resizeUI ( second ? ) { if ( ! second ) setTimeout ( function ( ) { resizeUI ( true ) ; } , 0 ) ; if ( ui . video . mode === ViewMode . Small && ui . chat . wrapper . style . display === "none" ) { ui . video . wrapper . style . display = "none" ; ui . wave . wrapper . style . flex = "auto" ; } else { ui . video . wrapper . style . display = "" ; ui . wave . wrapper . style . flex = "" ; } let idealSize = 0 ; for ( let ci = 0 ; ci < ui . wrapper . childNodes . length ; ci ++ ) { const c = < HTMLElement > ui . wrapper . childNodes [ ci ] ; if ( c . style . display === "none" ) continue ; if ( c === ui . video . wrapper ) { if ( ui . video . mode === ViewMode . Studio ) { let vsize = ui . video . side . childNodes . length * 96 ; if ( vsize > 240 ) vsize = 240 ; idealSize += vsize ; } else { idealSize += 240 ; } } else if ( c === ui . wave . wrapper && ui . video . mode !== ViewMode . Studio ) { idealSize += 100 ; } else { idealSize += c . offsetHeight ; } } for ( const pn in ui . panels ) { const panel = ( < any > ui . panels ) [ pn ] . wrapper ; if ( panel . style . display === "block" ) idealSize = Math . max ( idealSize , panel . scrollHeight + 60 ) ; } if ( window . innerHeight === idealSize ) return ; if ( ui . manualSize && window . innerHeight >= idealSize ) return ; if ( ! ui . resizing ) ui . outerInnerHeightDiff = window . outerHeight - window . innerHeight ; if ( ui . resizing ) clearTimeout ( ui . resizing ) ; ui . resized = false ; ui . resizing = setTimeout ( function ( ) { ui . resizing = null ; if ( ! ui . resized ) onResize ( ) ; } , 500 ) ; window . resizeTo ( window . outerWidth , ui . outerInnerHeightDiff + idealSize ) ; } util . events . addEventListener ( "ui.resize-needed" , function ( ) { resizeUI ( ) ; } ) ; export function onResize ( ) { ui . resized = true ; ui . manualSize = ! ui . resizing ; ui . wrapper . style . height = window . innerHeight + "px" ; if ( ui . video . mode === ViewMode . Gallery ) updateVideoUI ( 0 ) ; } export function userListAdd ( idx , name , fromMaster ) { if ( ( "master" in config . config ) !== fromMaster ) return ; const userList = ui . panels . userList ; while ( userList . users . length <= idx ) userList . users . push ( null ) ; let user = userList . users [ idx ] ; if ( user ) { if ( name ) { user . name . innerText = name ; user . name . setAttribute ( "aria-label" , name + ": Not speaking" ) ; ui . video . users [ idx ] . name . innerText = name ; styleVideoEl ( ui . video . users [ idx ] , name ) ; } return ; } user = userList . users [ idx ] = { wrapper : dce ( "div" ) , name : dce ( "div" ) , volume : dce ( "input" ) , volumeStatus : dce ( "div" ) } ; const volumeWrapper = dce ( "div" ) ; user . wrapper . classList . add ( "bigrflex" , "row" ) ; userList . userList . appendChild ( user . wrapper ) ; user . name . classList . add ( "half" ) ; user . name . style . backgroundColor = ui . colors [ "user-list-silent" ] ; user . name . innerText = name ; user . name . setAttribute ( "role" , "note" ) ; user . name . setAttribute ( "aria-label" , name + ": Not speaking" ) ; user . wrapper . appendChild ( user . name ) ; volumeWrapper . classList . add ( "rflex" , "half" ) ; user . wrapper . appendChild ( volumeWrapper ) ; user . volume . style . flex = "auto" ; user . volume . style . width = "6em" ; Object . assign ( user . volume , { type : "range" , min : 0 , max : 400 , value : 100 } ) ; user . volume . setAttribute ( "aria-label" , "Volume for " + name ) ; volumeWrapper . appendChild ( user . volume ) ; user . volumeStatus . innerHTML = "&nbsp;100%" ; volumeWrapper . appendChild ( user . volumeStatus ) ; let mousing = false ; function volChange ( ) { const vol = user . volume ; if ( mousing ) { for ( let i = 100 ; i <= 300 ; i += 100 ) if ( + vol . value >= i - 10 && + vol . value <= i + 10 ) vol . value = "" + i ; } user . volumeStatus . innerHTML = "&nbsp;" + vol . value + "%" ; ui . outprocSetPerUserGain ( idx , ( + vol . value ) / 100 ) ; } user . volume . onmousedown = function ( ) { mousing = true ; } ; user . volume . onmouseup = function ( ) { mousing = false ; } ; saveConfigSlider ( user . volume , "user-volume3-" + name , volChange ) ; volChange ( ) ; videoAdd ( idx , name ) ; updateVideoUI ( idx , false , fromMaster ) ; if ( ! ui . panels . outputConfig . muteInterface . checked ) ui . sounds . chimeUp . play ( ) . catch ( console . error ) ; } util . events . addEventListener ( "net.info." + prot . info . id , function ( ev ) { const val = ev . detail . val ; userListAdd ( val , config . username , false ) ; } ) ; export function videoAdd ( idx , name ) { if ( ui . video . users [ idx ] ) return ; while ( ui . video . users . length <= idx ) ui . video . users . push ( null ) ; const ctx = ui . video . users [ idx ] = { boxA : dce ( "div" ) , boxB : dce ( "div" ) , video : dce ( "video" ) , audio : dce ( "audio" ) , standin : dce ( "div" ) , name : dce ( "span" ) , caption : dce ( "div" ) , captions : < any [ ] > [ ] , popout : dce ( "button" ) , admin : < HTMLButtonElement > null , waveformWrapper : dce ( "div" ) } ; const boxA = ctx . boxA ; boxA . classList . add ( "ecvideo-a" ) ; const box = ctx . boxB ; box . classList . add ( "ecvideo" ) ; box . style . border = "4px solid " + ui . colors [ "video-silent" ] ; boxA . appendChild ( box ) ; const video = ctx . video ; video . height = 0 ; video . muted = true ; video . autoplay = true ; Object . assign ( video . style , { position : "absolute" , left : "0" , top : "0" , width : "100%" , height : "100%" } ) ; box . appendChild ( video ) ; const audio = ctx . audio ; audio . muted = true ; audio . autoplay = true ; audio . style . display = "none" ; box . appendChild ( audio ) ; video . onclick = function ( ) { if ( ui . video . selected === idx ) ui . video . selected = - 1 ; else ui . video . selected = idx ; updateVideoUI ( idx ) ; } ; const standin = ctx . standin ; Object . assign ( standin . style , { position : "absolute" , left : "8px" , top : "8px" , right : "8px" , bottom : "8px" , cursor : "default" } ) ; box . appendChild ( standin ) ; standin . onclick = video . onclick ; styleVideoEl ( ctx , name ) ; const nspan = ctx . name ; nspan . classList . add ( "namelabel" ) ; nspan . innerText = name || "" ; nspan . setAttribute ( "role" , "note" ) ; nspan . setAttribute ( "aria-label" , nspan . innerText + ": Not speaking" ) ; box . appendChild ( nspan ) ; const caption = ctx . caption ; caption . classList . add ( "caption" ) ; box . appendChild ( caption ) ; const popout = ctx . popout ; popout . classList . add ( "pobutton" , "tbutton" , "interface" , "streamer-interface" ) ; popout . innerHTML = '<i class="fa fa-window-restore"></i>' ; popout . title = "Pop out" ; popout . setAttribute ( "aria-label" , "Pop out" ) ; box . appendChild ( popout ) ; let w = null ; function popoutOpen ( ) { let width = 800 , height = 450 ; if ( video . srcObject ) { const vt = video . srcObject . getVideoTracks ( ) ; if ( vt && vt . length ) { const s = vt [ 0 ] . getSettings ( ) ; width = s . width ; height = s . height ; } } w = window . open ( "" , "" , "width=" + width + ",height=" + height + ",menubar=0,toolbar=0,location=0,personalbar=0,status=0" ) ; if ( ! w ) return ; w . document . title = nspan . innerText ; w . document . body . appendChild ( video ) ; w . onunload = popoutClose ; video . play ( ) . catch ( console . error ) ; } function popoutClose ( ) { w = null ; box . insertBefore ( video , box . childNodes [ 0 ] ) ; video . play ( ) . catch ( console . error ) ; } popout . onclick = function ( ) { if ( w ) w . close ( ) ; else popoutOpen ( ) ; } ; if ( "master" in config . config ) { const admin = ctx . admin = dce ( "button" ) ; admin . classList . add ( "ecstudio-admin-button" ) ; admin . innerHTML = '<i class="fas fa-user-cog"></i>' ; admin . title = "Administrate " + name ; admin . setAttribute ( "aria-label" , "Administrate " + name ) ; admin . style . height = "100%" ; admin . onclick = function ( ) { ui . masterUserAdmin ( idx ) ; } ; boxA . appendChild ( admin ) ; } const waveformWrapper = ctx . waveformWrapper ; waveformWrapper . classList . add ( "ecvideo-waveform" ) ; boxA . appendChild ( waveformWrapper ) ; } function styleVideoEl ( ctx , name ) { if ( ! name ) return ; let x = parseInt ( btoa ( unescape ( encodeURIComponent ( name . slice ( - 6 ) ) ) ) . replace ( /[^A-Za-z0-9]/g , "" ) , 36 ) ; const r = x % 4 ; x = Math . floor ( x / 4 ) ; const g = x % 4 ; x = Math . floor ( x / 4 ) ; const b = x % 4 ; x = Math . floor ( x / 4 ) ; const s = x % standinSVG . length ; ctx . video . style . backgroundColor = ctx . boxA . style . backgroundColor = "#" + r + g + b ; ctx . standin . innerHTML = standinSVG [ s ] . replace ( "##" , genStandinName ( name || "" ) ) ; } export function userListRemove ( idx , fromMaster ) { if ( ( "master" in config . config ) !== fromMaster ) return ; const user = ui . panels . userList . users [ idx ] ; if ( ! user ) return ; user . wrapper . parentNode . removeChild ( user . wrapper ) ; ui . panels . userList . users [ idx ] = null ; updateVideoUI ( idx , false , fromMaster ) ; if ( ! ui . panels . outputConfig . muteInterface . checked ) ui . sounds . chimeDown . play ( ) . catch ( console . error ) ; } util . netEvent ( "data" , "user" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . user ; const index = msg . getUint32 ( p . index , true ) ; const status = msg . getUint32 ( p . status , true ) ; const nick = util . decodeText ( msg . buffer . slice ( p . nick ) ) ; if ( status ) userListAdd ( index , nick , false ) ; else userListRemove ( index , false ) ; } ) ; export function userListUpdate ( idx , speaking , fromMaster ) { if ( ! fromMaster ) { const user = ui . panels . userList . users [ idx ] ; if ( ! user ) return ; user . name . style . backgroundColor = ui . colors [ "user-list-" + ( speaking ? "speaking" : "silent" ) ] ; user . name . setAttribute ( "aria-label" , user . name . innerText + ": " + ( speaking ? "Speaking" : "Not speaking" ) ) ; } if ( ( "master" in config . config ) !== fromMaster ) return ; updateVideoUI ( idx , speaking , fromMaster ) ; } util . events . addEventListener ( "ui.speech" , function ( ev ) { let user = ev . detail . user ; const status = ev . detail . status ; if ( user === null ) user = net . selfId ; userListUpdate ( user , status , false ) ; } ) ; util . netEvent ( "data" , "speech" , function ( ev ) { if ( ! config . useRTC ) { const msg = ev . detail ; const p = prot . parts . speech ; const indexStatus = msg . getUint32 ( p . indexStatus , true ) ; const index = indexStatus >>> 1 ; const status = ( indexStatus & 1 ) ; userListUpdate ( index , ! ! status , false ) ; } } ) ; export function updateVideoUI ( peer , speaking ? , fromMaster ? ) { const ctx = ui . video . users [ peer ] ; const users = ui . panels . userList . users ; const user = users [ peer ] ; const prevMajor = ui . video . major ; const gallery = ( ui . video . mode === ViewMode . Gallery ) ; const studio = ( ui . video . mode === ViewMode . Studio ) ; while ( lastSpeech . length <= peer ) lastSpeech . push ( null ) ; if ( typeof fromMaster !== "undefined" ) { if ( speaking ) lastSpeech [ peer ] = performance . now ( ) ; else lastSpeech [ peer ] = null ; let sw = "" ; if ( fromMaster ) { if ( speaking ) sw = "Transmitting" ; else sw = "Not transmitting" ; } else { if ( speaking ) sw = "Speaking" ; else sw = "Not speaking" ; } ctx . name . setAttribute ( "aria-label" , ctx . name . innerText + ": " + sw ) ; } if ( ! gallery && ! studio ) { if ( ! user ) { if ( ui . video . major === peer ) ui . video . major = - 1 ; if ( ui . video . selected === peer ) ui . video . selected = - 1 ; } if ( ui . video . selected !== - 1 ) { ui . video . major = ui . video . selected ; } else if ( ui . video . major === net . selfId || lastSpeech [ ui . video . major ] === null ) { let earliest = - 1 ; for ( let pi = 1 ; pi < users . length ; pi ++ ) { if ( users [ pi ] && ui . video . users [ pi ] && pi !== net . selfId && lastSpeech [ pi ] !== null && ( earliest === - 1 || lastSpeech [ pi ] < lastSpeech [ earliest ] ) ) earliest = pi ; } if ( earliest !== - 1 ) ui . video . major = earliest ; } if ( user ) { if ( ui . video . major === - 1 && peer !== net . selfId ) ui . video . major = peer ; } if ( ui . video . major === - 1 ) { for ( let pi = users . length - 1 ; pi >= 0 ; pi -- ) { if ( users [ pi ] && ui . video . users [ pi ] ) { ui . video . major = pi ; break ; } } } } else { ui . video . selected = ui . video . major = - 1 ; } util . dispatchEvent ( "ui.video.major" ) ; let moved = 0 ; for ( let pi = 0 ; pi < users . length ; pi ++ ) { const v = ui . video . users [ pi ] ; const u = users [ pi ] ; if ( ! v ) continue ; if ( u ) { const selected = ( ui . video . selected === pi ) ; if ( lastSpeech [ pi ] !== null ) v . boxB . style . borderColor = ui . colors [ "video-speaking" + ( selected ? "-sel" : "" ) ] ; else v . boxB . style . borderColor = ui . colors [ "video-silent" + ( selected ? "-sel" : "" ) ] ; } if ( ui . video . major === pi ) continue ; if ( ! u && v . boxA . parentNode ) { v . boxA . parentNode . removeChild ( v . boxA ) ; moved ++ ; } else if ( u && v . boxA . parentNode !== ui . video . side ) { ui . video . side . appendChild ( v . boxA ) ; moved ++ ; } } if ( gallery ) { const side = ui . video . side ; const total = side . childNodes . length ; let w = Math . round ( Math . sqrt ( ( 9 * side . offsetWidth * total ) / ( 16 * side . offsetHeight ) ) ) ; if ( w < 1 ) w = 1 ; const ew = Math . max ( ( 100 / w ) - 1 , 1 ) ; const mw = 100 / w ; ui . video . css . innerHTML = '[data-view-mode="gallery"] #ecvideo-side .ecvideo-a { flex: auto; width: ' + ew + '%; max-width: ' + mw + '%; }' ; } if ( studio && moved ) resizeUI ( ) ; if ( ui . video . major === prevMajor ) { return ; } ui . video . main . innerHTML = "" ; if ( ui . video . major !== - 1 ) { const v = ui . video . users [ ui . video . major ] ; ui . video . main . appendChild ( v . boxA ) ; } } function genStandinName ( name ) { name = name . replace ( /[&<>]/g , "" ) ; if ( name . length <= 2 ) return name ; const out = name [ 0 ] ; name = name . slice ( 1 ) ; let re ; try { re = RegExp ( "\\p{Lu}" , "gu" ) ; } catch ( ex ) { re = RegExp ( "[A-Z]" , "g" ) ; } const uc = name . match ( re ) ; if ( uc ) return out + uc [ uc . length - 1 ] ; return out + name [ 0 ] ; } export function caption ( peer , text , append , complete ) { const ctx = ui . video . users [ peer ] ; if ( ! ctx ) return ; let caption = null ; if ( ctx . captions . length ) { caption = ctx . captions [ ctx . captions . length - 1 ] ; if ( caption . complete ) caption = null ; } if ( caption ) clearTimeout ( caption . timeout ) ; if ( ! caption ) { caption = { div : dce ( "div" ) , span : dce ( "span" ) , complete : complete , timeout : null } ; ctx . caption . appendChild ( caption . div ) ; caption . div . appendChild ( caption . span ) ; ctx . captions . push ( caption ) ; } if ( append ) caption . span . innerText += text ; else caption . span . innerText = text ; caption . complete = complete ; caption . timeout = setTimeout ( function ( ) { for ( let i = 0 ; i < ctx . captions . length ; i ++ ) { const other = ctx . captions [ i ] ; if ( caption === other ) { ctx . captions . splice ( i , 1 ) ; break ; } } ctx . caption . removeChild ( caption . div ) ; } , 3000 ) ; }