resolved conflicts
This commit is contained in:
commit
aaaf9f07a4
42 changed files with 14004 additions and 1529 deletions
|
|
@ -98,17 +98,23 @@ def __edit(project_id, col_index, colname, new_name):
|
|||
if col_index not in list(old_metas.keys()):
|
||||
return {"errors": ["custom field not found"]}
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
if old_metas[col_index]["key"] != new_name:
|
||||
if old_metas[col_index]["key"] != new_name:
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(f"""UPDATE public.projects
|
||||
SET {colname} = %(value)s
|
||||
WHERE project_id = %(project_id)s
|
||||
AND deleted_at ISNULL
|
||||
RETURNING {colname};""",
|
||||
RETURNING {colname},
|
||||
(SELECT {colname} FROM projects WHERE project_id = %(project_id)s) AS old_{colname};""",
|
||||
{"project_id": project_id, "value": new_name})
|
||||
cur.execute(query=query)
|
||||
new_name = cur.fetchone()[colname]
|
||||
row = cur.fetchone()
|
||||
new_name = row[colname]
|
||||
old_name = row['old_' + colname]
|
||||
old_metas[col_index]["key"] = new_name
|
||||
projects.rename_metadata_condition(project_id=project_id,
|
||||
old_metadata_key=old_name,
|
||||
new_metadata_key=new_name)
|
||||
return {"data": old_metas[col_index]}
|
||||
|
||||
|
||||
|
|
@ -121,8 +127,8 @@ def edit(tenant_id, project_id, index: int, new_name: str):
|
|||
def delete(tenant_id, project_id, index: int):
|
||||
index = int(index)
|
||||
old_segments = get(project_id)
|
||||
old_segments = [k["index"] for k in old_segments]
|
||||
if index not in old_segments:
|
||||
old_indexes = [k["index"] for k in old_segments]
|
||||
if index not in old_indexes:
|
||||
return {"errors": ["custom field not found"]}
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
|
|
@ -132,7 +138,8 @@ def delete(tenant_id, project_id, index: int):
|
|||
WHERE project_id = %(project_id)s AND deleted_at ISNULL;""",
|
||||
{"project_id": project_id})
|
||||
cur.execute(query=query)
|
||||
|
||||
projects.delete_metadata_condition(project_id=project_id,
|
||||
metadata_key=old_segments[old_indexes.index(index)]["key"])
|
||||
return {"data": get(project_id)}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -413,7 +413,6 @@ def update_project_conditions(project_id, conditions):
|
|||
create_project_conditions(project_id, to_be_created)
|
||||
|
||||
if to_be_updated:
|
||||
logger.debug(to_be_updated)
|
||||
update_project_condition(project_id, to_be_updated)
|
||||
|
||||
return get_conditions(project_id)
|
||||
|
|
@ -428,3 +427,45 @@ def get_projects_ids(tenant_id):
|
|||
cur.execute(query=query)
|
||||
rows = cur.fetchall()
|
||||
return [r["project_id"] for r in rows]
|
||||
|
||||
|
||||
def delete_metadata_condition(project_id, metadata_key):
|
||||
sql = """\
|
||||
UPDATE public.projects_conditions
|
||||
SET filters=(SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
|
||||
FROM jsonb_array_elements(filters) AS elem
|
||||
WHERE NOT (elem ->> 'type' = 'metadata'
|
||||
AND elem ->> 'source' = %(metadata_key)s))
|
||||
WHERE project_id = %(project_id)s
|
||||
AND jsonb_typeof(filters) = 'array'
|
||||
AND EXISTS (SELECT 1
|
||||
FROM jsonb_array_elements(filters) AS elem
|
||||
WHERE elem ->> 'type' = 'metadata'
|
||||
AND elem ->> 'source' = %(metadata_key)s);"""
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(sql, {"project_id": project_id, "metadata_key": metadata_key})
|
||||
cur.execute(query)
|
||||
|
||||
|
||||
def rename_metadata_condition(project_id, old_metadata_key, new_metadata_key):
|
||||
sql = """\
|
||||
UPDATE public.projects_conditions
|
||||
SET filters = (SELECT jsonb_agg(CASE
|
||||
WHEN elem ->> 'type' = 'metadata' AND elem ->> 'source' = %(old_metadata_key)s
|
||||
THEN elem || ('{"source": "'||%(new_metadata_key)s||'"}')::jsonb
|
||||
ELSE elem END)
|
||||
FROM jsonb_array_elements(filters) AS elem)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND jsonb_typeof(filters) = 'array'
|
||||
AND EXISTS (SELECT 1
|
||||
FROM jsonb_array_elements(filters) AS elem
|
||||
WHERE elem ->> 'type' = 'metadata'
|
||||
AND elem ->> 'source' = %(old_metadata_key)s);"""
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(sql, {"project_id": project_id, "old_metadata_key": old_metadata_key,
|
||||
"new_metadata_key": new_metadata_key})
|
||||
cur.execute(query)
|
||||
|
||||
# TODO: make project conditions use metadata-column-name instead of metadata-key
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ const EVENTS_DEFINITION = {
|
|||
UPDATE_EVENT: "UPDATE_SESSION", // tab become active/inactive, page title change, changed session object (rare case), call start/end
|
||||
CONNECT_ERROR: "connect_error",
|
||||
CONNECT_FAILED: "connect_failed",
|
||||
ERROR: "error"
|
||||
ERROR: "error",
|
||||
WEBRTC_AGENT_CALL: "WEBRTC_AGENT_CALL",
|
||||
},
|
||||
//The following list of events will be only emitted by the server
|
||||
server: {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ async function onConnect(socket) {
|
|||
// Handle update event
|
||||
socket.on(EVENTS_DEFINITION.listen.UPDATE_EVENT, (...args) => onUpdateEvent(socket, ...args));
|
||||
|
||||
// Handle webrtc events
|
||||
socket.on(EVENTS_DEFINITION.listen.WEBRTC_AGENT_CALL, (...args) => onWebrtcAgentHandler(socket, ...args));
|
||||
|
||||
// Handle errors
|
||||
socket.on(EVENTS_DEFINITION.listen.ERROR, err => errorHandler(EVENTS_DEFINITION.listen.ERROR, err));
|
||||
socket.on(EVENTS_DEFINITION.listen.CONNECT_ERROR, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_ERROR, err));
|
||||
|
|
@ -186,6 +189,16 @@ async function onUpdateEvent(socket, ...args) {
|
|||
}
|
||||
}
|
||||
|
||||
async function onWebrtcAgentHandler(socket, ...args) {
|
||||
if (socket.handshake.query.identity === IDENTITIES.agent) {
|
||||
const agentIdToConnect = args[0]?.data?.toAgentId;
|
||||
logger.debug(`${socket.id} sent webrtc event to agent:${agentIdToConnect}`);
|
||||
if (agentIdToConnect && socket.handshake.sessionData.AGENTS_CONNECTED.includes(agentIdToConnect)) {
|
||||
socket.to(agentIdToConnect).emit(EVENTS_DEFINITION.listen.WEBRTC_AGENT_CALL, args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onAny(socket, eventName, ...args) {
|
||||
if (Object.values(EVENTS_DEFINITION.listen).indexOf(eventName) >= 0) {
|
||||
logger.debug(`received event:${eventName}, should be handled by another listener, stopping onAny.`);
|
||||
|
|
|
|||
|
|
@ -427,7 +427,6 @@ def update_project_conditions(project_id, conditions):
|
|||
create_project_conditions(project_id, to_be_created)
|
||||
|
||||
if to_be_updated:
|
||||
print(to_be_updated)
|
||||
update_project_condition(project_id, to_be_updated)
|
||||
|
||||
return get_conditions(project_id)
|
||||
|
|
@ -486,3 +485,45 @@ def is_authorized_batch(project_ids, tenant_id):
|
|||
cur.execute(query=query)
|
||||
rows = cur.fetchall()
|
||||
return [r["project_id"] for r in rows]
|
||||
|
||||
|
||||
def delete_metadata_condition(project_id, metadata_key):
|
||||
sql = """\
|
||||
UPDATE public.projects_conditions
|
||||
SET filters=(SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
|
||||
FROM jsonb_array_elements(filters) AS elem
|
||||
WHERE NOT (elem ->> 'type' = 'metadata'
|
||||
AND elem ->> 'source' = %(metadata_key)s))
|
||||
WHERE project_id = %(project_id)s
|
||||
AND jsonb_typeof(filters) = 'array'
|
||||
AND EXISTS (SELECT 1
|
||||
FROM jsonb_array_elements(filters) AS elem
|
||||
WHERE elem ->> 'type' = 'metadata'
|
||||
AND elem ->> 'source' = %(metadata_key)s);"""
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(sql, {"project_id": project_id, "metadata_key": metadata_key})
|
||||
cur.execute(query)
|
||||
|
||||
|
||||
def rename_metadata_condition(project_id, old_metadata_key, new_metadata_key):
|
||||
sql = """\
|
||||
UPDATE public.projects_conditions
|
||||
SET filters = (SELECT jsonb_agg(CASE
|
||||
WHEN elem ->> 'type' = 'metadata' AND elem ->> 'source' = %(old_metadata_key)s
|
||||
THEN elem || ('{"source": "'||%(new_metadata_key)s||'"}')::jsonb
|
||||
ELSE elem END)
|
||||
FROM jsonb_array_elements(filters) AS elem)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND jsonb_typeof(filters) = 'array'
|
||||
AND EXISTS (SELECT 1
|
||||
FROM jsonb_array_elements(filters) AS elem
|
||||
WHERE elem ->> 'type' = 'metadata'
|
||||
AND elem ->> 'source' = %(old_metadata_key)s);"""
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(sql, {"project_id": project_id, "old_metadata_key": old_metadata_key,
|
||||
"new_metadata_key": new_metadata_key})
|
||||
cur.execute(query)
|
||||
|
||||
# TODO: make project conditions use metadata-column-name instead of metadata-key
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import stl from './chatWindow.module.css';
|
|||
import VideoContainer from '../components/VideoContainer';
|
||||
|
||||
export interface Props {
|
||||
incomeStream: MediaStream[] | null;
|
||||
incomeStream: { stream: MediaStream, isAgent: boolean }[] | null;
|
||||
localStream: LocalStream | null;
|
||||
userId: string;
|
||||
isPrestart?: boolean;
|
||||
|
|
@ -54,8 +54,8 @@ function ChatWindow({
|
|||
>
|
||||
{incomeStream ? (
|
||||
incomeStream.map((stream) => (
|
||||
<React.Fragment key={stream.id}>
|
||||
<VideoContainer stream={stream} setRemoteEnabled={setRemoteEnabled} />
|
||||
<React.Fragment key={stream.stream.id}>
|
||||
<VideoContainer stream={stream.stream} setRemoteEnabled={setRemoteEnabled} isAgent={stream.isAgent} />
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -66,6 +66,7 @@ function ChatWindow({
|
|||
stream={localStream ? localStream.stream : null}
|
||||
muted
|
||||
height={anyRemoteEnabled ? 50 : 'unset'}
|
||||
local
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ function AssistActions({
|
|||
} = store.get();
|
||||
|
||||
const [isPrestart, setPrestart] = useState(false);
|
||||
const [incomeStream, setIncomeStream] = useState<MediaStream[] | null>([]);
|
||||
const [incomeStream, setIncomeStream] = useState<{ stream: MediaStream; isAgent: boolean }[] | null>([]);
|
||||
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
|
||||
const [callObject, setCallObject] = useState<{ end:() => void } | null>(null);
|
||||
|
||||
|
|
@ -130,18 +130,25 @@ function AssistActions({
|
|||
}
|
||||
}, [peerConnectionStatus]);
|
||||
|
||||
const addIncomeStream = (stream: MediaStream) => {
|
||||
const addIncomeStream = (stream: MediaStream, isAgent: boolean) => {
|
||||
setIncomeStream((oldState) => {
|
||||
if (oldState === null) return [stream];
|
||||
if (!oldState.find((existingStream) => existingStream.id === stream.id)) {
|
||||
if (oldState === null) return [{ stream, isAgent }];
|
||||
if (!oldState.find((existingStream) => existingStream.stream.id === stream.id)) {
|
||||
audioContextManager.mergeAudioStreams(stream);
|
||||
return [...oldState, stream];
|
||||
return [...oldState, { stream, isAgent }];
|
||||
}
|
||||
return oldState;
|
||||
});
|
||||
};
|
||||
|
||||
function call(additionalAgentIds?: string[]) {
|
||||
const removeIncomeStream = (stream: MediaStream) => {
|
||||
setIncomeStream((prevState) => {
|
||||
if (!prevState) return [];
|
||||
return prevState.filter((existingStream) => existingStream.stream.id !== stream.id);
|
||||
});
|
||||
};
|
||||
|
||||
function call() {
|
||||
RequestLocalStream()
|
||||
.then((lStream) => {
|
||||
setLocalStream(lStream);
|
||||
|
|
@ -150,16 +157,17 @@ function AssistActions({
|
|||
lStream,
|
||||
addIncomeStream,
|
||||
() => {
|
||||
player.assistManager.ping(AssistActionsPing.call.end, agentId);
|
||||
lStream.stop.bind(lStream);
|
||||
player.assistManager.ping(AssistActionsPing.call.end, agentId)
|
||||
lStream.stop.apply(lStream);
|
||||
removeIncomeStream(lStream.stream);
|
||||
},
|
||||
onReject,
|
||||
onError,
|
||||
);
|
||||
setCallObject(callPeer());
|
||||
if (additionalAgentIds) {
|
||||
callPeer(additionalAgentIds);
|
||||
}
|
||||
// if (additionalAgentIds) {
|
||||
// callPeer(additionalAgentIds);
|
||||
// }
|
||||
})
|
||||
.catch(onError);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,17 @@ interface Props {
|
|||
muted?: boolean;
|
||||
height?: number | string;
|
||||
setRemoteEnabled?: (isEnabled: boolean) => void;
|
||||
local?: boolean;
|
||||
isAgent?: boolean;
|
||||
}
|
||||
|
||||
function VideoContainer({
|
||||
stream, muted = false, height = 280, setRemoteEnabled,
|
||||
stream,
|
||||
muted = false,
|
||||
height = 280,
|
||||
setRemoteEnabled,
|
||||
local,
|
||||
isAgent,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const [isEnabled, setEnabled] = React.useState(false);
|
||||
|
|
@ -17,7 +24,7 @@ function VideoContainer({
|
|||
if (ref.current) {
|
||||
ref.current.srcObject = stream;
|
||||
}
|
||||
}, [ref.current, stream, stream.getVideoTracks()[0]?.getSettings().width]);
|
||||
}, [ref.current, stream, stream?.getVideoTracks()[0]?.getSettings().width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stream) {
|
||||
|
|
@ -49,9 +56,19 @@ function VideoContainer({
|
|||
width: isEnabled ? undefined : '0px!important',
|
||||
height: isEnabled ? undefined : '0px!important',
|
||||
border: '1px solid grey',
|
||||
transform: local ? 'scaleX(-1)' : undefined,
|
||||
}}
|
||||
>
|
||||
<video autoPlay ref={ref} muted={muted} style={{ height }} />
|
||||
<video autoPlay ref={ref} muted={muted} style={{ height: height }} />
|
||||
{isAgent ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
Agent
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function ProjectCaptureRate(props: Props) {
|
|||
const [conditions, setConditions] = React.useState<Conditions[]>([]);
|
||||
const { projectId, platform } = props.project;
|
||||
const isMobile = platform !== 'web';
|
||||
const { settingsStore, userStore } = useStore();
|
||||
const { settingsStore, userStore, customFieldStore } = useStore();
|
||||
const isAdmin = userStore.account.admin || userStore.account.superAdmin;
|
||||
const { isEnterprise } = userStore;
|
||||
const [changed, setChanged] = useState(false);
|
||||
|
|
@ -38,7 +38,13 @@ function ProjectCaptureRate(props: Props) {
|
|||
useEffect(() => {
|
||||
if (projectId) {
|
||||
setChanged(false);
|
||||
void fetchCaptureConditions(projectId);
|
||||
const fetchData = async () => {
|
||||
if (isEnterprise) {
|
||||
await customFieldStore.fetchListActive(projectId + '');
|
||||
}
|
||||
void fetchCaptureConditions(projectId);
|
||||
};
|
||||
void fetchData();
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import ProjectForm from 'Components/Client/Projects/ProjectForm';
|
|||
import Project from '@/mstore/types/project';
|
||||
|
||||
function Projects() {
|
||||
const { projectsStore } = useStore();
|
||||
const { projectsStore, customFieldStore } = useStore();
|
||||
const history = useHistory();
|
||||
const { project, pid, tab } = projectsStore.config;
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
|
@ -25,6 +25,10 @@ function Projects() {
|
|||
const tab = params.get('tab');
|
||||
projectsStore.setConfigProject(pid ? parseInt(pid) : undefined);
|
||||
projectsStore.setConfigTab(tab);
|
||||
|
||||
return () => {
|
||||
void customFieldStore.fetchListActive(projectsStore.activeSiteId + '');
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ function WidgetSessions(props: Props) {
|
|||
if (!widget.series) return;
|
||||
const seriesOptions = widget.series.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.seriesId,
|
||||
value: item.seriesId ?? item.name
|
||||
}));
|
||||
setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]);
|
||||
}, [widget.series]);
|
||||
}, [widget.series.length]);
|
||||
|
||||
const fetchSessions = (metricId: any, filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
|
|
|
|||
|
|
@ -88,13 +88,16 @@ function FunnelWidget(props: Props) {
|
|||
const viewType = metric?.viewType;
|
||||
const isHorizontal = viewType === 'columnChart';
|
||||
const noEvents = metric.series[0].filter.filters.length === 0;
|
||||
const isUsers = metric?.metricFormat === 'userCount';
|
||||
return (
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
title={(
|
||||
<div className="flex items-center text-lg">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
{noEvents ? 'Select an event to start seeing the funnel' : 'No data available for the selected period.'}
|
||||
{noEvents
|
||||
? 'Select an event to start seeing the funnel'
|
||||
: 'No data available for the selected period.'}
|
||||
</div>
|
||||
)}
|
||||
show={!stages || stages.length === 0}
|
||||
|
|
@ -140,7 +143,7 @@ function FunnelWidget(props: Props) {
|
|||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Total conversion</span>
|
||||
<Tooltip
|
||||
title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}
|
||||
title={`${funnel.totalConversions} ${isUsers ? 'Users' : 'Sessions'} ${funnel.totalConversionsPercentage}%`}
|
||||
>
|
||||
<Tag
|
||||
bordered={false}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function LivePlayerBlockHeader({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds} />
|
||||
<AssistActions userId={userId} isCallActive={isCallActive} agentIds={agentIds ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ export default class WebLivePlayer extends WebPlayer {
|
|||
config,
|
||||
wpState,
|
||||
(id) => this.messageManager.getNode(id),
|
||||
uiErrorHandler,
|
||||
agentId,
|
||||
uiErrorHandler
|
||||
);
|
||||
this.assistManager.connect(session.agentToken!, agentId, projectId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export default class AssistManager {
|
|||
...RemoteControl.INITIAL_STATE,
|
||||
...ScreenRecording.INITIAL_STATE,
|
||||
};
|
||||
private agentIds: string[] = [];
|
||||
|
||||
// TODO: Session type
|
||||
constructor(
|
||||
|
|
@ -79,6 +80,7 @@ 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;
|
||||
},
|
||||
|
|
@ -198,6 +200,12 @@ export default class AssistManager {
|
|||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// socket.onAny((event, ...args) => {
|
||||
// logger.log(`📩 Socket: ${event}`, args);
|
||||
// });
|
||||
|
||||
|
||||
socket.on('connect', () => {
|
||||
waitingForMessages = true;
|
||||
// TODO: reconnect happens frequently on bad network
|
||||
|
|
@ -274,6 +282,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;
|
||||
|
|
@ -295,6 +307,11 @@ export default class AssistManager {
|
|||
this.config,
|
||||
this.peerID,
|
||||
this.getAssistVersion,
|
||||
{
|
||||
...this.session.agentInfo,
|
||||
id: agentId,
|
||||
},
|
||||
this.agentIds,
|
||||
);
|
||||
this.remoteControl = new RemoteControl(
|
||||
this.store,
|
||||
|
|
@ -315,7 +332,7 @@ export default class AssistManager {
|
|||
this.canvasReceiver = new CanvasReceiver(this.peerID, this.config, this.getNode, {
|
||||
...this.session.agentInfo,
|
||||
id: agentId,
|
||||
});
|
||||
}, socket);
|
||||
|
||||
document.addEventListener('visibilitychange', this.onVisChange);
|
||||
});
|
||||
|
|
@ -324,7 +341,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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import type Peer from 'peerjs';
|
||||
import type { MediaConnection } from 'peerjs';
|
||||
|
||||
import { userStore } from 'App/mstore';
|
||||
import type { LocalStream } from './LocalStream';
|
||||
import type { Socket } from './types';
|
||||
import type { Store } from '../../common/types';
|
||||
import { userStore } from "App/mstore";
|
||||
import logger from '@/logger';
|
||||
|
||||
export enum CallingState {
|
||||
NoCall,
|
||||
|
|
@ -19,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;
|
||||
|
||||
|
|
@ -26,13 +30,11 @@ export default class Call {
|
|||
calling: CallingState.NoCall,
|
||||
};
|
||||
|
||||
private _peer: Peer | null = null;
|
||||
|
||||
private connectionAttempts: number = 0;
|
||||
|
||||
private callConnection: MediaConnection[] = [];
|
||||
|
||||
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> }>,
|
||||
|
|
@ -40,11 +42,35 @@ export default class Call {
|
|||
private config: RTCIceServer[] | null,
|
||||
private peerID: string,
|
||||
private getAssistVersion: () => number,
|
||||
private agent: Record<string, any>,
|
||||
private agentIds: string[],
|
||||
) {
|
||||
|
||||
socket.on('WEBRTC_AGENT_CALL', (data) => {
|
||||
switch (data.type) {
|
||||
case WEBRTC_CALL_AGENT_EVENT_TYPES.OFFER:
|
||||
this.handleOffer(data, true);
|
||||
break;
|
||||
case WEBRTC_CALL_AGENT_EVENT_TYPES.ICE_CANDIDATE:
|
||||
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[] }}) => {
|
||||
this.callAgentsInSession({ agentIds: data.data.agentIds });
|
||||
});
|
||||
|
||||
socket.on('call_end', () => {
|
||||
this.onRemoteCallEnd();
|
||||
});
|
||||
socket.on('videofeed', ({ streamId, enabled }) => {
|
||||
|
||||
socket.on('videofeed', (data: { data: { streamId: string; enabled: boolean }}) => {
|
||||
const { streamId, enabled } = data.data;
|
||||
if (this.videoStreams[streamId]) {
|
||||
this.videoStreams[streamId].enabled = enabled;
|
||||
}
|
||||
|
|
@ -60,14 +86,13 @@ export default class Call {
|
|||
});
|
||||
socket.on('messages_gz', () => {
|
||||
if (reconnecting) {
|
||||
// 'messages' come frequently, so it is better to have Reconnecting
|
||||
// When the connection is restored, we initiate a re-creation of the connection
|
||||
this._callSessionPeer();
|
||||
reconnecting = false;
|
||||
}
|
||||
});
|
||||
socket.on('messages', () => {
|
||||
if (reconnecting) {
|
||||
// 'messages' come frequently, so it is better to have Reconnecting
|
||||
this._callSessionPeer();
|
||||
reconnecting = false;
|
||||
}
|
||||
|
|
@ -75,111 +100,239 @@ export default class Call {
|
|||
socket.on('disconnect', () => {
|
||||
this.store.update({ calling: CallingState.NoCall });
|
||||
});
|
||||
|
||||
socket.on('webrtc_call_offer', (data: { data: { from: string, offer: RTCSessionDescriptionInit } }) => {
|
||||
this.handleOffer(data.data);
|
||||
});
|
||||
|
||||
socket.on('webrtc_call_answer', (data: { data: { from: string, answer: RTCSessionDescriptionInit } }) => {
|
||||
this.handleAnswer(data.data);
|
||||
});
|
||||
socket.on('webrtc_call_ice_candidate', (data: { data: { from: string, candidate: RTCIceCandidateInit } }) => {
|
||||
this.handleIceCandidate({ candidate: data.data.candidate, from: data.data.from });
|
||||
});
|
||||
|
||||
this.assistVersion = this.getAssistVersion();
|
||||
}
|
||||
|
||||
private getPeer(): Promise<Peer> {
|
||||
if (this._peer && !this._peer.disconnected) {
|
||||
return Promise.resolve(this._peer);
|
||||
// CREATE A LOCAL PEER
|
||||
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 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) => {
|
||||
pc.addTrack(track, this.callArgs!.localStream.stream);
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const urlObject = new URL(window.env.API_EDP || window.location.origin);
|
||||
|
||||
// @ts-ignore TODO: set module in ts settings
|
||||
return import('peerjs').then(({ default: Peer }) => {
|
||||
if (this.cleaned) {
|
||||
return Promise.reject('Already cleaned');
|
||||
}
|
||||
const peerOpts: Peer.PeerJSOption = {
|
||||
host: urlObject.hostname,
|
||||
path: '/assist',
|
||||
port:
|
||||
urlObject.port === ''
|
||||
? location.protocol === 'https:'
|
||||
? 443
|
||||
: 80
|
||||
: parseInt(urlObject.port),
|
||||
};
|
||||
if (this.config) {
|
||||
peerOpts.config = {
|
||||
iceServers: this.config,
|
||||
// @ts-ignore
|
||||
sdpSemantics: 'unified-plan',
|
||||
iceTransportPolicy: 'all',
|
||||
};
|
||||
}
|
||||
const peer = (this._peer = new Peer(peerOpts));
|
||||
peer.on('call', (call) => {
|
||||
console.log('getting call from', call.peer);
|
||||
call.answer(this.callArgs?.localStream.stream);
|
||||
this.callConnection.push(call);
|
||||
|
||||
this.callArgs?.localStream.onVideoTrack((vTrack) => {
|
||||
const sender = call.peerConnection.getSenders().find((s) => s.track?.kind === 'video');
|
||||
if (!sender) {
|
||||
console.warn('No video sender found');
|
||||
return;
|
||||
}
|
||||
sender.replaceTrack(vTrack);
|
||||
});
|
||||
|
||||
call.on('stream', (stream) => {
|
||||
this.videoStreams[call.peer] = stream.getVideoTracks()[0];
|
||||
this.callArgs && this.callArgs.onStream(stream);
|
||||
});
|
||||
|
||||
call.on('close', this.onRemoteCallEnd);
|
||||
call.on('error', (e) => {
|
||||
console.error('PeerJS error (on call):', e);
|
||||
this.initiateCallEnd();
|
||||
this.callArgs && this.callArgs.onError && this.callArgs.onError();
|
||||
});
|
||||
});
|
||||
peer.on('error', (e) => {
|
||||
if (e.type === 'disconnected') {
|
||||
return peer.reconnect();
|
||||
} if (e.type !== 'peer-unavailable') {
|
||||
console.error(`PeerJS error (on peer). Type ${e.type}`, e);
|
||||
// when ice is ready we send it
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
if (isAgent) {
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
peer.on('open', () => resolve(peer));
|
||||
// when we receive a remote track, we write it to videoStreams[peerId]
|
||||
pc.ontrack = (event) => {
|
||||
const stream = event.streams[0];
|
||||
if (stream && !this.videoStreams[remotePeerId]) {
|
||||
const clonnedStream = stream.clone();
|
||||
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, remotePeerId !== this.callID && isAgentId(remotePeerId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If the connection is lost, we end the call
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState === "disconnected" || pc.connectionState === "failed") {
|
||||
this.onRemoteCallEnd();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle track replacement when local video changes
|
||||
if (this.callArgs && this.callArgs.localStream) {
|
||||
this.callArgs.localStream.onVideoTrack((vTrack: MediaStreamTrack) => {
|
||||
const sender = pc.getSenders().find((s) => s.track?.kind === 'video');
|
||||
if (!sender) {
|
||||
logger.warn('No video sender found');
|
||||
return;
|
||||
}
|
||||
sender.replaceTrack(vTrack);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
// ESTABLISHING A CONNECTION
|
||||
private async _peerConnection({ remotePeerId, isAgent, socketId, localPeerId }: { remotePeerId: string, isAgent?: boolean, socketId?: string, localPeerId?: string }) {
|
||||
try {
|
||||
// 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);
|
||||
|
||||
// Sending offer
|
||||
if (isAgent) {
|
||||
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 });
|
||||
}
|
||||
this.connectAttempts = 0;
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
// Trying to reconnect
|
||||
const tryReconnect = async (error: any) => {
|
||||
if (error.type === 'peer-unavailable' && this.connectAttempts < 5) {
|
||||
this.connectAttempts++;
|
||||
logger.log('reconnecting', this.connectAttempts);
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await this._peerConnection({ remotePeerId });
|
||||
} else {
|
||||
logger.log('error', this.connectAttempts);
|
||||
this.callArgs?.onError?.('Could not establish a connection with the peer after 5 attempts');
|
||||
}
|
||||
};
|
||||
await tryReconnect(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
if (!pc) {
|
||||
if (isAgent) {
|
||||
this.connections[fromCallId] = await this.createPeerConnection({ remotePeerId: fromCallId, isAgent, localPeerId: this.callID });
|
||||
pc = this.connections[fromCallId];
|
||||
} 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.localDescription) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
if (isAgent) {
|
||||
this.socket.emit('WEBRTC_AGENT_CALL', { from: this.callID, answer, toAgentId: getSocketIdByCallId(fromCallId), type: WEBRTC_CALL_AGENT_EVENT_TYPES.ANSWER });
|
||||
} else {
|
||||
this.socket.emit('webrtc_call_answer', { from: fromCallId, answer });
|
||||
}
|
||||
} else {
|
||||
logger.warn("Skipping setRemoteDescription: Already in stable state");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error setting remote description from answer", e);
|
||||
this.callArgs?.onError?.(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Process the received answer to offer
|
||||
private async handleAnswer(data: { from: string, answer: RTCSessionDescriptionInit }, isAgent?: boolean) {
|
||||
// set to remotePeerId data.from
|
||||
logger.log("RECEIVED ANSWER", data);
|
||||
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, this.connections);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// if the connection is not established yet, then set remoteDescription to peer
|
||||
if (pc.signalingState !== "stable") {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
|
||||
} else {
|
||||
logger.warn("Skipping setRemoteDescription: Already in stable state");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error setting remote description from answer", e);
|
||||
this.callArgs?.onError?.(e);
|
||||
}
|
||||
}
|
||||
|
||||
// process the received iceCandidate
|
||||
private async handleIceCandidate(data: { from: string, candidate: RTCIceCandidateInit }) {
|
||||
const callId = data.from;
|
||||
const pc = this.connections[callId];
|
||||
if (!pc) return;
|
||||
// if there are ice candidates then add candidate to peer
|
||||
if (data.candidate && (data.candidate.sdpMid || data.candidate.sdpMLineIndex !== null)) {
|
||||
try {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||
} catch (e) {
|
||||
logger.error("Error adding ICE candidate", e);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Invalid ICE candidate skipped:", data.candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// handle call ends
|
||||
private handleCallEnd() {
|
||||
if (this.store.get().calling !== CallingState.NoCall) this.callArgs && this.callArgs.onCallEnd();
|
||||
// If the call is not completed, then call onCallEnd
|
||||
if (this.store.get().calling !== CallingState.NoCall) {
|
||||
this.callArgs && this.callArgs.onCallEnd();
|
||||
}
|
||||
// change state to NoCall
|
||||
this.store.update({ calling: CallingState.NoCall });
|
||||
this.callConnection[0] && this.callConnection[0].close();
|
||||
// Close all created RTCPeerConnection
|
||||
Object.values(this.connections).forEach((pc) => pc.close());
|
||||
this.callArgs?.onCallEnd();
|
||||
// Clear connections
|
||||
this.connections = {};
|
||||
this.callArgs = null;
|
||||
this.videoStreams = {};
|
||||
this.callArgs = null;
|
||||
// TODO: We have it separated, right? (check)
|
||||
// this.toggleAnnotation(false)
|
||||
}
|
||||
|
||||
// Call completion event handler by signal
|
||||
private onRemoteCallEnd = () => {
|
||||
if ([CallingState.Requesting, CallingState.Connecting].includes(this.store.get().calling)) {
|
||||
// If the call has not started yet, then call onReject
|
||||
this.callArgs && this.callArgs.onReject();
|
||||
this.callConnection[0] && this.callConnection[0].close();
|
||||
// Close all connections and reset callArgs
|
||||
Object.values(this.connections).forEach((pc) => pc.close());
|
||||
this.connections = {};
|
||||
this.callArgs?.onCallEnd();
|
||||
this.store.update({ calling: CallingState.NoCall });
|
||||
this.callArgs = null;
|
||||
} else {
|
||||
// Call the full call completion handler
|
||||
this.handleCallEnd();
|
||||
}
|
||||
};
|
||||
|
||||
// Ends the call and sends the call_end signal
|
||||
initiateCallEnd = async () => {
|
||||
const userName = userStore.account.name;
|
||||
this.emitData('call_end', userName);
|
||||
this.emitData('call_end', this.callID);
|
||||
this.handleCallEnd();
|
||||
// TODO: We have it separated, right? (check)
|
||||
// const remoteControl = this.store.get().remoteControl
|
||||
// if (remoteControl === RemoteControlStatus.Enabled) {
|
||||
// this.socket.emit("release_control")
|
||||
// this.toggleRemoteControl(false)
|
||||
// }
|
||||
};
|
||||
|
||||
private emitData = (event: string, data?: any) => {
|
||||
|
|
@ -192,7 +345,7 @@ export default class Call {
|
|||
|
||||
private callArgs: {
|
||||
localStream: LocalStream;
|
||||
onStream: (s: MediaStream) => void;
|
||||
onStream: (s: MediaStream, isAgent: boolean) => void;
|
||||
onCallEnd: () => void;
|
||||
onReject: () => void;
|
||||
onError?: (arg?: any) => void;
|
||||
|
|
@ -200,7 +353,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,
|
||||
|
|
@ -214,117 +367,88 @@ export default class Call {
|
|||
};
|
||||
}
|
||||
|
||||
call(thirdPartyPeers?: string[]): { end: () => void } {
|
||||
if (thirdPartyPeers && thirdPartyPeers.length > 0) {
|
||||
this.addPeerCall(thirdPartyPeers);
|
||||
} else {
|
||||
this._callSessionPeer();
|
||||
}
|
||||
// Initiates a call
|
||||
call(): { end: () => void } {
|
||||
this._callSessionPeer();
|
||||
// this.callAgentsInSession({ agentIds: this.agentInCallIds });
|
||||
return {
|
||||
end: this.initiateCallEnd,
|
||||
};
|
||||
}
|
||||
|
||||
// Notify peers of local video state change
|
||||
toggleVideoLocalStream(enabled: boolean) {
|
||||
this.getPeer().then((peer) => {
|
||||
this.emitData('videofeed', { streamId: peer.id, enabled });
|
||||
});
|
||||
this.emitData('videofeed', { streamId: this.callID, enabled });
|
||||
}
|
||||
|
||||
/** Connecting to the other agents that are already
|
||||
* in the call with the user
|
||||
*/
|
||||
addPeerCall(thirdPartyPeers: string[]) {
|
||||
thirdPartyPeers.forEach((peer) => this._peerConnection(peer));
|
||||
}
|
||||
|
||||
/** Connecting to the app user */
|
||||
// Calls the method to create a connection with a peer
|
||||
private _callSessionPeer() {
|
||||
if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) {
|
||||
return;
|
||||
}
|
||||
this.store.update({ calling: CallingState.Connecting });
|
||||
const tab = this.store.get().currentTab;
|
||||
if (!this.store.get().currentTab) {
|
||||
console.warn('No tab data to connect to peer');
|
||||
if (!tab) {
|
||||
logger.warn('No tab data to connect to peer');
|
||||
}
|
||||
const peerId = this.getAssistVersion() === 1
|
||||
? this.peerID
|
||||
: `${this.peerID}-${tab || Object.keys(this.store.get().tabs)[0]}`;
|
||||
|
||||
// Generate a peer identifier depending on the assist version
|
||||
this.callID = this.getCallId();
|
||||
|
||||
const userName = userStore.account.name;
|
||||
this.emitData('_agent_name', userName);
|
||||
void this._peerConnection(peerId);
|
||||
void this._peerConnection({ remotePeerId: this.callID });
|
||||
}
|
||||
|
||||
connectAttempts = 0;
|
||||
|
||||
private async _peerConnection(remotePeerId: string) {
|
||||
try {
|
||||
const peer = await this.getPeer();
|
||||
// let canCall = false
|
||||
|
||||
const tryReconnect = async (e: any) => {
|
||||
peer.off('error', tryReconnect);
|
||||
console.log(e.type, this.connectAttempts);
|
||||
if (e.type === 'peer-unavailable' && this.connectAttempts < 5) {
|
||||
this.connectAttempts++;
|
||||
console.log('reconnecting', this.connectAttempts);
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await this._peerConnection(remotePeerId);
|
||||
} else {
|
||||
console.log('error', this.connectAttempts);
|
||||
this.callArgs?.onError?.('Could not establish a connection with the peer after 5 attempts');
|
||||
}
|
||||
};
|
||||
const call = peer.call(remotePeerId, this.callArgs!.localStream.stream);
|
||||
peer.on('error', tryReconnect);
|
||||
|
||||
peer.on('connection', () => {
|
||||
this.callConnection.push(call);
|
||||
this.connectAttempts = 0;
|
||||
|
||||
this.callArgs?.localStream.onVideoTrack((vTrack) => {
|
||||
const sender = call.peerConnection.getSenders().find((s) => s.track?.kind === 'video');
|
||||
if (!sender) {
|
||||
console.warn('No video sender found');
|
||||
return;
|
||||
}
|
||||
sender.replaceTrack(vTrack);
|
||||
private callAgentsInSession({ agentIds }: { agentIds: string[] }) {
|
||||
if (agentIds) {
|
||||
const filteredAgentIds = agentIds.filter((id: string) => id.split('-')[3] !== this.agent.id.toString());
|
||||
const newIds = filteredAgentIds.filter((id: string) => !this.agentInCallIds.includes(id));
|
||||
const removedIds = this.agentInCallIds.filter((id: string) => !filteredAgentIds.includes(id));
|
||||
removedIds.forEach((id: string) => this.agentDisconnected(id));
|
||||
if (this.store.get().calling === CallingState.OnCall) {
|
||||
newIds.forEach((id: string) => {
|
||||
const socketId = getSocketIdByCallId(id);
|
||||
this._peerConnection({ remotePeerId: id, isAgent: true, socketId, localPeerId: this.callID });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
call.on('stream', (stream) => {
|
||||
this.store.get().calling !== CallingState.OnCall
|
||||
&& this.store.update({ calling: CallingState.OnCall });
|
||||
|
||||
this.videoStreams[call.peer] = stream.getVideoTracks()[0];
|
||||
|
||||
this.callArgs && this.callArgs.onStream(stream);
|
||||
});
|
||||
|
||||
call.on('close', this.onRemoteCallEnd);
|
||||
call.on('error', (e) => {
|
||||
console.error('PeerJS error (on call):', e);
|
||||
this.initiateCallEnd();
|
||||
this.callArgs && this.callArgs.onError && this.callArgs.onError();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.agentInCallIds = filteredAgentIds;
|
||||
}
|
||||
}
|
||||
|
||||
private cleaned: boolean = false;
|
||||
|
||||
clean() {
|
||||
this.cleaned = true; // sometimes cleaned before modules loaded
|
||||
void this.initiateCallEnd();
|
||||
if (this._peer) {
|
||||
console.log('destroying peer...');
|
||||
const peer = this._peer; // otherwise it calls reconnection on data chan close
|
||||
this._peer = null;
|
||||
peer.disconnect();
|
||||
peer.destroy();
|
||||
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) {
|
||||
this.connections[agentId]?.close();
|
||||
delete this.connections[agentId];
|
||||
}
|
||||
|
||||
// Method for clearing resources
|
||||
clean() {
|
||||
void this.initiateCallEnd();
|
||||
Object.values(this.connections).forEach((pc) => pc.close());
|
||||
this.connections = {};
|
||||
this.callArgs?.onCallEnd();
|
||||
}
|
||||
}
|
||||
|
||||
function isAgentId(id: string): boolean {
|
||||
return id.endsWith('_agent');
|
||||
}
|
||||
|
||||
function getSocketIdByCallId(callId?: string): string | undefined {
|
||||
const socketIdRegex = /-\d{2}-(.*?)\-agent/;
|
||||
const match = callId?.match(socketIdRegex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Peer from 'peerjs';
|
||||
import { VElement } from 'Player/web/managers/DOM/VirtualDOM';
|
||||
import MessageManager from 'Player/web/MessageManager';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
let frameCounter = 0;
|
||||
|
||||
|
|
@ -18,71 +18,102 @@ function draw(
|
|||
|
||||
export default class CanvasReceiver {
|
||||
private streams: Map<string, MediaStream> = new Map();
|
||||
// Store RTCPeerConnection for each remote peer
|
||||
private connections: Map<string, RTCPeerConnection> = new Map();
|
||||
private cId: string;
|
||||
|
||||
private peer: Peer | null = null;
|
||||
|
||||
// sendSignal – for sending signals (offer/answer/ICE)
|
||||
constructor(
|
||||
private readonly peerIdPrefix: string,
|
||||
private readonly config: RTCIceServer[] | null,
|
||||
private readonly getNode: MessageManager['getNode'],
|
||||
private readonly agentInfo: Record<string, any>,
|
||||
private readonly socket: Socket,
|
||||
) {
|
||||
// @ts-ignore
|
||||
const urlObject = new URL(window.env.API_EDP || window.location.origin);
|
||||
const peerOpts: Peer.PeerJSOption = {
|
||||
host: urlObject.hostname,
|
||||
path: '/assist',
|
||||
port:
|
||||
urlObject.port === ''
|
||||
? location.protocol === 'https:'
|
||||
? 443
|
||||
: 80
|
||||
: parseInt(urlObject.port),
|
||||
// Form an id like in PeerJS
|
||||
this.cId = `${this.peerIdPrefix}-${this.agentInfo.id}-canvas`;
|
||||
|
||||
this.socket.on('webrtc_canvas_offer', (data: { data: { offer: RTCSessionDescriptionInit, id: string }}) => {
|
||||
const { offer, id } = data.data;
|
||||
if (checkId(id, this.cId)) {
|
||||
this.handleOffer(offer, id);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('webrtc_canvas_ice_candidate', (data: { data: { candidate: RTCIceCandidateInit, id: string }}) => {
|
||||
const {candidate, id } = data.data;
|
||||
if (checkId(id, this.cId)) {
|
||||
this.handleCandidate(candidate, id);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('webrtc_canvas_restart', () => {
|
||||
this.clear();
|
||||
});
|
||||
}
|
||||
|
||||
async handleOffer(offer: RTCSessionDescriptionInit, id: string): Promise<void> {
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: this.config ? this.config : [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
});
|
||||
|
||||
// Save the connection
|
||||
this.connections.set(id, pc);
|
||||
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.socket.emit('webrtc_canvas_ice_candidate', ({ candidate: event.candidate, id }));
|
||||
}
|
||||
};
|
||||
if (this.config) {
|
||||
peerOpts.config = {
|
||||
iceServers: this.config,
|
||||
// @ts-ignore
|
||||
sdpSemantics: 'unified-plan',
|
||||
iceTransportPolicy: 'all',
|
||||
};
|
||||
}
|
||||
const id = `${this.peerIdPrefix}-${this.agentInfo.id}-canvas`;
|
||||
const canvasPeer = new Peer(id, peerOpts);
|
||||
this.peer = canvasPeer;
|
||||
canvasPeer.on('error', (err) => console.error('canvas peer error', err));
|
||||
canvasPeer.on('call', (call) => {
|
||||
call.answer();
|
||||
const canvasId = call.peer.split('-')[2];
|
||||
call.on('stream', (stream) => {
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
|
||||
const stream = event.streams[0];
|
||||
if (stream) {
|
||||
// Detect canvasId from remote peer id
|
||||
const canvasId = id.split('-')[4];
|
||||
this.streams.set(canvasId, stream);
|
||||
setTimeout(() => {
|
||||
const node = this.getNode(parseInt(canvasId, 10));
|
||||
const videoEl = spawnVideo(
|
||||
this.streams.get(canvasId)?.clone() as MediaStream,
|
||||
node as VElement,
|
||||
);
|
||||
const videoEl = spawnVideo(stream.clone() as MediaStream, node as VElement);
|
||||
if (node) {
|
||||
draw(
|
||||
videoEl,
|
||||
node.node as HTMLCanvasElement,
|
||||
(node.node as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D,
|
||||
);
|
||||
} else {
|
||||
console.log('NODE', canvasId, 'IS NOT FOUND');
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
call.on('error', (err) => console.error('canvas call error', err));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
|
||||
this.socket.emit('webrtc_canvas_answer', { answer: answer, id });
|
||||
}
|
||||
|
||||
async handleCandidate(candidate: RTCIceCandidateInit, id: string): Promise<void> {
|
||||
const pc = this.connections.get(id);
|
||||
if (pc) {
|
||||
try {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
||||
} catch (e) {
|
||||
console.error('Error adding ICE candidate', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.peer) {
|
||||
// otherwise it calls reconnection on data chan close
|
||||
const { peer } = this;
|
||||
this.peer = null;
|
||||
peer.disconnect();
|
||||
peer.destroy();
|
||||
}
|
||||
this.connections.forEach((pc) => {
|
||||
pc.close();
|
||||
});
|
||||
this.connections.clear();
|
||||
this.streams.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +189,10 @@ function spawnDebugVideo(stream: MediaStream, node: VElement) {
|
|||
});
|
||||
}
|
||||
|
||||
function checkId(id: string, cId: string): boolean {
|
||||
return id.includes(cId);
|
||||
}
|
||||
|
||||
/** simple peer example
|
||||
* // @ts-ignore
|
||||
* const peer = new SLPeer({ initiator: false })
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@
|
|||
"mobx": "^6.13.3",
|
||||
"mobx-persist-store": "^1.1.5",
|
||||
"mobx-react-lite": "^4.0.7",
|
||||
"peerjs": "1.3.2",
|
||||
"prismjs": "^1.29.0",
|
||||
"rc-time-picker": "^3.7.3",
|
||||
"react": "^18.2.0",
|
||||
|
|
|
|||
|
|
@ -3551,13 +3551,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^10.14.33":
|
||||
version: 10.17.60
|
||||
resolution: "@types/node@npm:10.17.60"
|
||||
checksum: 10c1/63eca0b871718af75e369cd2a57e70939ff347f4b1fbd98dca40d005262ab48021e3829f58ed5e828d0886e98054cfa354648118712d28779eb4a0f855e70a93
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/parse-json@npm:^4.0.0":
|
||||
version: 4.0.2
|
||||
resolution: "@types/parse-json@npm:4.0.2"
|
||||
|
|
@ -6963,9 +6956,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"electron-to-chromium@npm:^1.5.73":
|
||||
version: 1.5.105
|
||||
resolution: "electron-to-chromium@npm:1.5.105"
|
||||
checksum: 10c1/01ff737ebdeb0554ece250c91b913634f4af811ed8e18ccd3ca791af925483631278878b58a5a8d28ce41616d7f6f15f695083a0e7550bb84f27ff82b009a00e
|
||||
version: 1.5.107
|
||||
resolution: "electron-to-chromium@npm:1.5.107"
|
||||
checksum: 10c1/9846b40660331f1d4dc92e3980d5cd7e03ae68ff919dfd6bf1615c6f8ae3dcf4a54e4c14793c981aa4b53001067f57bf0a4937371bc4651ca56b554b4c608b53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -7819,13 +7812,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "eventemitter3@npm:3.1.2"
|
||||
checksum: 10c1/e177ace79a8f6d9884adad7d820f819132c5ec6e9d88e598043943f1951000901ad871f79dbd385fc980c7720b9f5644a2a12e95a767310a00401b304839f999
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1":
|
||||
version: 4.0.7
|
||||
resolution: "eventemitter3@npm:4.0.7"
|
||||
|
|
@ -11391,8 +11377,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"minipass-fetch@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "minipass-fetch@npm:4.0.0"
|
||||
version: 4.0.1
|
||||
resolution: "minipass-fetch@npm:4.0.1"
|
||||
dependencies:
|
||||
encoding: "npm:^0.1.13"
|
||||
minipass: "npm:^7.0.3"
|
||||
|
|
@ -11401,7 +11387,7 @@ __metadata:
|
|||
dependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
checksum: 10c1/5831cf620b43a062ae180be3d59b7e7d694c4ae4d71a5ed6edb39a0bfe0c8cbb3aba6b95d9e61cb04bcc2ea112924f9f653a06a6e2de26bb9c92f37c32795b2c
|
||||
checksum: 10c1/33a9c0e55d0bac4c5c5df926ff4bc7622b4cead27f7b42c077555e7ee892801a15ae933b698638f4952775ada91397a2a0617da9e729a8063dcc48184370cfcc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -12058,7 +12044,6 @@ __metadata:
|
|||
mobx-persist-store: "npm:^1.1.5"
|
||||
mobx-react-lite: "npm:^4.0.7"
|
||||
node-gyp: "npm:^9.0.0"
|
||||
peerjs: "npm:1.3.2"
|
||||
postcss: "npm:^8.4.48"
|
||||
postcss-import: "npm:^16.1.0"
|
||||
postcss-loader: "npm:^8.1.1"
|
||||
|
|
@ -12410,25 +12395,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"peerjs-js-binarypack@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "peerjs-js-binarypack@npm:1.0.1"
|
||||
checksum: 10c1/5670269e0a61400151f99727c53956c2971a6d5999a6ba4684b565cc3d4429b81fd04cb45b040677192d58537a79df1eadb2a7379cb1ffd0b04f01e6b292ef48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"peerjs@npm:1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "peerjs@npm:1.3.2"
|
||||
dependencies:
|
||||
"@types/node": "npm:^10.14.33"
|
||||
eventemitter3: "npm:^3.1.2"
|
||||
peerjs-js-binarypack: "npm:1.0.1"
|
||||
webrtc-adapter: "npm:^7.7.1"
|
||||
checksum: 10c1/fdbe1794fdab77b2d610ad1dd8fdf705aa4f128fc1c62cab861e8fb99a1255a71199701d6cd4a42d8a82047e4b59710848ff0fe1131aad3652d66665c0a8dffa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pend@npm:~1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "pend@npm:1.2.0"
|
||||
|
|
@ -13599,8 +13565,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"rc-picker@npm:~4.11.2":
|
||||
version: 4.11.2
|
||||
resolution: "rc-picker@npm:4.11.2"
|
||||
version: 4.11.3
|
||||
resolution: "rc-picker@npm:4.11.3"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.24.7"
|
||||
"@rc-component/trigger": "npm:^2.0.0"
|
||||
|
|
@ -13624,7 +13590,7 @@ __metadata:
|
|||
optional: true
|
||||
moment:
|
||||
optional: true
|
||||
checksum: 10c1/b7d175c9b5ce7f4f6f003064362407c8977c73486653f253290427fe59e6e21c6abde291ed00e70856cedde4d34450e2bb2a4ca5423fae732413075578531670
|
||||
checksum: 10c1/508edfdeaecdd4eee26784cd955d4497f0a380269ae2e0c52d4913215e28e59b93a410f630b8a848f248ed387356e65c5e1ebb2636c4764dbb808cd83baab217
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -14733,15 +14699,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rtcpeerconnection-shim@npm:^1.2.15":
|
||||
version: 1.2.15
|
||||
resolution: "rtcpeerconnection-shim@npm:1.2.15"
|
||||
dependencies:
|
||||
sdp: "npm:^2.6.0"
|
||||
checksum: 10c1/75b9e83fab08bb9eb90db61f05861db2a6a96f7abae9971359965b0cfd195a22227d8782b3b0fd6897e6a7501d63eee5fb3c92f030245cc421d2e745e99aaa7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"run-applescript@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "run-applescript@npm:7.0.0"
|
||||
|
|
@ -14920,13 +14877,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sdp@npm:^2.12.0, sdp@npm:^2.6.0":
|
||||
version: 2.12.0
|
||||
resolution: "sdp@npm:2.12.0"
|
||||
checksum: 10c1/68ea1f4b58672b5a6e381bd08e4f0b66bc2a6f70b3dad06f05e0d44d4209e1ae7615159570f4f112179b65dbd855778b2b68ab44471cc0b28666335641332a2c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"select-hose@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "select-hose@npm:2.0.0"
|
||||
|
|
@ -15725,9 +15675,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"strnum@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "strnum@npm:1.1.1"
|
||||
checksum: 10c1/8559a9fbc2c7733f2b4e6aa6ffa5df2a5974c693c8a841694f6e3d3d2c0aec5cecfb090f106c51d143f1961969cb757e0442553278fea94293fbaa27c2ba9f35
|
||||
version: 1.1.2
|
||||
resolution: "strnum@npm:1.1.2"
|
||||
checksum: 10c1/e63cd5379e06167478f2d8b5729c3c9960a706a91c9e10135dd7349f2254dadf4733f920846144c1c420086815dffd029c01894f8d29dcfd80337b7e38dbee63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -16146,21 +16096,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tldts-core@npm:^6.1.78":
|
||||
version: 6.1.78
|
||||
resolution: "tldts-core@npm:6.1.78"
|
||||
checksum: 10c1/6327860e5fc75119c5256bcba389163b0eab419cdc4cc516b4c237a4f8b78c4711855b18f2fd1e87365adb1760397cdb0b5b18a8d4582a7844a72da0077d4a86
|
||||
"tldts-core@npm:^6.1.79":
|
||||
version: 6.1.79
|
||||
resolution: "tldts-core@npm:6.1.79"
|
||||
checksum: 10c1/b7c580c52fbddb054c07b711665f434f77e1831122db2d12dd875862fbdb036db6e003e6c731b2cce6ebd22cf6afb501e8de27d6bdb115c7aba25e73823d7f11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tldts@npm:^6.1.32":
|
||||
version: 6.1.78
|
||||
resolution: "tldts@npm:6.1.78"
|
||||
version: 6.1.79
|
||||
resolution: "tldts@npm:6.1.79"
|
||||
dependencies:
|
||||
tldts-core: "npm:^6.1.78"
|
||||
tldts-core: "npm:^6.1.79"
|
||||
bin:
|
||||
tldts: bin/cli.js
|
||||
checksum: 10c1/abde0fd838874eeeddea6c23ca6c6ef59d2b26451ba0895f7919d4e8601821413bfe34e2f6e58bad4639cb9ee5e0366a7bac75f67c01dc167d1579ae38f2228e
|
||||
checksum: 10c1/99297d3529d12a6dff29a2ce41c44c6254507c882ce650f54fb7113207a5e94e657836857cb95203584e325069101a887501a7e13773659050a47724ede08909
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -16652,8 +16602,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"update-browserslist-db@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "update-browserslist-db@npm:1.1.2"
|
||||
version: 1.1.3
|
||||
resolution: "update-browserslist-db@npm:1.1.3"
|
||||
dependencies:
|
||||
escalade: "npm:^3.2.0"
|
||||
picocolors: "npm:^1.1.1"
|
||||
|
|
@ -16661,7 +16611,7 @@ __metadata:
|
|||
browserslist: ">= 4.21.0"
|
||||
bin:
|
||||
update-browserslist-db: cli.js
|
||||
checksum: 10c1/29136a665ed0a3e35a909ff4d86503e532d3e54de6b58a3dde0184ecdeb396dacc441f37b89c6b847566c04f089e80d818685848a8542821410ac46c10fdbb13
|
||||
checksum: 10c1/55a3bf6271f24602da837f41ef99b5f78f979c2435024783473e0879e08fd4131a93e3a8f1d2d91bae8d3f9d3c9a50274676cfd349d72b5212d8529bfd2af0c2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -17094,16 +17044,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webrtc-adapter@npm:^7.7.1":
|
||||
version: 7.7.1
|
||||
resolution: "webrtc-adapter@npm:7.7.1"
|
||||
dependencies:
|
||||
rtcpeerconnection-shim: "npm:^1.2.15"
|
||||
sdp: "npm:^2.12.0"
|
||||
checksum: 10c1/24330cf7be5f46599b333fbfdd54ac16c76c24c4c7744bcda9beed8aa77135924aaaf9f39e7844066b8c218a81dd13a6e0d4e221294d826572ec631b5750ea9a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4":
|
||||
version: 0.7.4
|
||||
resolution: "websocket-driver@npm:0.7.4"
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
apiVersion: v2
|
||||
name: peers
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful peers or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those peers and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
AppVersion: "v1.22.0"
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "peers.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "peers.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "peers.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "peers.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "peers.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "peers.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "peers.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "peers.labels" -}}
|
||||
helm.sh/chart: {{ include "peers.chart" . }}
|
||||
{{ include "peers.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- if .Values.global.appLabels }}
|
||||
{{- .Values.global.appLabels | toYaml | nindent 0}}
|
||||
{{- end}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "peers.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "peers.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "peers.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "peers.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "peers.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "peers.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "peers.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "peers.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
shareProcessNamespace: true
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
{{- if .Values.global.enterpriseEditionLicense }}
|
||||
image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}-ee"
|
||||
{{- else }}
|
||||
image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if .Values.healthCheck}}
|
||||
{{- .Values.healthCheck | toYaml | nindent 10}}
|
||||
{{- end}}
|
||||
env:
|
||||
- name: ASSIST_KEY
|
||||
value: {{ .Values.global.assistKey }}
|
||||
- name: S3_KEY
|
||||
{{- if .Values.global.s3.existingSecret }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.global.s3.existingSecret }}
|
||||
key: access-key
|
||||
{{- else }}
|
||||
value: {{ .Values.global.s3.accessKey }}
|
||||
{{- end }}
|
||||
{{- range $key, $val := .Values.global.env }}
|
||||
- name: {{ $key }}
|
||||
value: '{{ $val }}'
|
||||
{{- end }}
|
||||
{{- range $key, $val := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: '{{ $val }}'
|
||||
{{- end}}
|
||||
ports:
|
||||
{{- range $key, $val := .Values.service.ports }}
|
||||
- name: {{ $key }}
|
||||
containerPort: {{ $val }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- with .Values.persistence.mounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.persistence.volumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "peers.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "peers.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
{{- if .Values.ingress.enabled }}
|
||||
{{- $fullName := include "peers.fullname" . -}}
|
||||
{{- $peerjsSvcPort := .Values.service.ports.peerjs -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$1
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: "{{ tpl .Values.ingress.className . }}"
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.global.domainName }}
|
||||
{{- if .Values.ingress.tls.secretName}}
|
||||
secretName: {{ .Values.ingress.tls.secretName }}
|
||||
{{- end}}
|
||||
rules:
|
||||
- host: {{ .Values.global.domainName }}
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $peerjsSvcPort }}
|
||||
path: /assist/(.*)
|
||||
{{- end }}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "peers.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
{{- range $key, $val := .Values.service.ports }}
|
||||
- port: {{ $val }}
|
||||
targetPort: {{ $key }}
|
||||
protocol: TCP
|
||||
name: {{ $key }}
|
||||
{{- end}}
|
||||
selector:
|
||||
{{- include "peers.selectorLabels" . | nindent 4 }}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "peers.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
{{- if .Values.serviceMonitor.additionalLabels }}
|
||||
{{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
endpoints:
|
||||
{{- .Values.serviceMonitor.scrapeConfigs | toYaml | nindent 4 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "peers.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "peers.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "peers.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "peers.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "peers.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
# Default values for openreplay.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: "{{ .Values.global.openReplayContainerRegistry }}/peers"
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: "peers"
|
||||
fullnameOverride: "peers-openreplay"
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
podSecurityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
fsGroup: 1001
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
# podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
# securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
#service:
|
||||
# type: ClusterIP
|
||||
# port: 9000
|
||||
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
additionalLabels:
|
||||
release: observability
|
||||
scrapeConfigs:
|
||||
- port: metrics
|
||||
honorLabels: true
|
||||
interval: 15s
|
||||
path: /metrics
|
||||
scheme: http
|
||||
scrapeTimeout: 10s
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
peerjs: 9000
|
||||
metrics: 8888
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "{{ .Values.global.ingress.controller.ingressClassResource.name }}"
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
tls:
|
||||
secretName: openreplay-ssl
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
env:
|
||||
debug: 0
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
|
||||
persistence: {}
|
||||
# # Spec of spec.template.spec.containers[*].volumeMounts
|
||||
# mounts:
|
||||
# - name: kafka-ssl
|
||||
# mountPath: /opt/kafka/ssl
|
||||
# # Spec of spec.template.spec.volumes
|
||||
# volumes:
|
||||
# - name: kafka-ssl
|
||||
# secret:
|
||||
# secretName: kafka-ssl
|
||||
Binary file not shown.
|
|
@ -1,490 +1,545 @@
|
|||
<!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>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.connecting-message {
|
||||
/* margin-top: 50%; */
|
||||
font-size: 20px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
display: none;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenReplay | Assist</title>
|
||||
|
||||
.status-connecting .connecting-message {
|
||||
/* display: block; */
|
||||
}
|
||||
.status-connecting .card {
|
||||
/* display: none; */
|
||||
}
|
||||
<!--CSS -->
|
||||
<!-- <link href="css/styles.css" rel="stylesheet"> -->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card{
|
||||
font: 14px 'Roboto', sans-serif;
|
||||
/* min-width: 324px; */
|
||||
width: 300px;
|
||||
/* max-width: 800px; */
|
||||
/* border: solid thin #ccc; */
|
||||
/* box-shadow: 0 0 10px #aaa; */
|
||||
border: solid 4px rgba(0, 0, 0, 0.2);
|
||||
border-radius: .5rem;
|
||||
}
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.card-footers {
|
||||
display: flex;
|
||||
border-bottom: solid thin #CCC;
|
||||
padding: 5px 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.connecting-message {
|
||||
/* margin-top: 50%; */
|
||||
font-size: 20px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
display: none;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.card-footers .assist-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.status-connecting .connecting-message {
|
||||
/* display: block; */
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #CC0000 !important;
|
||||
color: white;
|
||||
}
|
||||
.status-connecting .card {
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #FF0000 !important;
|
||||
color: white;
|
||||
}
|
||||
.card {
|
||||
font: 14px 'Roboto', sans-serif;
|
||||
/* min-width: 324px; */
|
||||
width: 300px;
|
||||
/* max-width: 800px; */
|
||||
/* border: solid thin #ccc; */
|
||||
/* box-shadow: 0 0 10px #aaa; */
|
||||
border: solid 4px rgba(0, 0, 0, 0.2);
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 5px 8px;
|
||||
font-size: 14px;
|
||||
border-radius: .5rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.card-footers {
|
||||
display: flex;
|
||||
border-bottom: solid thin #CCC;
|
||||
padding: 5px 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.card-footers .assist-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
.btn-danger {
|
||||
background-color: #CC0000 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card .card-header{
|
||||
cursor: move;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: solid thin #ccc;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: #FF0000 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#agent-name, #duration{
|
||||
cursor:default;
|
||||
}
|
||||
.btn {
|
||||
padding: 5px 8px;
|
||||
font-size: 14px;
|
||||
border-radius: .5rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#video-container {
|
||||
background-color: rgb(90, 90, 90);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* width: 300px; */
|
||||
}
|
||||
.btn span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#video-container video {
|
||||
width: 100% !important;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
#local-stream, #remote-stream {
|
||||
/* display:none; */ /* TODO uncomment this line */
|
||||
}
|
||||
#video-container.remote #remote-stream {
|
||||
display: block;
|
||||
}
|
||||
#video-container.local {
|
||||
min-height: 100px;
|
||||
}
|
||||
#video-container.local #local-stream {
|
||||
display: block;
|
||||
}
|
||||
.btn:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
#local-stream{
|
||||
width: 35%;
|
||||
/* top: 50%; */
|
||||
/* left: 70%; */
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
border: thin solid rgba(255,255,255, .3);
|
||||
overflow: hidden;
|
||||
}
|
||||
.card .card-header {
|
||||
cursor: move;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: solid thin #ccc;
|
||||
}
|
||||
|
||||
#audio-btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#audio-btn .bi-mic {
|
||||
fill: #CC0000;
|
||||
}
|
||||
#audio-btn .bi-mic-mute {
|
||||
display:none;
|
||||
}
|
||||
#audio-btn:after {
|
||||
/* text-transform: capitalize; */
|
||||
color: #CC0000;
|
||||
content: 'Mute';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#audio-btn.muted .bi-mic-mute {
|
||||
display: inline-block;
|
||||
}
|
||||
#audio-btn.muted .bi-mic {
|
||||
display:none;
|
||||
}
|
||||
#audio-btn.muted:after {
|
||||
content: 'Unmute';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#agent-name,
|
||||
#duration {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#video-container {
|
||||
background-color: rgb(90, 90, 90);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* width: 300px; */
|
||||
}
|
||||
|
||||
#video-container video {
|
||||
width: 100% !important;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-stream,
|
||||
#remote-stream {
|
||||
/* display:none; */
|
||||
/* TODO uncomment this line */
|
||||
}
|
||||
|
||||
#video-container.remote #remote-stream {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#video-container.local {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
#video-container.local #local-stream {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#local-stream {
|
||||
width: 35%;
|
||||
/* top: 50%; */
|
||||
/* left: 70%; */
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
border: thin solid rgba(255, 255, 255, .3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#audio-btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#audio-btn .bi-mic {
|
||||
fill: #CC0000;
|
||||
}
|
||||
|
||||
#audio-btn .bi-mic-mute {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#audio-btn:after {
|
||||
/* text-transform: capitalize; */
|
||||
color: #CC0000;
|
||||
content: 'Mute';
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#audio-btn.muted .bi-mic-mute {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#audio-btn.muted .bi-mic {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#audio-btn.muted:after {
|
||||
content: 'Unmute';
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
#video-btn .bi-camera-video {
|
||||
fill: #CC0000;
|
||||
}
|
||||
#video-btn .bi-camera-video-off {
|
||||
display:none;
|
||||
}
|
||||
#video-btn:after {
|
||||
/* text-transform: capitalize; */
|
||||
color: #CC0000;
|
||||
content: 'Stop Video';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#video-btn.off:after {
|
||||
content: 'Start Video';
|
||||
padding-left: 5px;
|
||||
}
|
||||
#video-btn.off .bi-camera-video-off {
|
||||
display: inline-block;
|
||||
}
|
||||
#video-btn.off .bi-camera-video {
|
||||
display:none;
|
||||
}
|
||||
#video-btn .bi-camera-video {
|
||||
fill: #CC0000;
|
||||
}
|
||||
|
||||
/* CHART */
|
||||
#chat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
#video-btn .bi-camera-video-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages { display: none; }
|
||||
#chat-card .chat-input { display: none; }
|
||||
#chat-card .chat-header .arrow-state { transform: rotate(180deg); }
|
||||
#chat-card.active .chat-messages { display: flex; }
|
||||
#chat-card.active .chat-input { display: flex; }
|
||||
#chat-card.active .chat-header .arrow-state { transform: rotate(0deg); }
|
||||
#video-btn:after {
|
||||
/* text-transform: capitalize; */
|
||||
color: #CC0000;
|
||||
content: 'Stop Video';
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#chat-card .chat-header {
|
||||
border-bottom: solid thin #ccc;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
#video-btn.off:after {
|
||||
content: 'Start Video';
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#chat-card .chat-header .chat-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#video-btn.off .bi-camera-video-off {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#chat-card .chat-header .chat-title span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
#video-btn.off .bi-camera-video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages {
|
||||
padding: 8px 16px;
|
||||
overflow-y: auto;
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
/* CHART */
|
||||
#chat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#chat-card .message-text {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
color: #666666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
#chat-card .chat-messages {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chat-card .message .message-text {
|
||||
/* max-width: 70%; */
|
||||
width: fit-content;
|
||||
}
|
||||
#chat-card .message {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
#chat-card .chat-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.left .message-text {
|
||||
text-align: left;
|
||||
background: #D7E2E2;
|
||||
border-radius: 0px 30px 30px 30px;
|
||||
}
|
||||
#chat-card .chat-header .arrow-state {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
#chat-card .message .message-user {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999999;
|
||||
}
|
||||
#chat-card .message .message-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
#chat-card.active .chat-messages {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.right {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
#chat-card.active .chat-input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.right .message-text {
|
||||
background: #E4E4E4;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 30px 30px 0px 30px;
|
||||
}
|
||||
#chat-card.active .chat-header .arrow-state {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
#chat-card .chat-input {
|
||||
margin: 10px;
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
background-color: #DDDDDD;
|
||||
position: relative;
|
||||
}
|
||||
#chat-card .chat-header {
|
||||
border-bottom: solid thin #ccc;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chat-card .chat-input .input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
}
|
||||
#chat-card .chat-header .chat-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background-color: #AAA;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
.send-btn:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
.send-btn svg {
|
||||
fill: #DDDDDD;
|
||||
}
|
||||
#chat-card .chat-header .chat-title span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.confirm-window .title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.confirm-window {
|
||||
font: 14px 'Roboto', sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #F3F3F3;
|
||||
border-radius: .5rem;
|
||||
/* position: absolute; */
|
||||
width: fit-content;
|
||||
color: #666666;
|
||||
display: none;
|
||||
}
|
||||
.confirm-window .actions {
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
box-shadow: 0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
#chat-card .chat-messages {
|
||||
padding: 8px 16px;
|
||||
overflow-y: auto;
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
#chat-card .message-text {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
color: #666666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: rgba(0, 167, 47, 1);
|
||||
color: white;
|
||||
}
|
||||
#chat-card .message .message-text {
|
||||
/* max-width: 70%; */
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* .btn-error:hover,
|
||||
#chat-card .message {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.left .message-text {
|
||||
text-align: left;
|
||||
background: #D7E2E2;
|
||||
border-radius: 0px 30px 30px 30px;
|
||||
}
|
||||
|
||||
#chat-card .message .message-user {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
#chat-card .message .message-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.right {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#chat-card .chat-messages .message.right .message-text {
|
||||
background: #E4E4E4;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 30px 30px 0px 30px;
|
||||
}
|
||||
|
||||
#chat-card .chat-input {
|
||||
margin: 10px;
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
background-color: #DDDDDD;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#chat-card .chat-input .input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background-color: #AAA;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.send-btn svg {
|
||||
fill: #DDDDDD;
|
||||
}
|
||||
|
||||
.confirm-window .title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.confirm-window {
|
||||
font: 14px 'Roboto', sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #F3F3F3;
|
||||
border-radius: .5rem;
|
||||
/* position: absolute; */
|
||||
width: fit-content;
|
||||
color: #666666;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.confirm-window .actions {
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
box-shadow: 0px 0px 3.99778px 1.99889px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: rgba(0, 167, 47, 1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* .btn-error:hover,
|
||||
.btn-success:hover {
|
||||
filter: brightness(0.9);
|
||||
} */
|
||||
|
||||
.btn-error {
|
||||
background: #FFE9E9;
|
||||
/* border-color: #d43f3a; */
|
||||
color: #CC0000;
|
||||
}
|
||||
</style>
|
||||
.btn-error {
|
||||
background: #FFE9E9;
|
||||
/* border-color: #d43f3a; */
|
||||
color: #CC0000;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
</head>
|
||||
|
||||
|
||||
<body data-openreplay-hidden>
|
||||
<div id="remote-control-confirm" class="confirm-window">
|
||||
<div class="title">The agent is requesting remote control</div>
|
||||
<div class="actions">
|
||||
<button class="text-uppercase btn btn-lg btn-success" style="margin-right: 10px">Grant remote access</button>
|
||||
<button class="text-uppercase btn btn-lg btn-error">Reject</button>
|
||||
</div>
|
||||
<body data-openreplay-hidden>
|
||||
<div id="remote-control-confirm" class="confirm-window">
|
||||
<div class="title">The agent is requesting remote control</div>
|
||||
<div class="actions">
|
||||
<button class="text-uppercase btn btn-lg btn-success" style="margin-right: 10px">Grant remote access</button>
|
||||
<button class="text-uppercase btn btn-lg btn-error">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="call-confirm" class="confirm-window">
|
||||
<div class="title">Answer the call so the agent can assist.</div>
|
||||
<div class="actions">
|
||||
<button class="text-uppercase btn btn-lg btn-success" style="margin-right: 10px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-telephone" viewBox="0 0 16 16">
|
||||
<path d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.568 17.568 0 0 0 4.168 6.608 17.569 17.569 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.678.678 0 0 0-.58-.122l-2.19.547a1.745 1.745 0 0 1-1.657-.459L5.482 8.062a1.745 1.745 0 0 1-.46-1.657l.548-2.19a.678.678 0 0 0-.122-.58L3.654 1.328zM1.884.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"/>
|
||||
</svg>
|
||||
<span>Answer</span>
|
||||
</button>
|
||||
<button class="text-uppercase btn btn-lg btn-error">Reject</button>
|
||||
</div>
|
||||
<div id="call-confirm" class="confirm-window">
|
||||
<div class="title">Answer the call so the agent can assist.</div>
|
||||
<div class="actions">
|
||||
<button class="text-uppercase btn btn-lg btn-success" style="margin-right: 10px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-telephone"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.568 17.568 0 0 0 4.168 6.608 17.569 17.569 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.678.678 0 0 0-.58-.122l-2.19.547a1.745 1.745 0 0 1-1.657-.459L5.482 8.062a1.745 1.745 0 0 1-.46-1.657l.548-2.19a.678.678 0 0 0-.122-.58L3.654 1.328zM1.884.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z" />
|
||||
</svg>
|
||||
<span>Answer</span>
|
||||
</button>
|
||||
<button class="text-uppercase btn btn-lg btn-error">Reject</button>
|
||||
</div>
|
||||
<section id="or-assist" class="status-connecting">
|
||||
<div class="connecting-message"> Connecting... </div>
|
||||
<div class="card shadow">
|
||||
<div class="drag-area card-header d-flex justify-content-between">
|
||||
<div class="user-info">
|
||||
<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">
|
||||
<!-- <p class="text-white m-auto text-center">Starting video...</p> -->
|
||||
<video id="video-local" autoplay muted></video>
|
||||
</div>
|
||||
|
||||
<div id="remote-stream" class="ratio ratio-4x3 m-0 p-0">
|
||||
<!-- <p id="remote-stream-placeholder" class="text-white m-auto text-center">Starting video...</p> -->
|
||||
<video id="video-remote" autoplay></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footers">
|
||||
<div class="assist-controls">
|
||||
<!-- Add class .muted to #audio-btn when user mutes audio -->
|
||||
<button
|
||||
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" 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" 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>
|
||||
</button>
|
||||
|
||||
<!--Add class .off to #video-btn when user stops video -->
|
||||
<button
|
||||
href="#"
|
||||
id="video-btn"
|
||||
class="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>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button id="end-call-btn" href="#" class="btn btn-danger btn-sm text-uppercase" style="margin-right: 8px;">End</button>
|
||||
</div>
|
||||
|
||||
<!-- CHAT - add .active class to show the messages and input -->
|
||||
<div id="chat-card" class="active">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16">
|
||||
<path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/>
|
||||
</svg>
|
||||
<span>Chat</span>
|
||||
</div>
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" class="bi bi-chevron-up arrow-state" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages">
|
||||
<div class="message left">
|
||||
<div class="message-text"> Hey, did you get the key? </div>
|
||||
<div>
|
||||
<span class="message-user">Username</span>
|
||||
<span class="message-time"> 00:00 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message right">
|
||||
<div class="message-text">
|
||||
Oui, merci!
|
||||
</div>
|
||||
<div>
|
||||
<span class="message-user">Username</span>
|
||||
<span class="message-time">00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<input type="text" class="input" placeholder="Type a message...">
|
||||
<div class="send-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section id="or-assist" class="status-connecting">
|
||||
<div class="connecting-message"> Connecting... </div>
|
||||
<div class="card shadow">
|
||||
<div class="drag-area card-header d-flex justify-content-between">
|
||||
<div class="user-info">
|
||||
<span>Call with</span>
|
||||
<!-- User Name -->
|
||||
<span id="agent-name" class="person-name fw-light">Support Agent</span>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
<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]">
|
||||
<!-- <p class="text-white m-auto text-center">Starting video...</p> -->
|
||||
<video id="video-local" autoplay muted class="scale-x-[-1]"></video>
|
||||
</div>
|
||||
|
||||
<div id="remote-stream" class="ratio ratio-4x3 m-0 p-0">
|
||||
<!-- <p id="remote-stream-placeholder" class="text-white m-auto text-center">Starting video...</p> -->
|
||||
<video id="video-remote" autoplay></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footers">
|
||||
<div class="assist-controls">
|
||||
<!-- Add class .muted to #audio-btn when user mutes audio -->
|
||||
<button 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" 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" 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>
|
||||
</button>
|
||||
|
||||
<!--Add class .off to #video-btn when user stops video -->
|
||||
<button href="#" id="video-btn" class="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>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button id="end-call-btn" href="#" class="btn btn-danger btn-sm text-uppercase"
|
||||
style="margin-right: 8px;">End</button>
|
||||
</div>
|
||||
|
||||
<!-- CHAT - add .active class to show the messages and input -->
|
||||
<div id="chat-card" class="active">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-chat"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z" />
|
||||
</svg>
|
||||
<span>Chat</span>
|
||||
</div>
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" class="bi bi-chevron-up arrow-state"
|
||||
viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages">
|
||||
<div class="message left">
|
||||
<div class="message-text"> Hey, did you get the key? </div>
|
||||
<div>
|
||||
<span class="message-user">Username</span>
|
||||
<span class="message-time"> 00:00 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message right">
|
||||
<div class="message-text">
|
||||
Oui, merci!
|
||||
</div>
|
||||
<div>
|
||||
<span class="message-user">Username</span>
|
||||
<span class="message-time">00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<input type="text" class="input" placeholder="Type a message...">
|
||||
<div class="send-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="bi bi-arrow-right-short"
|
||||
viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -152,8 +152,9 @@
|
|||
</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">
|
||||
<video id="video-local" autoplay muted></video>
|
||||
<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 id="remote-stream" class="ratio ratio-4x3 m-0 p-0">
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@
|
|||
"dependencies": {
|
||||
"csstype": "^3.0.10",
|
||||
"fflate": "^0.8.2",
|
||||
"peerjs": "1.5.4",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import type { Socket, } from 'socket.io-client'
|
||||
import { connect, } from 'socket.io-client'
|
||||
import Peer, { MediaConnection, } from 'peerjs'
|
||||
import type { Properties, } from 'csstype'
|
||||
import { App, } from '@openreplay/tracker'
|
||||
import type { Socket } from 'socket.io-client'
|
||||
import { connect } from 'socket.io-client'
|
||||
import type { Properties } from 'csstype'
|
||||
import { App } from '@openreplay/tracker'
|
||||
|
||||
import RequestLocalStream, { LocalStream, } from './LocalStream.js'
|
||||
import {hasTag,} from './guards.js'
|
||||
import RemoteControl, { RCStatus, } from './RemoteControl.js'
|
||||
import RequestLocalStream, { LocalStream } from './LocalStream.js'
|
||||
import { hasTag } from './guards.js'
|
||||
import RemoteControl, { RCStatus } from './RemoteControl.js'
|
||||
import CallWindow from './CallWindow.js'
|
||||
import AnnotationCanvas from './AnnotationCanvas.js'
|
||||
import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js'
|
||||
import { callConfirmDefault, } from './ConfirmWindow/defaults.js'
|
||||
import type { Options as ConfirmOptions, } from './ConfirmWindow/defaults.js'
|
||||
import { callConfirmDefault } from './ConfirmWindow/defaults.js'
|
||||
import type { Options as ConfirmOptions } from './ConfirmWindow/defaults.js'
|
||||
import ScreenRecordingState from './ScreenRecordingState.js'
|
||||
import { pkgVersion, } from './version.js'
|
||||
import { pkgVersion } from './version.js'
|
||||
import Canvas from './Canvas.js'
|
||||
import { gzip, } from 'fflate'
|
||||
// TODO: fully specified strict check with no-any (everywhere)
|
||||
// @ts-ignore
|
||||
const safeCastedPeer = Peer.default || Peer
|
||||
import { gzip } from 'fflate'
|
||||
|
||||
type StartEndCallback = (agentInfo?: Record<string, any>) => ((() => any) | void)
|
||||
|
||||
|
|
@ -52,26 +48,23 @@ export interface Options {
|
|||
confirmStyle?: Properties;
|
||||
|
||||
config: RTCConfiguration;
|
||||
serverURL: string
|
||||
serverURL: string;
|
||||
callUITemplate?: string;
|
||||
compressionEnabled: boolean;
|
||||
/**
|
||||
* Minimum amount of messages in a batch to trigger compression run
|
||||
* @default 5000
|
||||
* */
|
||||
compressionMinBatchSize: number
|
||||
*/
|
||||
compressionMinBatchSize: number;
|
||||
}
|
||||
|
||||
|
||||
enum CallingState {
|
||||
Requesting,
|
||||
True,
|
||||
False,
|
||||
}
|
||||
|
||||
|
||||
// TODO typing????
|
||||
type OptionalCallback = (()=>Record<string, unknown>) | void
|
||||
type OptionalCallback = (() => Record<string, unknown>) | void
|
||||
type Agent = {
|
||||
onDisconnect?: OptionalCallback,
|
||||
onControlReleased?: OptionalCallback,
|
||||
|
|
@ -84,8 +77,8 @@ export default class Assist {
|
|||
readonly version = pkgVersion
|
||||
|
||||
private socket: Socket | null = null
|
||||
private peer: Peer | null = null
|
||||
private canvasPeers: Record<number, Peer | null> = {}
|
||||
private calls: Map<string, RTCPeerConnection> = new Map();
|
||||
private canvasPeers: { [id: number]: RTCPeerConnection | null } = {}
|
||||
private canvasNodeCheckers: Map<number, any> = new Map()
|
||||
private assistDemandedRestart = false
|
||||
private callingState: CallingState = CallingState.False
|
||||
|
|
@ -103,20 +96,20 @@ export default class Assist {
|
|||
// @ts-ignore
|
||||
window.__OR_ASSIST_VERSION = this.version
|
||||
this.options = Object.assign({
|
||||
session_calling_peer_key: '__openreplay_calling_peer',
|
||||
session_control_peer_key: '__openreplay_control_peer',
|
||||
config: null,
|
||||
serverURL: null,
|
||||
onCallStart: ()=>{},
|
||||
onAgentConnect: ()=>{},
|
||||
onRemoteControlStart: ()=>{},
|
||||
callConfirm: {},
|
||||
controlConfirm: {}, // TODO: clear options passing/merging/overwriting
|
||||
recordingConfirm: {},
|
||||
socketHost: '',
|
||||
compressionEnabled: false,
|
||||
compressionMinBatchSize: 5000,
|
||||
},
|
||||
session_calling_peer_key: '__openreplay_calling_peer',
|
||||
session_control_peer_key: '__openreplay_control_peer',
|
||||
config: null,
|
||||
serverURL: null,
|
||||
onCallStart: () => { },
|
||||
onAgentConnect: () => { },
|
||||
onRemoteControlStart: () => { },
|
||||
callConfirm: {},
|
||||
controlConfirm: {}, // TODO: clear options passing/merging/overwriting
|
||||
recordingConfirm: {},
|
||||
socketHost: '',
|
||||
compressionEnabled: false,
|
||||
compressionMinBatchSize: 5000,
|
||||
},
|
||||
options,
|
||||
)
|
||||
|
||||
|
|
@ -155,7 +148,7 @@ export default class Assist {
|
|||
if (this.agentsConnected) {
|
||||
const batchSize = messages.length
|
||||
// @ts-ignore No need in statistics messages. TODO proper filter
|
||||
if (batchSize === 2 && messages[0]._id === 0 && messages[1]._id === 49) { return }
|
||||
if (batchSize === 2 && messages[0]._id === 0 && messages[1]._id === 49) { return }
|
||||
if (batchSize > this.options.compressionMinBatchSize && this.options.compressionEnabled) {
|
||||
const toSend: any[] = []
|
||||
if (batchSize > 10000) {
|
||||
|
|
@ -198,17 +191,17 @@ export default class Assist {
|
|||
private readonly setCallingState = (newState: CallingState): void => {
|
||||
this.callingState = newState
|
||||
}
|
||||
private getHost():string{
|
||||
private getHost(): string {
|
||||
if (this.options.socketHost) {
|
||||
return this.options.socketHost
|
||||
}
|
||||
if (this.options.serverURL){
|
||||
if (this.options.serverURL) {
|
||||
return new URL(this.options.serverURL).host
|
||||
}
|
||||
return this.app.getHost()
|
||||
}
|
||||
private getBasePrefixUrl(): string{
|
||||
if (this.options.serverURL){
|
||||
private getBasePrefixUrl(): string {
|
||||
if (this.options.serverURL) {
|
||||
return new URL(this.options.serverURL).pathname
|
||||
}
|
||||
return ''
|
||||
|
|
@ -232,7 +225,7 @@ export default class Assist {
|
|||
|
||||
// SocketIO
|
||||
const socket = this.socket = connect(this.getHost(), {
|
||||
path: this.getBasePrefixUrl()+'/ws-assist/socket',
|
||||
path: this.getBasePrefixUrl() + '/ws-assist/socket',
|
||||
query: {
|
||||
'peerId': peerID,
|
||||
'identity': 'session',
|
||||
|
|
@ -257,14 +250,19 @@ export default class Assist {
|
|||
if (args[0] === 'messages' || args[0] === 'UPDATE_SESSION') {
|
||||
return
|
||||
}
|
||||
app.debug.log('Socket:', ...args)
|
||||
if (args[0] !== 'webrtc_call_ice_candidate') {
|
||||
app.debug.log('Socket:', ...args)
|
||||
};
|
||||
socket.on('close', (e) => {
|
||||
app.debug.warn('Socket closed:', e);
|
||||
})
|
||||
})
|
||||
|
||||
const onGrand = (id: string) => {
|
||||
if (!callUI) {
|
||||
callUI = new CallWindow(app.debug.error, this.options.callUITemplate)
|
||||
}
|
||||
if (this.remoteControl){
|
||||
if (this.remoteControl) {
|
||||
callUI?.showRemoteControl(this.remoteControl.releaseControl)
|
||||
}
|
||||
this.agents[id] = { ...this.agents[id], onControlReleased: this.options.onRemoteControlStart(this.agents[id]?.agentInfo), }
|
||||
|
|
@ -274,26 +272,24 @@ export default class Assist {
|
|||
return callingAgents.get(id)
|
||||
}
|
||||
const onRelease = (id?: string | null, isDenied?: boolean) => {
|
||||
{
|
||||
if (id) {
|
||||
const cb = this.agents[id].onControlReleased
|
||||
delete this.agents[id].onControlReleased
|
||||
typeof cb === 'function' && cb()
|
||||
this.emit('control_rejected', id)
|
||||
}
|
||||
if (annot != null) {
|
||||
annot.remove()
|
||||
annot = null
|
||||
}
|
||||
callUI?.hideRemoteControl()
|
||||
if (this.callingState !== CallingState.True) {
|
||||
callUI?.remove()
|
||||
callUI = null
|
||||
}
|
||||
if (isDenied) {
|
||||
const info = id ? this.agents[id]?.agentInfo : {}
|
||||
this.options.onRemoteControlDeny?.(info || {})
|
||||
}
|
||||
if (id) {
|
||||
const cb = this.agents[id].onControlReleased
|
||||
delete this.agents[id].onControlReleased
|
||||
typeof cb === 'function' && cb()
|
||||
this.emit('control_rejected', id)
|
||||
}
|
||||
if (annot != null) {
|
||||
annot.remove()
|
||||
annot = null
|
||||
}
|
||||
callUI?.hideRemoteControl()
|
||||
if (this.callingState !== CallingState.True) {
|
||||
callUI?.remove()
|
||||
callUI = null
|
||||
}
|
||||
if (isDenied) {
|
||||
const info = id ? this.agents[id]?.agentInfo : {}
|
||||
this.options.onRemoteControlDeny?.(info || {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,11 +338,12 @@ export default class Assist {
|
|||
|
||||
|
||||
// TODO: restrict by id
|
||||
socket.on('moveAnnotation', (id, event) => processEvent(id, event, (_, d) => annot && annot.move(d)))
|
||||
socket.on('startAnnotation', (id, event) => processEvent(id, event, (_, d) => annot?.start(d)))
|
||||
socket.on('moveAnnotation', (id, event) => processEvent(id, event, (_, d) => annot && annot.move(d)))
|
||||
socket.on('startAnnotation', (id, event) => processEvent(id, event, (_, d) => annot?.start(d)))
|
||||
socket.on('stopAnnotation', (id, event) => processEvent(id, event, annot?.stop))
|
||||
|
||||
socket.on('NEW_AGENT', (id: string, info: AgentInfo) => {
|
||||
this.cleanCanvasConnections();
|
||||
this.agents[id] = {
|
||||
onDisconnect: this.options.onAgentConnect?.(info),
|
||||
agentInfo: info, // TODO ?
|
||||
|
|
@ -369,8 +366,10 @@ export default class Assist {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('AGENTS_CONNECTED', (ids: string[]) => {
|
||||
ids.forEach(id =>{
|
||||
this.cleanCanvasConnections();
|
||||
ids.forEach(id => {
|
||||
const agentInfo = this.agents[id]?.agentInfo
|
||||
this.agents[id] = {
|
||||
agentInfo,
|
||||
|
|
@ -385,7 +384,7 @@ export default class Assist {
|
|||
this.app.allowAppStart()
|
||||
setTimeout(() => {
|
||||
this.app.start().then(() => { this.assistDemandedRestart = false })
|
||||
.then(() => {
|
||||
.then(() => {
|
||||
this.remoteControl?.reconnect(ids)
|
||||
})
|
||||
.catch(e => app.debug.error(e))
|
||||
|
|
@ -400,37 +399,65 @@ export default class Assist {
|
|||
this.agents[id]?.onDisconnect?.()
|
||||
delete this.agents[id]
|
||||
|
||||
Object.values(this.calls).forEach(pc => pc.close())
|
||||
this.calls.clear();
|
||||
|
||||
recordingState.stopAgentRecording(id)
|
||||
endAgentCall(id)
|
||||
endAgentCall({ socketId: id })
|
||||
})
|
||||
|
||||
socket.on('NO_AGENT', () => {
|
||||
Object.values(this.agents).forEach(a => a.onDisconnect?.())
|
||||
this.cleanCanvasConnections();
|
||||
this.agents = {}
|
||||
if (recordingState.isActive) recordingState.stopRecording()
|
||||
})
|
||||
socket.on('call_end', (id) => {
|
||||
if (!callingAgents.has(id)) {
|
||||
app.debug.warn('Received call_end from unknown agent', id)
|
||||
|
||||
socket.on('call_end', (socketId, { data: callId }) => {
|
||||
if (!callingAgents.has(socketId)) {
|
||||
app.debug.warn('Received call_end from unknown agent', socketId)
|
||||
return
|
||||
}
|
||||
endAgentCall(id)
|
||||
|
||||
endAgentCall({ socketId, callId })
|
||||
})
|
||||
|
||||
socket.on('_agent_name', (id, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const name = info.data
|
||||
callingAgents.set(id, name)
|
||||
|
||||
if (!this.peer) {
|
||||
setupPeer()
|
||||
}
|
||||
updateCallerNames()
|
||||
})
|
||||
|
||||
socket.on('webrtc_canvas_answer', async (_, data: { answer, id }) => {
|
||||
const pc = this.canvasPeers[data.id];
|
||||
if (pc) {
|
||||
try {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
|
||||
} catch (e) {
|
||||
app.debug.error('Error adding ICE candidate', e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('webrtc_canvas_ice_candidate', async (_, data: { candidate, id }) => {
|
||||
const pc = this.canvasPeers[data.id];
|
||||
if (pc) {
|
||||
try {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||
} catch (e) {
|
||||
app.debug.error('Error adding ICE candidate', e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If a videofeed arrives, then we show the video in the ui
|
||||
socket.on('videofeed', (_, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const feedState = info.data
|
||||
callUI?.toggleVideoStream(feedState)
|
||||
})
|
||||
|
||||
socket.on('request_recording', (id, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const agentData = info.data
|
||||
|
|
@ -448,29 +475,52 @@ export default class Assist {
|
|||
}
|
||||
})
|
||||
|
||||
socket.on('webrtc_call_offer', async (_, data: { from: string, offer: RTCSessionDescriptionInit }) => {
|
||||
if (!this.calls.has(data.from)) {
|
||||
await handleIncomingCallOffer(data.from, data.offer);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('webrtc_call_ice_candidate', async (data: { from: string, candidate: RTCIceCandidateInit }) => {
|
||||
const pc = this.calls[data.from];
|
||||
if (pc) {
|
||||
try {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||
} catch (e) {
|
||||
app.debug.error('Error adding ICE candidate', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const callingAgents: Map<string, string> = new Map() // !! uses socket.io ID
|
||||
// TODO: merge peerId & socket.io id (simplest way - send peerId with the name)
|
||||
const calls: Record<string, MediaConnection> = {} // !! uses peerJS ID
|
||||
const lStreams: Record<string, LocalStream> = {}
|
||||
|
||||
function updateCallerNames() {
|
||||
callUI?.setAssistentName(callingAgents)
|
||||
}
|
||||
function endAgentCall(id: string) {
|
||||
callingAgents.delete(id)
|
||||
function endAgentCall({ socketId, callId }: { socketId: string, callId?: string }) {
|
||||
callingAgents.delete(socketId)
|
||||
|
||||
if (callingAgents.size === 0) {
|
||||
handleCallEnd()
|
||||
} else {
|
||||
updateCallerNames()
|
||||
//TODO: close() specific call and corresponding lStreams (after connecting peerId & socket.io id)
|
||||
if (callId) {
|
||||
handleCallEndWithAgent(callId)
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleCallEnd = () => { // Complete stop and clear all calls
|
||||
// Streams
|
||||
Object.values(calls).forEach(call => call.close())
|
||||
Object.keys(calls).forEach(peerId => {
|
||||
delete calls[peerId]
|
||||
})
|
||||
|
||||
const handleCallEndWithAgent = (id: string) => {
|
||||
this.calls.get(id)?.close()
|
||||
this.calls.delete(id)
|
||||
}
|
||||
|
||||
// call end handling
|
||||
const handleCallEnd = () => {
|
||||
Object.values(this.calls).forEach(pc => pc.close())
|
||||
this.calls.clear();
|
||||
Object.values(lStreams).forEach((stream) => { stream.stop() })
|
||||
Object.keys(lStreams).forEach((peerId: string) => { delete lStreams[peerId] })
|
||||
// UI
|
||||
|
|
@ -484,7 +534,7 @@ export default class Assist {
|
|||
callUI?.hideControls()
|
||||
}
|
||||
|
||||
this.emit('UPDATE_SESSION', { agentIds: [], isCallActive: false, })
|
||||
this.emit('UPDATE_SESSION', { agentIds: [], isCallActive: false })
|
||||
this.setCallingState(CallingState.False)
|
||||
sessionStorage.removeItem(this.options.session_calling_peer_key)
|
||||
|
||||
|
|
@ -498,175 +548,192 @@ export default class Assist {
|
|||
}
|
||||
}
|
||||
|
||||
// PeerJS call (todo: use native WebRTC)
|
||||
const peerOptions = {
|
||||
host: this.getHost(),
|
||||
path: this.getBasePrefixUrl()+'/assist',
|
||||
port: location.protocol === 'http:' && this.noSecureMode ? 80 : 443,
|
||||
debug: 2, //appOptions.__debug_log ? 2 : 0, // 0 Print nothing //1 Prints only errors. / 2 Prints errors and warnings. / 3 Prints all logs.
|
||||
}
|
||||
const setupPeer = () => {
|
||||
if (this.options.config) {
|
||||
peerOptions['config'] = this.options.config
|
||||
const handleIncomingCallOffer = async (from: string, offer: RTCSessionDescriptionInit) => {
|
||||
app.debug.log('handleIncomingCallOffer', from)
|
||||
let confirmAnswer: Promise<boolean>
|
||||
const callingPeerIds = JSON.parse(sessionStorage.getItem(this.options.session_calling_peer_key) || '[]')
|
||||
// if the caller is already in the list, then we immediately accept the call without ui
|
||||
if (callingPeerIds.includes(from) || this.callingState === CallingState.True) {
|
||||
confirmAnswer = Promise.resolve(true)
|
||||
} else {
|
||||
// set the state to wait for confirmation
|
||||
this.setCallingState(CallingState.Requesting)
|
||||
// call the call confirmation window
|
||||
confirmAnswer = requestCallConfirm()
|
||||
// sound notification of a call
|
||||
this.playNotificationSound()
|
||||
|
||||
// after 30 seconds we drop the call
|
||||
setTimeout(() => {
|
||||
if (this.callingState !== CallingState.Requesting) { return }
|
||||
initiateCallEnd()
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
const peer = new safeCastedPeer(peerID, peerOptions) as Peer
|
||||
this.peer = peer
|
||||
let peerReconnectAttempts = 0
|
||||
// @ts-ignore (peerjs typing)
|
||||
peer.on('error', e => app.debug.warn('Peer error: ', e.type, e))
|
||||
peer.on('disconnected', () => {
|
||||
if (peerReconnectAttempts < 30) {
|
||||
this.peerReconnectTimeout = setTimeout(() => {
|
||||
if (this.app.active() && !peer.destroyed) {
|
||||
peer.reconnect()
|
||||
}
|
||||
}, Math.min(peerReconnectAttempts, 8) * 2 * 1000)
|
||||
peerReconnectAttempts += 1
|
||||
try {
|
||||
// waiting for a decision on accepting the challenge
|
||||
const agreed = await confirmAnswer
|
||||
// if rejected, then terminate the call
|
||||
if (!agreed) {
|
||||
initiateCallEnd()
|
||||
this.options.onCallDeny?.()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const requestCallConfirm = () => {
|
||||
if (callConfirmAnswer) { // Already asking
|
||||
return callConfirmAnswer
|
||||
if (!callUI) {
|
||||
callUI = new CallWindow(app.debug.error, this.options.callUITemplate)
|
||||
callUI.setVideoToggleCallback((args: { enabled: boolean }) =>
|
||||
this.emit('videofeed', { streamId: from, enabled: args.enabled })
|
||||
);
|
||||
}
|
||||
callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || {
|
||||
text: this.options.confirmText,
|
||||
style: this.options.confirmStyle,
|
||||
})) // TODO: reuse ?
|
||||
return callConfirmAnswer = callConfirmWindow.mount().then(answer => {
|
||||
closeCallConfirmWindow()
|
||||
return answer
|
||||
})
|
||||
}
|
||||
|
||||
const initiateCallEnd = () => {
|
||||
this.emit('call_end')
|
||||
handleCallEnd()
|
||||
}
|
||||
const updateVideoFeed = ({ enabled, }) => this.emit('videofeed', { streamId: this.peer?.id, enabled, })
|
||||
|
||||
peer.on('call', (call) => {
|
||||
app.debug.log('Incoming call from', call.peer)
|
||||
let confirmAnswer: Promise<boolean>
|
||||
const callingPeerIds = JSON.parse(sessionStorage.getItem(this.options.session_calling_peer_key) || '[]')
|
||||
if (callingPeerIds.includes(call.peer) || this.callingState === CallingState.True) {
|
||||
confirmAnswer = Promise.resolve(true)
|
||||
} else {
|
||||
this.setCallingState(CallingState.Requesting)
|
||||
confirmAnswer = requestCallConfirm()
|
||||
this.playNotificationSound() // For every new agent during confirmation here
|
||||
|
||||
// TODO: only one (latest) timeout
|
||||
setTimeout(() => {
|
||||
if (this.callingState !== CallingState.Requesting) { return }
|
||||
initiateCallEnd()
|
||||
}, 30000)
|
||||
// show buttons in the call window
|
||||
callUI.showControls(initiateCallEnd)
|
||||
if (!annot) {
|
||||
annot = new AnnotationCanvas()
|
||||
annot.mount()
|
||||
}
|
||||
|
||||
confirmAnswer.then(async agreed => {
|
||||
if (!agreed) {
|
||||
initiateCallEnd()
|
||||
this.options.onCallDeny?.()
|
||||
return
|
||||
// callUI.setLocalStreams(Object.values(lStreams))
|
||||
try {
|
||||
// if there are no local streams in lStrems then we set
|
||||
if (!lStreams[from]) {
|
||||
app.debug.log('starting new stream for', from)
|
||||
// request a local stream, and set it to lStreams
|
||||
lStreams[from] = await RequestLocalStream()
|
||||
}
|
||||
// Request local stream for the new connection
|
||||
try {
|
||||
// lStreams are reusable so fare we don't delete them in the `endAgentCall`
|
||||
if (!lStreams[call.peer]) {
|
||||
app.debug.log('starting new stream for', call.peer)
|
||||
lStreams[call.peer] = await RequestLocalStream()
|
||||
}
|
||||
calls[call.peer] = call
|
||||
} catch (e) {
|
||||
app.debug.error('Audio media device request error:', e)
|
||||
initiateCallEnd()
|
||||
return
|
||||
}
|
||||
|
||||
if (!callUI) {
|
||||
callUI = new CallWindow(app.debug.error, this.options.callUITemplate)
|
||||
callUI.setVideoToggleCallback(updateVideoFeed)
|
||||
}
|
||||
callUI.showControls(initiateCallEnd)
|
||||
|
||||
if (!annot) {
|
||||
annot = new AnnotationCanvas()
|
||||
annot.mount()
|
||||
}
|
||||
// have to be updated
|
||||
// we pass the received tracks to Call ui
|
||||
callUI.setLocalStreams(Object.values(lStreams))
|
||||
|
||||
call.on('error', e => {
|
||||
app.debug.warn('Call error:', e)
|
||||
initiateCallEnd()
|
||||
})
|
||||
call.on('stream', (rStream) => {
|
||||
callUI?.addRemoteStream(rStream, call.peer)
|
||||
const onInteraction = () => { // do only if document.hidden ?
|
||||
callUI?.playRemote()
|
||||
document.removeEventListener('click', onInteraction)
|
||||
}
|
||||
document.addEventListener('click', onInteraction)
|
||||
})
|
||||
|
||||
// remote video on/off/camera change
|
||||
lStreams[call.peer].onVideoTrack(vTrack => {
|
||||
const sender = call.peerConnection.getSenders().find(s => s.track?.kind === 'video')
|
||||
if (!sender) {
|
||||
app.debug.warn('No video sender found')
|
||||
return
|
||||
}
|
||||
app.debug.log('sender found:', sender)
|
||||
void sender.replaceTrack(vTrack)
|
||||
})
|
||||
|
||||
call.answer(lStreams[call.peer].stream)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
initiateCallEnd()
|
||||
})
|
||||
|
||||
this.setCallingState(CallingState.True)
|
||||
if (!callEndCallback) { callEndCallback = this.options.onCallStart?.() }
|
||||
|
||||
const callingPeerIds = Object.keys(calls)
|
||||
sessionStorage.setItem(this.options.session_calling_peer_key, JSON.stringify(callingPeerIds))
|
||||
this.emit('UPDATE_SESSION', { agentIds: callingPeerIds, isCallActive: true, })
|
||||
}).catch(reason => { // in case of Confirm.remove() without user answer (not an error)
|
||||
app.debug.log(reason)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const startCanvasStream = (stream: MediaStream, id: number) => {
|
||||
const canvasPID = `${app.getProjectKey()}-${sessionId}-${id}`
|
||||
if (!this.canvasPeers[id]) {
|
||||
this.canvasPeers[id] = new safeCastedPeer(canvasPID, peerOptions) as Peer
|
||||
}
|
||||
this.canvasPeers[id]?.on('error', (e) => app.debug.error(e))
|
||||
|
||||
Object.values(this.agents).forEach(agent => {
|
||||
if (agent.agentInfo) {
|
||||
const target = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas`
|
||||
const connection = this.canvasPeers[id]?.connect(target)
|
||||
connection?.on('open', () => {
|
||||
if (agent.agentInfo) {
|
||||
const call = this.canvasPeers[id]?.call(target, stream.clone())
|
||||
call?.on('error', app.debug.error)
|
||||
}
|
||||
})
|
||||
connection?.on('error', (e) => app.debug.error(e))
|
||||
} else {
|
||||
app.debug.error('Assist: cant establish canvas peer to agent, no agent info')
|
||||
} catch (e) {
|
||||
app.debug.error('Error requesting local stream', e);
|
||||
// if something didn't work out, we terminate the call
|
||||
initiateCallEnd();
|
||||
return;
|
||||
}
|
||||
})
|
||||
// create a new RTCPeerConnection with ice server config
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
});
|
||||
|
||||
// get all local tracks and add them to RTCPeerConnection
|
||||
lStreams[from].stream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, lStreams[from].stream);
|
||||
});
|
||||
|
||||
// When we receive local ice candidates, we emit them via socket
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
socket.emit('webrtc_call_ice_candidate', { from, candidate: event.candidate });
|
||||
}
|
||||
};
|
||||
|
||||
// when we get a remote stream, add it to call ui
|
||||
pc.ontrack = (event) => {
|
||||
const rStream = event.streams[0];
|
||||
if (rStream && callUI) {
|
||||
callUI.addRemoteStream(rStream, from);
|
||||
const onInteraction = () => {
|
||||
callUI?.playRemote();
|
||||
document.removeEventListener('click', onInteraction);
|
||||
};
|
||||
document.addEventListener('click', onInteraction);
|
||||
}
|
||||
};
|
||||
|
||||
// Keep connection with the caller
|
||||
this.calls.set(from, pc);
|
||||
|
||||
// set remote description on incoming request
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
// create a response to the incoming request
|
||||
const answer = await pc.createAnswer();
|
||||
// set answer as local description
|
||||
await pc.setLocalDescription(answer);
|
||||
// set the response as local
|
||||
socket.emit('webrtc_call_answer', { from, answer });
|
||||
|
||||
// If the state changes to an error, we terminate the call
|
||||
// pc.onconnectionstatechange = () => {
|
||||
// if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
|
||||
// initiateCallEnd();
|
||||
// }
|
||||
// };
|
||||
|
||||
// Update track when local video changes
|
||||
lStreams[from].onVideoTrack(vTrack => {
|
||||
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
|
||||
if (!sender) {
|
||||
app.debug.warn('No video sender found')
|
||||
return
|
||||
}
|
||||
sender.replaceTrack(vTrack)
|
||||
})
|
||||
|
||||
// if the user closed the tab or switched, then we end the call
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
initiateCallEnd()
|
||||
})
|
||||
|
||||
// when everything is set, we change the state to true
|
||||
this.setCallingState(CallingState.True)
|
||||
if (!callEndCallback) { callEndCallback = this.options.onCallStart?.() }
|
||||
const callingPeerIdsNow = Array.from(this.calls.keys())
|
||||
// in session storage we write down everyone with whom the call is established
|
||||
sessionStorage.setItem(this.options.session_calling_peer_key, JSON.stringify(callingPeerIdsNow))
|
||||
this.emit('UPDATE_SESSION', { agentIds: callingPeerIdsNow, isCallActive: true })
|
||||
} catch (reason) {
|
||||
app.debug.log(reason);
|
||||
}
|
||||
};
|
||||
|
||||
// Functions for requesting confirmation, ending a call, notifying, etc.
|
||||
const requestCallConfirm = () => {
|
||||
if (callConfirmAnswer) { // If confirmation has already been requested
|
||||
return callConfirmAnswer;
|
||||
}
|
||||
callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || {
|
||||
text: this.options.confirmText,
|
||||
style: this.options.confirmStyle,
|
||||
}));
|
||||
return callConfirmAnswer = callConfirmWindow.mount().then(answer => {
|
||||
closeCallConfirmWindow();
|
||||
return answer;
|
||||
});
|
||||
};
|
||||
|
||||
const initiateCallEnd = () => {
|
||||
this.emit('call_end');
|
||||
handleCallEnd();
|
||||
};
|
||||
|
||||
const startCanvasStream = async (stream: MediaStream, id: number) => {
|
||||
for (const agent of Object.values(this.agents)) {
|
||||
if (!agent.agentInfo) return;
|
||||
|
||||
const uniqueId = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas-${id}`;
|
||||
|
||||
if (!this.canvasPeers[uniqueId]) {
|
||||
this.canvasPeers[uniqueId] = new RTCPeerConnection({
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
});
|
||||
this.setupPeerListeners(uniqueId);
|
||||
|
||||
stream.getTracks().forEach((track) => {
|
||||
this.canvasPeers[uniqueId]?.addTrack(track, stream);
|
||||
});
|
||||
|
||||
// Create SDP offer
|
||||
const offer = await this.canvasPeers[uniqueId].createOffer();
|
||||
await this.canvasPeers[uniqueId].setLocalDescription(offer);
|
||||
|
||||
// Send offer via signaling server
|
||||
socket.emit('webrtc_canvas_offer', { offer, id: uniqueId });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.nodes.attachNodeCallback((node) => {
|
||||
const id = app.nodes.getID(node)
|
||||
if (id && hasTag(node, 'canvas')) {
|
||||
if (id && hasTag(node, 'canvas') && !app.sanitizer.isHidden(id)) {
|
||||
app.debug.log(`Creating stream for canvas ${id}`)
|
||||
const canvasHandler = new Canvas(
|
||||
node as unknown as HTMLCanvasElement,
|
||||
|
|
@ -686,14 +753,30 @@ export default class Assist {
|
|||
if (!isPresent) {
|
||||
canvasHandler.stop()
|
||||
this.canvasMap.delete(id)
|
||||
this.canvasPeers[id]?.destroy()
|
||||
this.canvasPeers[id] = null
|
||||
if (this.canvasPeers[id]) {
|
||||
this.canvasPeers[id]?.close()
|
||||
this.canvasPeers[id] = null
|
||||
}
|
||||
clearInterval(int)
|
||||
}
|
||||
}, 5000)
|
||||
this.canvasNodeCheckers.set(id, int)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private setupPeerListeners(id: string) {
|
||||
const peer = this.canvasPeers[id];
|
||||
if (!peer) return;
|
||||
// ICE candidates
|
||||
peer.onicecandidate = (event) => {
|
||||
if (event.candidate && this.socket) {
|
||||
this.socket.emit('webrtc_canvas_ice_candidate', {
|
||||
candidate: event.candidate,
|
||||
id,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private playNotificationSound() {
|
||||
|
|
@ -706,26 +789,32 @@ export default class Assist {
|
|||
}
|
||||
}
|
||||
|
||||
// clear all data
|
||||
private clean() {
|
||||
// sometimes means new agent connected, so we keep id for control
|
||||
this.remoteControl?.releaseControl(false, true)
|
||||
this.remoteControl?.releaseControl(false, true);
|
||||
if (this.peerReconnectTimeout) {
|
||||
clearTimeout(this.peerReconnectTimeout)
|
||||
this.peerReconnectTimeout = null
|
||||
}
|
||||
if (this.peer) {
|
||||
this.peer.destroy()
|
||||
this.app.debug.log('Peer destroyed')
|
||||
}
|
||||
this.cleanCanvasConnections();
|
||||
Object.values(this.calls).forEach(pc => pc.close())
|
||||
this.calls.clear();
|
||||
if (this.socket) {
|
||||
this.socket.disconnect()
|
||||
this.app.debug.log('Socket disconnected')
|
||||
}
|
||||
this.canvasMap.clear()
|
||||
this.canvasPeers = []
|
||||
this.canvasPeers = {}
|
||||
this.canvasNodeCheckers.forEach((int) => clearInterval(int))
|
||||
this.canvasNodeCheckers.clear()
|
||||
}
|
||||
|
||||
private cleanCanvasConnections() {
|
||||
Object.values(this.canvasPeers).forEach(pc => pc?.close())
|
||||
this.canvasPeers = {}
|
||||
this.socket?.emit('webrtc_canvas_restart')
|
||||
}
|
||||
}
|
||||
|
||||
/** simple peers impl
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default class CallWindow {
|
|||
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
|
||||
|
|
@ -62,7 +63,6 @@ export default class CallWindow {
|
|||
this.adjustIframeSize()
|
||||
iframe.onload = null
|
||||
}
|
||||
|
||||
// ?
|
||||
text = text.replace(/href="css/g, `href="${baseHref}/css`)
|
||||
doc.open()
|
||||
|
|
@ -71,8 +71,9 @@ export default class CallWindow {
|
|||
|
||||
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()
|
||||
|
|
@ -334,4 +335,4 @@ export default class CallWindow {
|
|||
this.toggleRemoteVideoUI(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,10 +63,10 @@ export default class RemoteControl {
|
|||
this.releaseControl(true)
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.confirm?.remove()
|
||||
})
|
||||
.catch(e => {
|
||||
.then(() => {
|
||||
this.confirm?.remove()
|
||||
})
|
||||
.catch(e => {
|
||||
this.confirm?.remove()
|
||||
console.error(e)
|
||||
})
|
||||
|
|
@ -113,7 +113,7 @@ export default class RemoteControl {
|
|||
|
||||
scroll = (id, d) => { id === this.agentID && this.mouse?.scroll(d) }
|
||||
move = (id, xy) => {
|
||||
return id === this.agentID && this.mouse?.move(xy)
|
||||
return id === this.agentID && this.mouse?.move(xy)
|
||||
}
|
||||
private focused: HTMLElement | null = null
|
||||
click = (id, xy) => {
|
||||
|
|
|
|||
10418
tracker/tracker-redux/.pnp.cjs
generated
Executable file
10418
tracker/tracker-redux/.pnp.cjs
generated
Executable file
File diff suppressed because one or more lines are too long
2126
tracker/tracker-redux/.pnp.loader.mjs
generated
Normal file
2126
tracker/tracker-redux/.pnp.loader.mjs
generated
Normal file
File diff suppressed because it is too large
Load diff
BIN
tracker/tracker-redux/.yarn/install-state.gz
Normal file
BIN
tracker/tracker-redux/.yarn/install-state.gz
Normal file
Binary file not shown.
|
|
@ -28,14 +28,16 @@
|
|||
"redux": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.8",
|
||||
"@openreplay/tracker": "file:../tracker",
|
||||
"prettier": "^1.18.2",
|
||||
"replace-in-files-cli": "^1.0.0",
|
||||
"typescript": "^4.6.0-dev.20211126",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"prettier": "^1.18.2",
|
||||
"replace-in-files": "^3.0.0",
|
||||
"replace-in-files-cli": "^1.0.0",
|
||||
"rollup": "^4.14.0",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
}
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.6.0-dev.20211126"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue