import {MAP_SIZE} from '@components/Constants' import {connection} from '@models/api' import {ConnectionStates} from '@models/api/Constants' import {t} from '@models/locales' import {priorityCalculator} from '@models/middleware/trafficControl' import { defaultInformation } from '@models/Participant' import {urlParameters} from '@models/url' import {addV2, diffSet, mulV2} from '@models/utils' import {createJitisLocalTracksFromStream} from '@models/utils/jitsiTrack' import map from '@stores/Map' import participants from '@stores/participants/Participants' import {action, autorun, computed, makeObservable, observable, when} from 'mobx' export type ErrorType = '' | 'connection' | 'retry' | 'noMic' | 'micPermission' | 'channel' | 'entrance' | 'afk' | 'kicked' export class ErrorInfo { @computed get fatal() { return !this.type } @observable type:ErrorType = 'entrance' @observable types: Set<ErrorType> = new Set() @observable supressedTypes:Set<ErrorType> = new Set() reason? = '' name? = '' @action setType(type: ErrorType, name?:string, reason?:string){ this.type = type this.types.add(type) this.reason = reason this.name = name } @computed get title() { switch(this.type){ case 'connection': return t('etConnection') case 'retry': return t('etRetry') case 'noMic': return t('etNoMic') case 'micPermission': return t('etMicPermission') case 'channel': return t('etNoChannel') case 'entrance': return '' case 'afk': return t('afkTitle') case 'kicked': return `Kicked by ${this.name}. ${this.reason}` } return this.type } @computed get message() { switch(this.type){ case 'connection': return t('emConnection') case 'retry': return t('emRetry') case 'noMic': return t('emNoMic') case 'micPermission': return t('emMicPermission') case 'channel': return t('emNoChannel') case 'entrance': return '' case 'afk': return t('afkMessage') case 'kicked': return '' } return `no message defined for ${this.type}` } show(){ if (this.type === ''){ const types = diffSet(this.types, this.supressedTypes) if (types.size){ this.type = types.values().next().value } } return this.type!=='' && !this.supressedTypes.has(this.type) } constructor() { makeObservable(this) if (urlParameters['testBot'] !== null) { this.clear() } autorun(() => { if (this.show()) { map.keyInputUsers.add('errorDialog') }else { map.keyInputUsers.delete('errorDialog') } }) autorun(() => { if (participants.local.physics.awayFromKeyboard){ this.setType('afk') } }) } // media devices private videoInputs:MediaDeviceInfo[] = [] private audioInputs:MediaDeviceInfo[] = [] private audioOutputs:MediaDeviceInfo[] = [] private enumerateDevices() { navigator.mediaDevices.enumerateDevices().then((devices) => { this.videoInputs = [] this.audioInputs = [] this.audioOutputs = [] devices.forEach((device) => { if (device.kind === 'videoinput') { this.videoInputs.push(device) } if (device.kind === 'audioinput') { this.audioInputs.push(device) } if (device.kind === 'audiooutput') { this.audioOutputs.push(device) } }) }).catch(() => { console.error('Device enumeration error') }) } /// check errors after try to start the connection to the XMPP server. @action connectionStart() { // even when reload, user interaction is needed to play sound. // const nav = window?.performance?.getEntriesByType('navigation')[0] as any // console.log(nav) if (urlParameters.skipEntrance !== null/* || nav.type === 'reload'*/ ){ this.clear() participants.local.sendInformation() } this.enumerateDevices() if (urlParameters.testBot === null) { when(() => this.type === '', () => { setTimeout(this.checkConnection.bind(this), 4 * 1000) }) }else { // testBot setTimeout(this.startTestBot.bind(this), 3000) } } @action clear(type?: ErrorType) { if (this.type === 'afk'){ participants.local.physics.awayFromKeyboard = false } if (type){ this.types.delete(type) if (this.type === type) { this.type = '' } }else{ this.types.clear() this.type = '' } } @action checkConnection() { if (connection.state !== ConnectionStates.CONNECTED) { this.setType('connection') setTimeout(this.checkConnection.bind(this), 5 * 1000) }else { this.clear('connection') this.checkMic() } } @action checkMic() { if (participants.localId && !participants.local.muteAudio && !connection.conference.getLocalMicTrack()) { if (this.audioInputs.length) { this.setType('micPermission') // this.message += 'You have: ' // this.audioInputs.forEach((device) => { this.message += `[${device.deviceId} - ${device.label}]` }) }else { this.clear('micPermission') this.setType('noMic') } setTimeout(this.checkMic.bind(this), 5 * 1000) }else { if (participants.local.muteAudio){ setTimeout(this.checkMic.bind(this), 5 * 1000) } this.clear('noMic') this.clear('micPermission') this.checkRemote() } } checkRemote() { if (participants.remote.size > 0) { /* //console.log(stringify(connection.conference)) //console.log(stringify(d.chatRoom.xmpp.connection.jingle.sessions)) for(const sid in d.chatRoom.xmpp.connection.jingle.sessions){ const sess = d.chatRoom.xmpp.connection.jingle.sessions[sid] const pc = sess.peerconnection.peerconnection as RTCPeerConnection console.log(pc) console.log('currentLocal:', pc.currentLocalDescription?.sdp) console.log('currentRemote:', pc.currentRemoteDescription?.sdp) //console.log(pc.remoteDescription?.sdp) } */ setTimeout(this.checkChannel.bind(this), 3 * 1000) }else { setTimeout(this.checkRemote.bind(this), 1 * 1000) } } @action checkChannel() { if (participants.remote.size > 0) { if (!connection.conference._jitsiConference?.rtc._channel?.isOpen()) { this.setType('channel') setTimeout(this.checkChannel.bind(this), 5 * 1000) }else { this.clear('channel') } }else { this.checkRemote() } } // testBot canvas: HTMLCanvasElement|undefined = undefined oscillator: OscillatorNode|undefined = undefined startTestBot () { participants.local.muteAudio = false participants.local.muteVideo = false let counter = 0 // Create dummy audio const ctxA = new AudioContext() this.oscillator = ctxA.createOscillator() this.oscillator.type = 'triangle' // sine, square, sawtooth, triangle const destination = ctxA.createMediaStreamDestination() this.oscillator.connect(destination) this.oscillator.start() // Create dummy video this.canvas = document.createElement('canvas') const width = 240 const height = 240 this.canvas.style.width = `${width}px` this.canvas.style.height = `${height}px` const ctx = this.canvas.getContext('2d') const center = [Math.random() * MAP_SIZE / 2, (Math.random() - 0.5) * MAP_SIZE] const draw = () => { if (!ctx || !ctxA) { return } // update audio frequency also this.oscillator?.frequency.setValueAtTime(440 + counter % 440, ctxA.currentTime) // 440HzはA4(4番目のラ) // update camera image const colors = ['green', 'blue'] const nearest = priorityCalculator.tracksToAccept[0].length ? participants.remote.get(priorityCalculator.tracksToAccept[0][0].endpointId) : undefined if (nearest && !nearest?.tracks.avatarOk) { colors[0] = 'yellow' } if (nearest && nearest?.tracks.audio?.getTrack().muted) { colors[1] = 'red' } //if (priorityCalculator.tracksToAccept[0][0]?.track.getTrack()?.muted) { colors[0] = 'yellow' } //if (priorityCalculator.tracksToAccept[1][0]?.track.getTrack()?.muted) { colors[1] = 'red' } ctx.fillStyle = colors[0] ctx.beginPath() ctx.ellipse(width * 0.63, height * 0.33, width * 0.1, height * 0.4, counter / 20, 0, Math.PI * 2) ctx.fill() ctx.fillStyle = colors[1] ctx.beginPath() ctx.ellipse(width * 0.63, height * 0.33, width * 0.1, height * 0.4, -counter / 20, 0, Math.PI * 2) ctx.fill() counter += 1 } setInterval(draw, 1000 / 20) const chars = '01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijlkmnopqrstuvwxyz' const randChar = () => chars.substr(Math.floor(Math.random() * chars.length), 1) participants.local.information = defaultInformation participants.local.information.name = `testBot ${randChar()}${randChar()}${randChar()}` participants.local.sendInformation() participants.local.pose.position = center as [number, number] const move = () => { participants.local.pose.position = addV2(center, mulV2(100, [Math.cos(counter / 60), Math.sin(counter / 60)])) // Reload when not connected. // This does not work well: It causes unnecessary reload because WebSocket can often be reconnected. /* if (urlParameters.testBot !== null && connection.conference._jitsiConference?.room && !connection.conference._jitsiConference?.room.connected){ setTimeout(()=>{ if (!connection.conference._jitsiConference?.room.connected){ window.location.reload() // testBot will reload when channel is closed. } }, 20 * 1000) } */ } const win = window as any if (win.requestIdleCallback) { const moveTask = () => { // onIdle, wait 50ms and run move() setTimeout(() => { move() win.requestIdleCallback(moveTask) }, 50) } moveTask() }else { setInterval(move, 1000) } const vidoeStream = (this.canvas as any).captureStream(20) as MediaStream const audioStream = destination.stream const stream = new MediaStream() stream.addTrack(audioStream.getAudioTracks()[0]) stream.addTrack(vidoeStream.getVideoTracks()[0]) const tracks = createJitisLocalTracksFromStream(stream) connection.conference.setLocalCameraTrack(tracks[0]) connection.conference.setLocalMicTrack(tracks[1]) } } const errorInfo = new ErrorInfo() declare const d:any d.error = errorInfo export default errorInfo