add conference call
This commit is contained in:
parent
ab63f7ecb3
commit
67999f1373
7 changed files with 449 additions and 115 deletions
|
|
@ -146,7 +146,7 @@ function AssistActions({
|
|||
setIncomeStream([]);
|
||||
};
|
||||
|
||||
function call(additionalAgentIds?: string[]) {
|
||||
function call() {
|
||||
RequestLocalStream()
|
||||
.then((lStream) => {
|
||||
setLocalStream(lStream);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ function LivePlayerBlockHeader({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds} />
|
||||
<AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export default class WebLivePlayer extends WebPlayer {
|
|||
config,
|
||||
wpState,
|
||||
(id) => this.messageManager.getNode(id),
|
||||
agentId,
|
||||
uiErrorHandler
|
||||
);
|
||||
this.assistManager.connect(session.agentToken!, agentId, projectId);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import RemoteControl, { RemoteControlStatus } from './RemoteControl';
|
|||
import ScreenRecording, { SessionRecordingStatus } from './ScreenRecording';
|
||||
import CanvasReceiver from 'Player/web/assist/CanvasReceiver';
|
||||
import { gunzipSync } from 'fflate';
|
||||
import logger from '@/logger';
|
||||
|
||||
export { RemoteControlStatus, SessionRecordingStatus, CallingState };
|
||||
|
||||
|
|
@ -68,6 +67,7 @@ export default class AssistManager {
|
|||
...RemoteControl.INITIAL_STATE,
|
||||
...ScreenRecording.INITIAL_STATE,
|
||||
};
|
||||
private agentIds: string[] = [];
|
||||
|
||||
// TODO: Session type
|
||||
constructor(
|
||||
|
|
@ -78,9 +78,10 @@ export default class AssistManager {
|
|||
private config: RTCIceServer[] | null,
|
||||
private store: Store<typeof AssistManager.INITIAL_STATE>,
|
||||
private getNode: MessageManager['getNode'],
|
||||
public readonly agentId: number,
|
||||
public readonly uiErrorHandler?: {
|
||||
error: (msg: string) => void;
|
||||
}
|
||||
},
|
||||
) {}
|
||||
|
||||
public getAssistVersion = () => this.assistVersion;
|
||||
|
|
@ -194,9 +195,9 @@ export default class AssistManager {
|
|||
},
|
||||
}));
|
||||
|
||||
socket.onAny((event, ...args) => {
|
||||
logger.log(`📩 Socket: ${event}`, args);
|
||||
});
|
||||
// socket.onAny((event, ...args) => {
|
||||
// logger.log(`📩 Socket: ${event}`, args);
|
||||
// });
|
||||
|
||||
|
||||
socket.on('connect', () => {
|
||||
|
|
@ -275,6 +276,10 @@ export default class AssistManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (data.agentIds) {
|
||||
const filteredAgentIds = this.agentIds.filter((id: string) => id.split('-')[3] !== agentId.toString());
|
||||
this.agentIds = filteredAgentIds;
|
||||
}
|
||||
});
|
||||
socket.on('SESSION_DISCONNECTED', (e) => {
|
||||
waitingForMessages = true;
|
||||
|
|
@ -299,7 +304,8 @@ export default class AssistManager {
|
|||
{
|
||||
...this.session.agentInfo,
|
||||
id: agentId,
|
||||
}
|
||||
},
|
||||
this.agentIds,
|
||||
);
|
||||
this.remoteControl = new RemoteControl(
|
||||
this.store,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ export interface State {
|
|||
currentTab?: string;
|
||||
}
|
||||
|
||||
const WEBRTC_CALL_AGENT_EVENT_TYPES = {
|
||||
OFFER: 'offer',
|
||||
ANSWER: 'answer',
|
||||
ICE_CANDIDATE: 'ice-candidate',
|
||||
}
|
||||
|
||||
export default class Call {
|
||||
private assistVersion = 1;
|
||||
static readonly INITIAL_STATE: Readonly<State> = {
|
||||
|
|
@ -26,6 +32,8 @@ export default class Call {
|
|||
private connections: Record<string, RTCPeerConnection> = {};
|
||||
private connectAttempts = 0;
|
||||
private videoStreams: Record<string, MediaStreamTrack> = {};
|
||||
private callID: string;
|
||||
private agentInCallIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private store: Store<State & { tabs: Set<string> }>,
|
||||
|
|
@ -34,36 +42,29 @@ export default class Call {
|
|||
private peerID: string,
|
||||
private getAssistVersion: () => number,
|
||||
private agent: Record<string, any>,
|
||||
private agentInCallIds: string[] = [],
|
||||
private callId: string,
|
||||
private agentIds: string[],
|
||||
) {
|
||||
|
||||
socket.on('WEBRTC_AGENT_CALL', (data) => {
|
||||
console.log("!WEBRTC AGENT CALL RECEIVED", 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:
|
||||
this.handleAnswer(data, true);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('UPDATE_SESSION', (data: { data: { agentIds: string[] }}) => {
|
||||
const { agentIds } = data.data;
|
||||
console.log("AGENT IDS", agentIds);
|
||||
if (agentIds) {
|
||||
const filteredAgentIds = agentIds.filter((id: string) => id.split('_')[3] !== this.agent.id.toString());
|
||||
console.log("!!! FILTERED IDS", filteredAgentIds);
|
||||
const newIds = filteredAgentIds.filter((id: string) => !this.agentInCallIds.includes(id));
|
||||
console.log("!!! NEW IDS", newIds);
|
||||
const removedIds = this.agentInCallIds.filter((id: string) => !filteredAgentIds.includes(id));
|
||||
console.log("!!! REMOVED IDS", removedIds);
|
||||
removedIds.forEach((id: string) => this.agentDisconnected(id));
|
||||
if (store.get().calling !== CallingState.OnCall) {
|
||||
newIds.forEach((id: string) => {
|
||||
console.log("CALL3 for", id);
|
||||
const socketId = getSocketIdByCallId(id);
|
||||
console.log("FOUND SOCKET ID", socketId);
|
||||
this._peerConnection(id, true, socketId);
|
||||
});
|
||||
}
|
||||
|
||||
this.agentInCallIds = filteredAgentIds;
|
||||
}
|
||||
console.log("UPDATE SESSION", data.data.agentIds);
|
||||
this.callAgentsInSession({ agentIds: data.data.agentIds });
|
||||
});
|
||||
|
||||
socket.on('call_end', () => {
|
||||
|
|
@ -108,6 +109,7 @@ export default class Call {
|
|||
});
|
||||
|
||||
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 } }) => {
|
||||
|
|
@ -118,12 +120,17 @@ export default class Call {
|
|||
}
|
||||
|
||||
// CREATE A LOCAL PEER
|
||||
private async createPeerConnection(callId: string): Promise<RTCPeerConnection> {
|
||||
private async createPeerConnection({ remotePeerId, localPeerId, isAgent }: { remotePeerId: string, isAgent?: boolean, localPeerId?: string }): Promise<RTCPeerConnection> {
|
||||
// create pc with ice config
|
||||
|
||||
const pc = new RTCPeerConnection({
|
||||
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) => {
|
||||
|
|
@ -134,7 +141,12 @@ export default class Call {
|
|||
// when ice is ready we send it
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.socket.emit('webrtc_call_ice_candidate', { from: callId, candidate: 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 });
|
||||
}
|
||||
} else {
|
||||
logger.log("ICE candidate gathering complete");
|
||||
}
|
||||
|
|
@ -143,13 +155,15 @@ export default class Call {
|
|||
// when we receive a remote track, we write it to videoStreams[peerId]
|
||||
pc.ontrack = (event) => {
|
||||
const stream = event.streams[0];
|
||||
if (stream) {
|
||||
this.videoStreams[callId] = stream.getVideoTracks()[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 });
|
||||
}
|
||||
if (this.callArgs) {
|
||||
this.callArgs.onStream(stream, isAgentId(callId));
|
||||
this.callArgs.onStream(stream, remotePeerId !== this.callID && isAgentId(remotePeerId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -177,21 +191,22 @@ export default class Call {
|
|||
}
|
||||
|
||||
// ESTABLISHING A CONNECTION
|
||||
private async _peerConnection(remotePeerId: string, isAgent?: boolean, socketId?: string) {
|
||||
private async _peerConnection({ remotePeerId, isAgent, socketId, localPeerId }: { remotePeerId: string, isAgent?: boolean, socketId?: string, localPeerId?: string }) {
|
||||
console.log("_ PEER CONNECTION", remotePeerId);
|
||||
try {
|
||||
// Create RTCPeerConnection
|
||||
const pc = await this.createPeerConnection(remotePeerId);
|
||||
// Create RTCPeerConnection with client
|
||||
const pc = await this.createPeerConnection({ remotePeerId, localPeerId, isAgent });
|
||||
this.connections[remotePeerId] = pc;
|
||||
|
||||
// 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("CALL6", remotePeerId, socketId);
|
||||
this.socket.emit('WEBRTC_AGENT_CALL', { from: remotePeerId, offer, toAgentId: socketId });
|
||||
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 });
|
||||
}
|
||||
|
|
@ -204,8 +219,7 @@ export default class Call {
|
|||
this.connectAttempts++;
|
||||
logger.log('reconnecting', this.connectAttempts);
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
console.log("CALL2")
|
||||
await this._peerConnection(remotePeerId);
|
||||
await this._peerConnection({ remotePeerId });
|
||||
} else {
|
||||
logger.log('error', this.connectAttempts);
|
||||
this.callArgs?.onError?.('Could not establish a connection with the peer after 5 attempts');
|
||||
|
|
@ -216,21 +230,34 @@ export default class Call {
|
|||
}
|
||||
|
||||
// Process the received offer to answer
|
||||
private async handleOffer(data: { from: string, offer: RTCSessionDescriptionInit }) {
|
||||
private async handleOffer(data: { from: string, offer: RTCSessionDescriptionInit }, isAgent?: boolean) {
|
||||
// set to remotePeerId data.from
|
||||
const callId = data.from;
|
||||
const pc = this.connections[callId];
|
||||
const fromCallId = data.from;
|
||||
let pc = this.connections[fromCallId];
|
||||
console.log("3 !!! HANDLE OFFER", isAgent, pc);
|
||||
if (!pc) {
|
||||
logger.error("No connection found for remote peer", callId);
|
||||
return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
try {
|
||||
// if the connection is not established yet, then set remoteDescription to peer
|
||||
if (pc.signalingState !== "stable") {
|
||||
if (!pc.localDescription) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
this.socket.emit('webrtc_call_answer', { from: callId, answer: pc.localDescription });
|
||||
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 {
|
||||
logger.warn("Skipping setRemoteDescription: Already in stable state");
|
||||
}
|
||||
|
|
@ -241,15 +268,16 @@ export default class Call {
|
|||
}
|
||||
|
||||
// Process the received answer to offer
|
||||
private async handleAnswer(data: { from: string, answer: RTCSessionDescriptionInit }) {
|
||||
private async handleAnswer(data: { from: string, answer: RTCSessionDescriptionInit }, isAgent?: boolean) {
|
||||
// set to remotePeerId data.from
|
||||
if (this.agentInCallIds.includes(data.from)) {
|
||||
console.log("5 !!! HANDLE ANSWER", this.connections, data.from, this.connections[data.from]);
|
||||
if (this.agentInCallIds.includes(data.from) && !isAgent) {
|
||||
return;
|
||||
}
|
||||
const callId = data.from;
|
||||
const pc = this.connections[callId];
|
||||
if (!pc) {
|
||||
logger.error("No connection found for remote peer", callId);
|
||||
logger.error("No connection found for remote peer", callId, this.connections);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -267,6 +295,7 @@ 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;
|
||||
|
|
@ -317,7 +346,7 @@ export default class Call {
|
|||
|
||||
// Ends the call and sends the call_end signal
|
||||
initiateCallEnd = async () => {
|
||||
this.emitData('call_end', this.callId);
|
||||
this.emitData('call_end', this.callID);
|
||||
this.handleCallEnd();
|
||||
};
|
||||
|
||||
|
|
@ -339,7 +368,7 @@ export default class Call {
|
|||
|
||||
setCallArgs(
|
||||
localStream: LocalStream,
|
||||
onStream: (s: MediaStream) => void,
|
||||
onStream: (s: MediaStream, isAgent: boolean) => void,
|
||||
onCallEnd: () => void,
|
||||
onReject: () => void,
|
||||
onError?: (e?: any) => void
|
||||
|
|
@ -354,12 +383,10 @@ export default class Call {
|
|||
}
|
||||
|
||||
// Initiates a call
|
||||
call(thirdPartyPeers?: string[]): { end: () => void } {
|
||||
if (thirdPartyPeers && thirdPartyPeers.length > 0) {
|
||||
this.addPeerCall(thirdPartyPeers);
|
||||
} else {
|
||||
this._callSessionPeer();
|
||||
}
|
||||
call(): { end: () => void } {
|
||||
console.log("INTIATE CALL", this.agentIds);
|
||||
this._callSessionPeer();
|
||||
// this.callAgentsInSession({ agentIds: this.agentInCallIds });
|
||||
return {
|
||||
end: this.initiateCallEnd,
|
||||
};
|
||||
|
|
@ -367,7 +394,7 @@ export default class Call {
|
|||
|
||||
// Notify peers of local video state change
|
||||
toggleVideoLocalStream(enabled: boolean) {
|
||||
this.emitData('videofeed', { streamId: this.peerID, enabled });
|
||||
this.emitData('videofeed', { streamId: this.callID, enabled });
|
||||
}
|
||||
|
||||
// Connect with other agents
|
||||
|
|
@ -378,7 +405,6 @@ export default class Call {
|
|||
|
||||
// Calls the method to create a connection with a peer
|
||||
private _callSessionPeer() {
|
||||
console.log("CALL1")
|
||||
if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -389,17 +415,42 @@ export default class Call {
|
|||
}
|
||||
|
||||
// Generate a peer identifier depending on the assist version
|
||||
const peerId =
|
||||
this.getAssistVersion() === 1
|
||||
? this.peerID
|
||||
: `${this.peerID}_${tab || Array.from(this.store.get().tabs)[0]}_${this.agent.id}_${this.socket.id}_agent`;
|
||||
this.callID = this.getCallId();
|
||||
|
||||
console.log("PEER IDDDD", peerId);
|
||||
this.callId = peerId;
|
||||
|
||||
const userName = userStore.account.name;
|
||||
this.emitData('_agent_name', userName);
|
||||
void this._peerConnection(peerId);
|
||||
void this._peerConnection({ remotePeerId: this.callID });
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
this.agentInCallIds = filteredAgentIds;
|
||||
}
|
||||
}
|
||||
|
||||
private getCallId() {
|
||||
const tab = this.store.get().currentTab;
|
||||
if (!tab) {
|
||||
logger.warn('No tab data to connect to peer');
|
||||
}
|
||||
|
||||
// Generate a peer identifier depending on the assist version
|
||||
return `${this.peerID}-${tab || Array.from(this.store.get().tabs)[0]}-${this.agent.id}-${this.socket.id}-agent`;
|
||||
}
|
||||
|
||||
agentDisconnected(agentId: string) {
|
||||
|
|
@ -420,6 +471,10 @@ function isAgentId(id: string): boolean {
|
|||
return id.endsWith('_agent');
|
||||
}
|
||||
|
||||
function getSocketIdByCallId(callId: string): string | undefined {
|
||||
return callId.split('_')[3];
|
||||
}
|
||||
function getSocketIdByCallId(callId?: string): string | undefined {
|
||||
const socketIdRegex = /-\d{2}-(.*?)\-agent/;
|
||||
const match = callId?.match(socketIdRegex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,7 +426,6 @@ export default class Assist {
|
|||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const name = info.data
|
||||
callingAgents.set(id, name)
|
||||
console.log('CALLING AGENTS', callingAgents)
|
||||
updateCallerNames()
|
||||
})
|
||||
|
||||
|
|
@ -477,7 +476,6 @@ export default class Assist {
|
|||
})
|
||||
|
||||
socket.on('webrtc_call_offer', async (_, data: { from: string, offer: RTCSessionDescriptionInit }) => {
|
||||
console.log("OFFER FROM", data.from)
|
||||
if (!this.calls.has(data.from)) {
|
||||
await handleIncomingCallOffer(data.from, data.offer);
|
||||
}
|
||||
|
|
@ -517,7 +515,6 @@ export default class Assist {
|
|||
}
|
||||
|
||||
const handleCallEndWithAgent = (id: string) => {
|
||||
console.log("!!!!", this.calls.get(id))
|
||||
this.calls.get(id)?.close()
|
||||
this.calls.delete(id)
|
||||
}
|
||||
|
|
@ -621,6 +618,7 @@ 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);
|
||||
});
|
||||
|
||||
|
|
@ -635,10 +633,10 @@ 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();
|
||||
callUI?.playRemote(from);
|
||||
document.removeEventListener('click', onInteraction);
|
||||
};
|
||||
document.addEventListener('click', onInteraction);
|
||||
|
|
|
|||
|
|
@ -3,15 +3,252 @@ 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: HTMLVideoElement | null = null
|
||||
private vRemote: Map<string, HTMLVideoElement | null> = new Map()
|
||||
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
|
||||
|
|
@ -19,7 +256,8 @@ export default class CallWindow {
|
|||
private controlsContainer: HTMLElement | null = null
|
||||
private onToggleVideo: (args: any) => void
|
||||
private tsInterval: ReturnType<typeof setInterval>
|
||||
private remoteVideo: MediaStreamTrack
|
||||
private remoteVideos: Map<string, MediaStreamTrack> = new Map()
|
||||
private vContainer: HTMLDivElement | null = null
|
||||
|
||||
private readonly load: Promise<void>
|
||||
|
||||
|
|
@ -51,7 +289,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((text) => {
|
||||
.then(() => {
|
||||
iframe.onload = () => {
|
||||
const assistSection = doc.getElementById('or-assist')
|
||||
setTimeout(() => {
|
||||
|
|
@ -62,17 +300,19 @@ export default class CallWindow {
|
|||
this.adjustIframeSize()
|
||||
iframe.onload = null
|
||||
}
|
||||
|
||||
// ?
|
||||
text = text.replace(/href="css/g, `href="${baseHref}/css`)
|
||||
const newText = text.replace(/href="css/g, `href="${baseHref}/css`)
|
||||
doc.open()
|
||||
doc.write(text)
|
||||
doc.write(newText)
|
||||
doc.close()
|
||||
|
||||
this.vLocal = doc.getElementById('video-local') as HTMLVideoElement | null
|
||||
this.vRemote = doc.getElementById('video-remote') as HTMLVideoElement | null
|
||||
// this.vRemote = doc.getElementById('video-remote') as HTMLVideoElement | null
|
||||
this.remoteStreamVideoContainerSample = doc.querySelector('[data-attr="remote-stream"]');
|
||||
this.remoteStreamVideoContainerSample?.remove();
|
||||
|
||||
this.videoContainer = doc.getElementById('video-container')
|
||||
|
||||
|
||||
this.audioBtn = doc.getElementById('audio-btn')
|
||||
if (this.audioBtn) {
|
||||
this.audioBtn.onclick = () => this.toggleAudio()
|
||||
|
|
@ -138,30 +378,52 @@ export default class CallWindow {
|
|||
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
|
||||
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))
|
||||
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.'
|
||||
}
|
||||
}, 1000)
|
||||
this.vRemote.set(peerId, videoElement)
|
||||
this.videoContainer.appendChild(newRemoteStreamVideoContainer);
|
||||
console.log('ДОБАВИЛИ ВИДЕО В ДОМ для', peerId);
|
||||
}
|
||||
}
|
||||
|
||||
// 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')
|
||||
|
|
@ -199,8 +461,13 @@ export default class CallWindow {
|
|||
this.localStreams = streams
|
||||
}
|
||||
|
||||
playRemote() {
|
||||
this.vRemote && this.vRemote.play()
|
||||
playRemote(peerId: string) {
|
||||
if (this.vRemote.has(peerId)) {
|
||||
const vRemote = this.vRemote.get(peerId)
|
||||
if (vRemote) {
|
||||
vRemote.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAssistentName(callingAgents: Map<string, string>) {
|
||||
|
|
@ -329,9 +596,16 @@ export default class CallWindow {
|
|||
}
|
||||
|
||||
toggleVideoStream({ streamId, enabled, }: { streamId: string, enabled: boolean }) {
|
||||
if (this.remoteVideoId === streamId) {
|
||||
this.remoteVideo.enabled = enabled
|
||||
this.toggleRemoteVideoUI(enabled)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue