openreplay/tracker/tracker-assist/src/CallWindow.ts
Andrey Babushkin 7365d8639c
updated widget link (#3158)
* updated widget link

* fix calls

* updated widget url
2025-03-18 11:07:09 +01:00

329 lines
No EOL
9.6 KiB
TypeScript

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<typeof setInterval>
private remoteVideo: MediaStreamTrack
private readonly load: Promise<void>
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<typeof setInterval>
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<string, string>) {
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)
}
}
}