Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 41 additions & 34 deletions ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class UserActorSignalProcessor {
case 'local-state':
return this.reviewLocalState(signal);
case 'error':
return this.processError(signal.errorType, signal.errorCode);
return this.processError(signal);
case 'negotiation-needed':
return this.processNegotiationNeeded(signal.oldNegotiationId);
case 'transfer':
Expand Down Expand Up @@ -135,19 +135,51 @@ export class UserActorSignalProcessor {
}
}

private async processError(errorType: ClientMediaSignalError['errorType'], errorCode?: string): Promise<void> {
private async processError(signal: ClientMediaSignalError): Promise<void> {
if (!this.signed) {
return;
}

switch (errorType) {
case 'signaling':
return this.onSignalingError(errorCode);
case 'service':
return this.onServiceError(errorCode);
default:
return this.onUnexpectedError(errorCode);
const { errorType = 'other', errorCode, critical = false, negotiationId, errorDetails } = signal;
logger.error({
msg: 'Client reported an error',
errorType,
errorCode,
critical,
callId: this.callId,
role: this.role,
state: this.call.state,
});

// Store the error on this session's channel
void MediaCallChannels.addErrorById(this.channel._id, {
errorType,
ts: new Date(),
...(errorCode && { errorCode }),
...(errorDetails && { errorDetails }),
...(negotiationId && { negotiationId }),
}).catch(() => null);

let hangupReason: CallHangupReason = 'error';
if (errorType === 'service') {
hangupReason = 'service-error';

// Do not hangup on service errors after the call is already active;
// if the error happened on a renegotiation, then the service may still be able to rollback to a valid state
if (this.isPastNegotiation()) {
return;
}
}

if (!critical) {
return;
}

if (errorType === 'signaling') {
hangupReason = 'signaling-error';
}

await mediaCallDirector.hangup(this.call, this.agent, hangupReason);
}

private async processNegotiationNeeded(oldNegotiationId: string): Promise<void> {
Expand Down Expand Up @@ -273,29 +305,4 @@ export class UserActorSignalProcessor {
await this.clientIsActive();
}
}

private async onSignalingError(errorMessage?: string): Promise<void> {
logger.error({ msg: 'Client reported a signaling error', errorMessage, callId: this.callId, role: this.role, state: this.call.state });
await mediaCallDirector.hangup(this.call, this.agent, 'signaling-error');
}

private async onServiceError(errorMessage?: string): Promise<void> {
logger.error({ msg: 'Client reported a service error', errorMessage, callId: this.callId, role: this.role, state: this.call.state });
if (this.isPastNegotiation()) {
return;
}

await mediaCallDirector.hangup(this.call, this.agent, 'service-error');
}

private async onUnexpectedError(errorMessage?: string): Promise<void> {
logger.error({
msg: 'Client reported an unexpected error',
errorMessage,
callId: this.callId,
role: this.role,
state: this.call.state,
});
await mediaCallDirector.hangup(this.call, this.agent, 'error');
}
}
5 changes: 5 additions & 0 deletions packages/core-typings/src/mediaCalls/IMediaCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export interface IMediaCall extends IRocketChatRecord {

expiresAt: Date;

/** The timestamp of the moment the callee accepted the call */
acceptedAt?: Date;
/** The timestamp of the moment either side reported the call as active for the first time */
activatedAt?: Date;

callerRequestedId?: string;
parentCallId?: string;

Expand Down
10 changes: 10 additions & 0 deletions packages/core-typings/src/mediaCalls/IMediaCallChannel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { IRocketChatRecord } from '../IRocketChatRecord';
import type { MediaCallActorType } from './IMediaCall';

export type MediaCallChannelError = {
ts: Date;
errorType: string;
errorCode?: string;
errorDetails?: string;
negotiationId?: string;
};

export interface IMediaCallChannel extends IRocketChatRecord {
callId: string;

Expand All @@ -20,4 +28,6 @@ export interface IMediaCallChannel extends IRocketChatRecord {
activeAt?: Date;
// The moment when the user left the call or hanged up
leftAt?: Date;

errors?: MediaCallChannelError[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type CallHangupReason =
| 'signaling-error' // Hanging up because of an error during the signal processing
| 'service-error' // Hanging up because of an error setting up the service connection
| 'media-error' // Hanging up because of an error setting up the media connection
| 'input-error' // Something wrong with the audio input track on the client
| 'error' // Hanging up because of an unidentified error
| 'unknown'; // One of the call's signed users reported they don't know this call

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type ServiceStateValue<ServiceStateMap extends DefaultServiceStateMap, K

export type ServiceProcessorEvents<ServiceStateMap extends DefaultServiceStateMap> = {
internalStateChange: keyof ServiceStateMap;
internalError: { critical: boolean; error: string | Error };
internalError: { critical: boolean; error: string | Error; errorDetails?: string };
negotiationNeeded: void;
};

Expand Down
10 changes: 10 additions & 0 deletions packages/media-signaling/src/definition/signals/client/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type ClientMediaSignalError = {
errorType?: 'signaling' | 'service' | 'other';
errorCode?: string;
negotiationId?: string;
critical?: boolean;
errorDetails?: string;
};

export const clientMediaSignalErrorSchema: JSONSchemaType<ClientMediaSignalError> = {
Expand Down Expand Up @@ -41,6 +43,14 @@ export const clientMediaSignalErrorSchema: JSONSchemaType<ClientMediaSignalError
type: 'string',
nullable: true,
},
critical: {
type: 'boolean',
nullable: true,
},
errorDetails: {
type: 'string',
nullable: true,
},
},
additionalProperties: false,
required: ['callId', 'contractId', 'type'],
Expand Down
68 changes: 56 additions & 12 deletions packages/media-signaling/src/lib/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { ClientContractState, ClientState } from '../definition/client';
import type { IMediaSignalLogger } from '../definition/logger';
import type { IWebRTCProcessor, WebRTCInternalStateMap } from '../definition/services';
import { isPendingState } from './services/states';
import { serializeError } from './utils/serializeError';
import type {
ServerMediaSignal,
ServerMediaSignalNewCall,
Expand Down Expand Up @@ -275,6 +276,12 @@ export class ClientMediaCall implements IClientMediaCall {
try {
this.prepareWebRtcProcessor();
} catch (e) {
this.sendError({
errorType: 'service',
errorCode: 'service-initialization-failed',
critical: true,
errorDetails: serializeError(e),
});
await this.rejectAsUnavailable();
throw e;
}
Expand Down Expand Up @@ -725,7 +732,7 @@ export class ClientMediaCall implements IClientMediaCall {
const { negotiationId } = signal;

if (this.shouldIgnoreWebRTC()) {
this.sendError({ errorType: 'service', errorCode: 'invalid-service', negotiationId });
this.sendError({ errorType: 'service', errorCode: 'invalid-service', negotiationId, critical: true });
return;
}

Expand All @@ -742,12 +749,18 @@ export class ClientMediaCall implements IClientMediaCall {
try {
offer = await this.webrtcProcessor.createOffer({ iceRestart });
} catch (e) {
this.sendError({ errorType: 'service', errorCode: 'failed-to-create-offer', negotiationId });
this.sendError({
errorType: 'service',
errorCode: 'failed-to-create-offer',
negotiationId,
critical: true,
errorDetails: serializeError(e),
});
throw e;
}

if (!offer) {
this.sendError({ errorType: 'service', errorCode: 'implementation-error', negotiationId });
this.sendError({ errorType: 'service', errorCode: 'implementation-error', negotiationId, critical: true });
}

await this.deliverSdp({ ...offer, negotiationId });
Expand Down Expand Up @@ -797,12 +810,18 @@ export class ClientMediaCall implements IClientMediaCall {
answer = await this.webrtcProcessor.createAnswer(signal);
} catch (e) {
this.config.logger?.error(e);
this.sendError({ errorType: 'service', errorCode: 'failed-to-create-answer', negotiationId });
this.sendError({
errorType: 'service',
errorCode: 'failed-to-create-answer',
negotiationId,
critical: true,
errorDetails: JSON.stringify(e),
});
throw e;
}

if (!answer) {
this.sendError({ errorType: 'service', errorCode: 'implementation-error', negotiationId });
this.sendError({ errorType: 'service', errorCode: 'implementation-error', negotiationId, critical: true });
return;
}

Expand Down Expand Up @@ -930,7 +949,7 @@ export class ClientMediaCall implements IClientMediaCall {
}

if (!this.acceptedLocally) {
this.config.transporter.sendError(this.callId, { errorType: 'signaling', errorCode: 'not-accepted' });
this.config.transporter.sendError(this.callId, { errorType: 'signaling', errorCode: 'not-accepted', critical: true });
this.config.logger?.error('Trying to activate a call that was not yet accepted locally.');
return;
}
Expand Down Expand Up @@ -1033,14 +1052,25 @@ export class ClientMediaCall implements IClientMediaCall {
}
}

private onWebRTCInternalError({ critical, error }: { critical: boolean; error: string | Error }): void {
private onWebRTCInternalError({
critical,
error,
errorDetails,
}: {
critical: boolean;
error: string | Error;
errorDetails?: string;
}): void {
this.config.logger?.debug('ClientMediaCall.onWebRTCInternalError', critical, error);
const errorCode = typeof error === 'object' ? error.message : error;
this.sendError({ errorType: 'service', errorCode, ...(this.currentNegotiationId && { negotiationId: this.currentNegotiationId }) });

if (critical) {
this.hangup('service-error');
}
this.sendError({
errorType: 'service',
errorCode,
...(this.currentNegotiationId && { negotiationId: this.currentNegotiationId }),
...(errorDetails && { errorDetails }),
critical,
});
}

private onWebRTCNegotiationNeeded(): void {
Expand Down Expand Up @@ -1069,11 +1099,25 @@ export class ClientMediaCall implements IClientMediaCall {
break;
case 'failed':
if (!this.isOver()) {
this.sendError({
errorType: 'service',
errorCode: 'connection-failed',
critical: true,
negotiationId: this.currentNegotiationId || undefined,
});

this.hangup('service-error');
}
break;
case 'closed':
if (!this.isOver()) {
this.sendError({
errorType: 'service',
errorCode: 'connection-closed',
critical: true,
negotiationId: this.currentNegotiationId || undefined,
});

this.hangup('service-error');
}
break;
Expand Down Expand Up @@ -1143,7 +1187,7 @@ export class ClientMediaCall implements IClientMediaCall {
try {
this.prepareWebRtcProcessor();
} catch (e) {
this.sendError({ errorType: 'service', errorCode: 'webrtc-not-implemented' });
this.sendError({ errorType: 'service', errorCode: 'webrtc-not-implemented', critical: true, errorDetails: serializeError(e) });
throw e;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/media-signaling/src/lib/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ export class MediaSignalingSession extends Emitter<MediaSignalingEvents> {
}

try {
call.hangup('service-error');
call.hangup('input-error');
} catch {
//
}
Expand Down
4 changes: 3 additions & 1 deletion packages/media-signaling/src/lib/TransportWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ export class MediaSignalTransportWrapper {
} as GenericClientMediaSignal<T>);
}

public sendError(callId: string, { errorType, errorCode, negotiationId }: Partial<ClientMediaSignalError>) {
public sendError(callId: string, { errorType, errorCode, negotiationId, critical, errorDetails }: Partial<ClientMediaSignalError>) {
this.sendToServer(callId, 'error', {
errorType: errorType || 'other',
...(errorCode && { errorCode }),
...(negotiationId && { negotiationId }),
...(critical ? { critical } : { critical: false }),
...(errorDetails && { errorDetails }),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
}
this.config.logger?.debug('MediaCallWebRTCProcessor.onIceCandidateError');
this.config.logger?.error(event);
this.emitter.emit('internalError', { critical: false, error: 'ice-candidate-error' });

this.emitter.emit('internalError', { critical: false, error: 'ice-candidate-error', errorDetails: JSON.stringify(event) });
}

private onNegotiationNeeded() {
Expand Down
37 changes: 37 additions & 0 deletions packages/media-signaling/src/lib/utils/serializeError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function serializeError(error: unknown): string | undefined {
try {
if (!error) {
return undefined;
}

if (typeof error === 'string') {
return error;
}

if (typeof error === 'object') {
if (error instanceof Error) {
return JSON.stringify({
...error,
name: error.name,
message: error.message,
});
}

const errorData: Record<string, any> = { ...error };
if ('name' in error) {
errorData.name = error.name;
}
if ('message' in error) {
errorData.message = error.message;
}

if (Object.keys(errorData).length > 0) {
return JSON.stringify(errorData);
}
}
} catch {
//
}

return undefined;
}
Loading
Loading