removed conference calls

This commit is contained in:
Андрей Бабушкин 2025-02-26 13:04:23 +01:00
parent 67999f1373
commit 16b5cb0644
5 changed files with 45 additions and 346 deletions

View file

@ -142,8 +142,11 @@ function AssistActions({
});
};
const removeIncomeStream = () => {
setIncomeStream([]);
const removeIncomeStream = (stream: MediaStream) => {
setIncomeStream((prevState) => {
if (!prevState) return [];
return prevState.filter((existingStream) => existingStream.stream.id !== stream.id);
});
};
function call() {

View file

@ -335,7 +335,7 @@ export default class AssistManager {
/**
* Sends event ping to stats service
* */
public ping(event: StatsEvent, id: number) {
public ping(event: StatsEvent, id: string) {
this.socket?.emit(event, id);
}

View file

@ -46,13 +46,11 @@ export default class Call {
) {
socket.on('WEBRTC_AGENT_CALL', (data) => {
console.log("!WEBRTC AGENT CALL RECEIVED", data.type, Object.keys(this.connections));
switch (data.type) {
case WEBRTC_CALL_AGENT_EVENT_TYPES.OFFER:
this.handleOffer(data, true);
break;
case WEBRTC_CALL_AGENT_EVENT_TYPES.ICE_CANDIDATE:
console.log("#### RECEIVED ICE CANDIDATE", data);
this.handleIceCandidate(data);
break;
case WEBRTC_CALL_AGENT_EVENT_TYPES.ANSWER:
@ -63,7 +61,6 @@ export default class Call {
})
socket.on('UPDATE_SESSION', (data: { data: { agentIds: string[] }}) => {
console.log("UPDATE SESSION", data.data.agentIds);
this.callAgentsInSession({ agentIds: data.data.agentIds });
});
@ -104,12 +101,10 @@ export default class Call {
});
socket.on('webrtc_call_offer', (data: { data: { from: string, offer: RTCSessionDescriptionInit } }) => {
console.log("RECEIVED OFFER", data);
this.handleOffer(data.data);
});
socket.on('webrtc_call_answer', (data: { data: { from: string, answer: RTCSessionDescriptionInit } }) => {
console.log("ПРИШЕЛ оБЫЧНЫЙ сОКЕТ ANSWER", data.data);
this.handleAnswer(data.data);
});
socket.on('webrtc_call_ice_candidate', (data: { data: { from: string, candidate: RTCIceCandidateInit } }) => {
@ -127,10 +122,6 @@ export default class Call {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
if (isAgent) {
console.log('!!! CREATED PC FOR AGENT', remotePeerId);
}
// If there is a local stream, add its tracks to the connection
if (this.callArgs && this.callArgs.localStream && this.callArgs.localStream.stream) {
this.callArgs.localStream.stream.getTracks().forEach((track) => {
@ -142,7 +133,6 @@ export default class Call {
pc.onicecandidate = (event) => {
if (event.candidate) {
if (isAgent) {
console.log("!!! SEND ICE CANDIDATE for", getSocketIdByCallId(remotePeerId));
this.socket.emit('WEBRTC_AGENT_CALL', { from: localPeerId, candidate: event.candidate, toAgentId: getSocketIdByCallId(remotePeerId), type: WEBRTC_CALL_AGENT_EVENT_TYPES.ICE_CANDIDATE });
} else {
this.socket.emit('webrtc_call_ice_candidate', { from: remotePeerId, candidate: event.candidate });
@ -157,7 +147,6 @@ export default class Call {
const stream = event.streams[0];
if (stream && !this.videoStreams[remotePeerId]) {
const clonnedStream = stream.clone();
console.log('SETTING VIDEOTRACKS TO', remotePeerId);
this.videoStreams[remotePeerId] = clonnedStream.getVideoTracks()[0];
if (this.store.get().calling !== CallingState.OnCall) {
this.store.update({ calling: CallingState.OnCall });
@ -192,7 +181,6 @@ export default class Call {
// ESTABLISHING A CONNECTION
private async _peerConnection({ remotePeerId, isAgent, socketId, localPeerId }: { remotePeerId: string, isAgent?: boolean, socketId?: string, localPeerId?: string }) {
console.log("_ PEER CONNECTION", remotePeerId);
try {
// Create RTCPeerConnection with client
const pc = await this.createPeerConnection({ remotePeerId, localPeerId, isAgent });
@ -201,11 +189,9 @@ export default class Call {
// Create an SDP offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
console.log("1 !!! CREATED PC WITH OFFER FOR AN AGENT", pc, offer);
// Sending offer
if (isAgent) {
console.log("2 !!! SENDING OFFER TO AGENT", socketId);
this.socket.emit('WEBRTC_AGENT_CALL', { from: localPeerId, offer, toAgentId: socketId, type: WEBRTC_CALL_AGENT_EVENT_TYPES.OFFER });
} else {
this.socket.emit('webrtc_call_offer', { from: remotePeerId, offer });
@ -232,14 +218,13 @@ export default class Call {
// Process the received offer to answer
private async handleOffer(data: { from: string, offer: RTCSessionDescriptionInit }, isAgent?: boolean) {
// set to remotePeerId data.from
logger.log("RECEIVED OFFER", data);
const fromCallId = data.from;
let pc = this.connections[fromCallId];
console.log("3 !!! HANDLE OFFER", isAgent, pc);
if (!pc) {
if (isAgent) {
this.connections[fromCallId] = await this.createPeerConnection({ remotePeerId: fromCallId, isAgent, localPeerId: this.callID });
pc = this.connections[fromCallId];
console.log("4 CREATED NEW PC FOR INCOMING OFFER", pc);
} else {
logger.error("No connection found for remote peer", fromCallId);
return;
@ -252,10 +237,8 @@ export default class Call {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
if (isAgent) {
console.log("4 !!! SENDING ANSWER TO AGENT", getSocketIdByCallId(fromCallId));
this.socket.emit('WEBRTC_AGENT_CALL', { from: this.callID, answer, toAgentId: getSocketIdByCallId(fromCallId), type: WEBRTC_CALL_AGENT_EVENT_TYPES.ANSWER });
} else {
console.log("4.1 !!! SENDING PLAIN ANSWER SOCKET TO AGENT", getSocketIdByCallId(fromCallId));
this.socket.emit('webrtc_call_answer', { from: fromCallId, answer });
}
} else {
@ -270,7 +253,7 @@ export default class Call {
// Process the received answer to offer
private async handleAnswer(data: { from: string, answer: RTCSessionDescriptionInit }, isAgent?: boolean) {
// set to remotePeerId data.from
console.log("5 !!! HANDLE ANSWER", this.connections, data.from, this.connections[data.from]);
logger.log("RECEIVED ANSWER", data);
if (this.agentInCallIds.includes(data.from) && !isAgent) {
return;
}
@ -295,7 +278,6 @@ export default class Call {
// process the received iceCandidate
private async handleIceCandidate(data: { from: string, candidate: RTCIceCandidateInit }) {
console.log("### GET ICE CANDIDATE", data);
const callId = data.from;
const pc = this.connections[callId];
if (!pc) return;
@ -325,6 +307,8 @@ export default class Call {
// Clear connections
this.connections = {};
this.callArgs = null;
this.videoStreams = {};
this.callArgs = null;
}
// Call completion event handler by signal
@ -384,7 +368,6 @@ export default class Call {
// Initiates a call
call(): { end: () => void } {
console.log("INTIATE CALL", this.agentIds);
this._callSessionPeer();
// this.callAgentsInSession({ agentIds: this.agentInCallIds });
return {
@ -397,12 +380,6 @@ export default class Call {
this.emitData('videofeed', { streamId: this.callID, enabled });
}
// Connect with other agents
// addPeerCall(thirdPartyPeers: string[]) {
// console.log("ADD THRID PARTY CALL", thirdPartyPeers);
// thirdPartyPeers.forEach((peerId) => this._peerConnection(peerId));
// }
// Calls the method to create a connection with a peer
private _callSessionPeer() {
if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) {
@ -425,16 +402,12 @@ export default class Call {
private callAgentsInSession({ agentIds }: { agentIds: string[] }) {
if (agentIds) {
const filteredAgentIds = agentIds.filter((id: string) => id.split('-')[3] !== this.agent.id.toString());
console.log("!!! FILTERED AGENT IDS", filteredAgentIds, this.agentInCallIds);
const newIds = filteredAgentIds.filter((id: string) => !this.agentInCallIds.includes(id));
console.log("!!! NEW AGENT IDS", newIds);
const removedIds = this.agentInCallIds.filter((id: string) => !filteredAgentIds.includes(id));
console.log("!!! REMOVED AGENT IDS", removedIds);
removedIds.forEach((id: string) => this.agentDisconnected(id));
if (this.store.get().calling === CallingState.OnCall) {
newIds.forEach((id: string) => {
const socketId = getSocketIdByCallId(id);
console.log("!!! INITIALISING CALL WITH AgENT", id);
this._peerConnection({ remotePeerId: id, isAgent: true, socketId, localPeerId: this.callID });
});
}

View file

@ -502,8 +502,6 @@ export default class Assist {
function endAgentCall({ socketId, callId }: { socketId: string, callId?: string }) {
callingAgents.delete(socketId)
console.log("CALLING AGENTS", callingAgents)
if (callingAgents.size === 0) {
handleCallEnd()
} else {
@ -618,7 +616,6 @@ export default class Assist {
// get all local tracks and add them to RTCPeerConnection
lStreams[from].stream.getTracks().forEach(track => {
console.log('GETTING TRACKS FROM', from);
pc.addTrack(track, lStreams[from].stream);
});
@ -633,10 +630,9 @@ export default class Assist {
pc.ontrack = (event) => {
const rStream = event.streams[0];
if (rStream && callUI) {
console.log('2 GETTING TRACKS FROM', from);
callUI.addRemoteStream(rStream, from);
const onInteraction = () => {
callUI?.playRemote(from);
callUI?.playRemote();
document.removeEventListener('click', onInteraction);
};
document.addEventListener('click', onInteraction);

View file

@ -3,246 +3,10 @@ import attachDND from './dnd.js'
const SS_START_TS_KEY = '__openreplay_assist_call_start_ts'
const text = `
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenReplay | Assist</title>
<!--CSS -->
<!-- <link href="css/styles.css" rel="stylesheet"> -->
<style>
.connecting-message {
margin-top: 50%;
font-size: 20px;
color: #aaa;
text-align: center;
display: none;
font-family: sans-serif;
}
.status-connecting .connecting-message {
display: block;
}
.status-connecting .card {
display: none;
}
.card {
min-width: 324px;
width: 350px;
max-width: 800px;
cursor: move;
}
#agent-name,
#duration {
cursor: default;
}
#local-stream,
#remote-stream {
display: none;
}
#video-container {
display: flex;
flex-direction: row;
position: relative;
}
#video-container {
display: none;
flex-direction: row;
position: relative;
}
[data-attr="remote-stream"] {
flex: 1;
}
[data-attr="remote-stream"] video {
aspect-ratio: 4 / 3;
width: 100%;
}
#video-container.remote {
display: flex;
}
#video-container.local {
min-height: 100px;
}
#video-container.local #local-stream {
display: block;
}
#local-stream {
width: 35%;
position: absolute;
z-index: 99;
bottom: 5px;
right: 5px;
}
#audio-btn .bi-mic-mute {
display: none;
}
#audio-btn, #video-btn {
color: #cc0000;
}
#audio-btn:after {
text-transform: capitalize;
content: 'Mute'
}
#audio-btn.muted, #video-btn.off {
color: #888;
}
#audio-btn.muted .bi-mic-mute {
display: inline-block;
}
#audio-btn.muted .bi-mic {
display: none;
}
#audio-btn.muted:after {
content: 'Unmute'
}
#video-btn .bi-camera-video-off {
display: none;
}
#video-btn:after {
text-transform: capitalize;
content: 'Stop Video'
}
#video-btn.off:after {
content: 'Start Video'
}
#video-btn.off .bi-camera-video-off {
display: inline-block;
}
#video-btn.off .bi-camera-video {
display: none;
}
.remote-control {
display: none;
justify-content: space-between;
flex-direction: row;
align-items: center;
padding: 8px 16px;
}
#title-span {
font-weight: 500;
}
</style>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<section id="or-assist" class="status-connecting">
<div class="card border-dark shadow drag-area">
<div class="connecting-message"> Connecting... </div>
<div id="controls">
<div class="card-header d-flex justify-content-between">
<div class="user-info">
<span id="title-span">Call with</span>
<!-- User Name -->
<span id="agent-name" class="person-name fw-light">Support Agent</span>
</div>
<div class="call-duration">
<!--Call Duration. -->
<span id="duration" class="card-subtitle mb-2 text-muted fw-light" data-bs-toggle="tooltip"
data-bs-placement="bottom" title="Duration">00:00</span>
</div>
</div>
<div id="video-container" class="card-body bg-dark p-0 d-flex align-items-center position-relative">
<div id="local-stream" class="ratio ratio-4x3 rounded m-0 p-0 shadow scale-x-[-1]">
<!-- fix horizontal mirroring -->
<video id="video-local" autoplay muted class="scale-x-[-1]"></video>
</div>
<div data-attr="remote-stream" class="m-0 p-0">
</div>
</div>
<div class="card-footer bg-transparent d-flex justify-content-between">
<div class="assist-controls">
<a href="#" id="audio-btn" class="btn btn-light btn-sm text-uppercase me-2"><i>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-mic"
viewBox="0 0 16 16">
<path
d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z" />
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-mic-mute"
viewBox="0 0 16 16">
<path
d="M13 8c0 .564-.094 1.107-.266 1.613l-.814-.814A4.02 4.02 0 0 0 12 8V7a.5.5 0 0 1 1 0v1zm-5 4c.818 0 1.578-.245 2.212-.667l.718.719a4.973 4.973 0 0 1-2.43.923V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 1 0v1a4 4 0 0 0 4 4zm3-9v4.879l-1-1V3a2 2 0 0 0-3.997-.118l-.845-.845A3.001 3.001 0 0 1 11 3z" />
<path
d="m9.486 10.607-.748-.748A2 2 0 0 1 6 8v-.878l-1-1V8a3 3 0 0 0 4.486 2.607zm-7.84-9.253 12 12 .708-.708-12-12-.708.708z" />
</svg>
</i></a>
<!-- Add class .mute to #audio-btn when user mutes audio -->
<a href="#" id="video-btn" class="off btn btn-light btn-sm text-uppercase ms-2"><i>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-camera-video" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-camera-video-off" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M10.961 12.365a1.99 1.99 0 0 0 .522-1.103l3.11 1.382A1 1 0 0 0 16 11.731V4.269a1 1 0 0 0-1.406-.913l-3.111 1.382A2 2 0 0 0 9.5 3H4.272l.714 1H9.5a1 1 0 0 1 1 1v6a1 1 0 0 1-.144.518l.605.847zM1.428 4.18A.999.999 0 0 0 1 5v6a1 1 0 0 0 1 1h5.014l.714 1H2a2 2 0 0 1-2-2V5c0-.675.334-1.272.847-1.634l.58.814zM15 11.73l-3.5-1.555v-4.35L15 4.269v7.462zm-4.407 3.56-10-14 .814-.58 10 14-.814.58z" />
</svg>
</i></a>
<!--Add class .off to #video-btn when user stops video -->
</div>
<div class="assist-end">
<a id="end-call-btn" style="min-width:55px;" href="#" class="btn btn-danger btn-sm text-uppercase">End</a>
</div>
</div>
</div>
<div id="remote-control-row" class="remote-control">
<div style="font-size: 13px;">This tab has remote control access</div>
<button style="min-width:55px;" id="end-control-btn" href="#" class="btn btn-outline-primary btn-sm text-uppercase">
Stop
</button>
</div>
</div>
</section>
</body>
</html>
`
export default class CallWindow {
private remoteVideoId: string
private readonly iframe: HTMLIFrameElement
private vRemote: Map<string, HTMLVideoElement | null> = new Map()
private vRemote: HTMLVideoElement | null = null
private vLocal: HTMLVideoElement | null = null
private audioBtn: HTMLElement | null = null
private videoBtn: HTMLElement | null = null
@ -256,8 +20,7 @@ export default class CallWindow {
private controlsContainer: HTMLElement | null = null
private onToggleVideo: (args: any) => void
private tsInterval: ReturnType<typeof setInterval>
private remoteVideos: Map<string, MediaStreamTrack> = new Map()
private vContainer: HTMLDivElement | null = null
private remoteVideo: MediaStreamTrack
private readonly load: Promise<void>
@ -289,7 +52,7 @@ export default class CallWindow {
// this.load = fetch(this.callUITemplate || baseHref + '/index2.html')
this.load = fetch(this.callUITemplate || baseHref + '/index.html')
.then((r) => r.text())
.then(() => {
.then((text) => {
iframe.onload = () => {
const assistSection = doc.getElementById('or-assist')
setTimeout(() => {
@ -301,15 +64,13 @@ export default class CallWindow {
iframe.onload = null
}
// ?
const newText = text.replace(/href="css/g, `href="${baseHref}/css`)
text = text.replace(/href="css/g, `href="${baseHref}/css`)
doc.open()
doc.write(newText)
doc.write(text)
doc.close()
this.vLocal = doc.getElementById('video-local') as HTMLVideoElement | null
// this.vRemote = doc.getElementById('video-remote') as HTMLVideoElement | null
this.remoteStreamVideoContainerSample = doc.querySelector('[data-attr="remote-stream"]');
this.remoteStreamVideoContainerSample?.remove();
this.vRemote = doc.getElementById('video-remote') as HTMLVideoElement | null
this.videoContainer = doc.getElementById('video-container')
@ -378,52 +139,30 @@ export default class CallWindow {
this.load
.then(() => {
// Video
console.log('VREMOTE', this.vRemote);
if (!this.vRemote.has(peerId)) {
if (this.remoteStreamVideoContainerSample && this.videoContainer) {
const newRemoteStreamVideoContainer = this.remoteStreamVideoContainerSample.cloneNode(true) as HTMLDivElement;
newRemoteStreamVideoContainer.setAttribute("data-peer-id", peerId);
const videoElement = document.createElement("video");
videoElement.autoplay = true;
newRemoteStreamVideoContainer.appendChild(videoElement);
const clonedStream = rStream.clone()
videoElement.srcObject = clonedStream
const videoElementTest = document.createElement("video");
videoElementTest.autoplay = true;
videoElementTest.srcObject = clonedStream;
videoElementTest.setAttribute("data-peer-id", peerId);
document.body.appendChild(videoElementTest);
this.remoteVideos.set(peerId, clonedStream.getVideoTracks()[0])
console.log("ADD REMOTE STREAM", clonedStream.getVideoTracks()[0]);
if (this.vPlaceholder) {
this.vPlaceholder.innerText =
'Video has been paused. Click anywhere to resume.'
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
let enabled = false
this.checkRemoteVideoInterval = setInterval(() => {
const settings = this.remoteVideo?.getSettings()
const isDummyVideoTrack = !this.remoteVideo.enabled || (!!settings && (settings.width === 2 || settings.frameRate === 0))
const shouldBeEnabled = !isDummyVideoTrack
if (enabled !== shouldBeEnabled) {
this.toggleRemoteVideoUI((enabled = shouldBeEnabled))
}
this.vRemote.set(peerId, videoElement)
this.videoContainer.appendChild(newRemoteStreamVideoContainer);
console.log('ДОБАВИЛИ ВИДЕО В ДОМ для', peerId);
}
}, 1000)
}
// Hack to determine if the remote video is enabled
// TODO: pass this info through socket
if (this.checkRemoteVideoInterval) {
clearInterval(this.checkRemoteVideoInterval)
} // just in case
let enabled = false
this.checkRemoteVideoInterval = setInterval(() => {
const settings = this.remoteVideos.get(peerId)?.getSettings()
const isDummyVideoTrack = !this.remoteVideos.get(peerId)?.enabled || (!!settings && (settings.width === 2 || settings.frameRate === 0))
const shouldBeEnabled = !isDummyVideoTrack
if (enabled !== shouldBeEnabled) {
this.toggleRemoteVideoUI((enabled = shouldBeEnabled))
}
}, 1000)
// Audio
if (!this.audioContainer) {
this.audioContainer = document.createElement('div')
@ -461,13 +200,8 @@ export default class CallWindow {
this.localStreams = streams
}
playRemote(peerId: string) {
if (this.vRemote.has(peerId)) {
const vRemote = this.vRemote.get(peerId)
if (vRemote) {
vRemote.play()
}
}
playRemote() {
this.vRemote && this.vRemote.play()
}
setAssistentName(callingAgents: Map<string, string>) {
@ -596,16 +330,9 @@ export default class CallWindow {
}
toggleVideoStream({ streamId, enabled, }: { streamId: string, enabled: boolean }) {
if (this.remoteVideos.has(streamId)) {
console.log("TRYING TO ENABLE", this.remoteVideos.get[streamId])
const track = this.remoteVideos.get(streamId)
if (track) {
console.log('ENABLING TRACK', track);
track.enabled = enabled
}
if (this.vRemote.size <= 1) {
this.toggleRemoteVideoUI(enabled)
}
if (this.remoteVideoId === streamId) {
this.remoteVideo.enabled = enabled
this.toggleRemoteVideoUI(enabled)
}
}
}
}