import type { LocalStream, } from './LocalStream.js' import attachDND from './dnd.js' const SS_START_TS_KEY = '__openreplay_assist_call_start_ts' export default class CallWindow { private remoteVideoId: string private readonly iframe: HTMLIFrameElement private vRemote: HTMLVideoElement | null = null private vLocal: HTMLVideoElement | null = null private audioBtn: HTMLElement | null = null private videoBtn: HTMLElement | null = null private endCallBtn: HTMLElement | null = null private agentNameElem: HTMLElement | null = null private remoteStreamVideoContainerSample: HTMLElement | null = null private videoContainer: HTMLElement | null = null private vPlaceholder: HTMLElement | null = null private remoteControlContainer: HTMLElement | null = null private remoteControlEndBtn: HTMLElement | null = null private controlsContainer: HTMLElement | null = null private onToggleVideo: (args: any) => void private tsInterval: ReturnType private remoteVideo: MediaStreamTrack private readonly load: Promise constructor(private readonly logError: (...args: any[]) => void, private readonly callUITemplate?: string) { const iframe = (this.iframe = document.createElement('iframe')) Object.assign(iframe.style, { position: 'fixed', zIndex: 2147483647 - 1, border: 'none', bottom: '50px', right: '10px', height: '200px', width: '200px', }) // TODO: find the best attribute name for the ignoring iframes iframe.setAttribute('data-openreplay-obscured', '') iframe.setAttribute('data-openreplay-hidden', '') iframe.setAttribute('data-openreplay-ignore', '') document.body.appendChild(iframe) const doc = iframe.contentDocument if (!doc) { logError('OpenReplay: CallWindow iframe document is not reachable.') return } // const baseHref = "https://static.openreplay.com/tracker-assist/test" const baseHref = 'https://static.openreplay.com/tracker-assist/widget' // this.load = fetch(this.callUITemplate || baseHref + '/index2.html') this.load = fetch(this.callUITemplate || baseHref + '/index.html') .then((r) => r.text()) .then((text) => { iframe.onload = () => { const assistSection = doc.getElementById('or-assist') setTimeout(() => { assistSection?.classList.remove('status-connecting') }, 0) //iframe.style.height = doc.body.scrollHeight + 'px'; //iframe.style.width = doc.body.scrollWidth + 'px'; this.adjustIframeSize() iframe.onload = null } // ? text = text.replace(/href="css/g, `href="${baseHref}/css`) doc.open() doc.write(text) doc.close() this.vLocal = doc.getElementById('video-local') as HTMLVideoElement | null this.vRemote = doc.getElementById('video-remote') as HTMLVideoElement | null this.videoContainer = doc.getElementById('video-container') this.audioBtn = doc.getElementById('audio-btn') if (this.audioBtn) { this.audioBtn.onclick = () => this.toggleAudio() } this.videoBtn = doc.getElementById('video-btn') if (this.videoBtn) { this.videoBtn.onclick = () => this.toggleVideo() } this.endCallBtn = doc.getElementById('end-call-btn') this.agentNameElem = doc.getElementById('agent-name') this.vPlaceholder = doc.querySelector('#remote-stream p') this.remoteControlContainer = doc.getElementById('remote-control-row') this.remoteControlEndBtn = doc.getElementById('end-control-btn') this.controlsContainer = doc.getElementById('controls') if (this.controlsContainer) { this.controlsContainer.style.display = 'none' } const tsElem = doc.getElementById('duration') if (tsElem) { const startTs = Number(sessionStorage.getItem(SS_START_TS_KEY)) || Date.now() sessionStorage.setItem(SS_START_TS_KEY, startTs.toString()) this.tsInterval = setInterval(() => { const ellapsed = Date.now() - startTs const secsFull = ~~(ellapsed / 1000) const mins = ~~(secsFull / 60) const secs = secsFull - mins * 60 tsElem.innerText = `${mins > 0 ? `${mins}m` : ''}${secs < 10 ? 0 : ''}${secs}s` }, 500) } const dragArea = doc.querySelector('.drag-area') if (dragArea) { // TODO: save coordinates on the new page attachDND(iframe, dragArea, doc.documentElement) } setTimeout(() => { const assistSection = doc.getElementById('or-assist') assistSection?.classList.remove('status-connecting') this.adjustIframeSize() }, 250) }) //this.toggleVideoUI(false) //this.toggleRemoteVideoUI(false) } private adjustIframeSize() { const doc = this.iframe.contentDocument if (!doc) { return } this.iframe.style.height = `${doc.body.scrollHeight}px` this.iframe.style.width = `${doc.body.scrollWidth}px` } private checkRemoteVideoInterval: ReturnType private audioContainer: HTMLDivElement | null = null addRemoteStream(rStream: MediaStream, peerId: string) { this.load .then(() => { // Video if (this.vRemote && !this.vRemote.srcObject) { this.vRemote.srcObject = rStream this.remoteVideo = rStream.getVideoTracks()[0] this.remoteVideoId = peerId if (this.vPlaceholder) { this.vPlaceholder.innerText = 'Video has been paused. Click anywhere to resume.' } // Hack to determine if the remote video is enabled // TODO: pass this info through socket if (this.checkRemoteVideoInterval) { clearInterval(this.checkRemoteVideoInterval) } // just in case } // Audio if (!this.audioContainer) { this.audioContainer = document.createElement('div') document.body.appendChild(this.audioContainer) } // Hack for audio. Doesen't work inside the iframe // because of some magical reasons (check if it is connected to autoplay?) const audioEl = document.createElement('audio') audioEl.autoplay = true audioEl.style.display = 'none' audioEl.srcObject = rStream this.audioContainer.appendChild(audioEl) }) .catch((e) => this.logError(e)) } toggleRemoteVideoUI(enable: boolean) { this.load .then(() => { if (this.videoContainer) { if (enable) { this.videoContainer.classList.add('remote') } else { this.videoContainer.classList.remove('remote') } this.adjustIframeSize() } }) .catch((e) => this.logError(e)) } private localStreams: LocalStream[] = [] // !TODO: separate streams manipulation from ui setLocalStreams(streams: LocalStream[]) { this.localStreams = streams } playRemote() { this.vRemote && this.vRemote.play() } setAssistentName(callingAgents: Map) { this.load .then(() => { if (this.agentNameElem) { const nameString = Array.from(callingAgents.values()).join(', ') const safeNames = nameString.length > 20 ? nameString.substring(0, 20) + '...' : nameString this.agentNameElem.innerText = safeNames } }) .catch((e) => this.logError(e)) } private toggleAudioUI(enabled: boolean) { if (!this.audioBtn) { return } if (enabled) { this.audioBtn.classList.remove('muted') } else { this.audioBtn.classList.add('muted') } } private toggleAudio() { let enabled = false this.localStreams.forEach((stream) => { enabled = stream.toggleAudio() || false }) this.toggleAudioUI(enabled) } private toggleVideoUI(enabled: boolean) { if (!this.videoBtn || !this.videoContainer) { return } if (enabled) { this.videoContainer.classList.add('local') this.videoBtn.classList.remove('off') } else { this.videoContainer.classList.remove('local') this.videoBtn.classList.add('off') } this.adjustIframeSize() } private toggleVideo() { this.localStreams.forEach((stream) => { stream .toggleVideo() .then((enabled) => { this.onToggleVideo?.({ streamId: stream.stream.id, enabled, }) this.toggleVideoUI(enabled) this.load .then(() => { if (this.vLocal && stream && !this.vLocal.srcObject) { this.vLocal.srcObject = stream.stream } }) .catch((e) => this.logError(e)) }) .catch((e) => this.logError(e)) }) } public showRemoteControl(endControl: () => void) { this.load .then(() => { if (this.remoteControlContainer) { this.remoteControlContainer.style.display = 'flex' } if (this.remoteControlEndBtn) { this.remoteControlEndBtn.onclick = endControl } this.adjustIframeSize() }) .catch((e) => this.logError(e)) } public showControls(endCall: () => void) { this.load .then(() => { if (this.controlsContainer) { this.controlsContainer.style.display = 'unset' } if (this.endCallBtn) { this.endCallBtn.onclick = endCall } this.adjustIframeSize() }) .catch((e) => this.logError(e)) } public hideControls() { if (this.controlsContainer) { this.controlsContainer.style.display = 'none' } this.adjustIframeSize() } public hideRemoteControl() { if (this.remoteControlContainer) { this.remoteControlContainer.style.display = 'none' } this.adjustIframeSize() } public setVideoToggleCallback(cb) { this.onToggleVideo = cb } remove() { clearInterval(this.tsInterval) clearInterval(this.checkRemoteVideoInterval) if (this.audioContainer && this.audioContainer.parentElement) { this.audioContainer.parentElement.removeChild(this.audioContainer) this.audioContainer = null } if (this.iframe.parentElement) { this.iframe.parentElement.removeChild(this.iframe) } sessionStorage.removeItem(SS_START_TS_KEY) this.localStreams = [] } toggleVideoStream({ streamId, enabled, }: { streamId: string, enabled: boolean }) { if (this.remoteVideoId === streamId) { this.remoteVideo.enabled = enabled this.toggleRemoteVideoUI(enabled) } } }