Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
68 changes: 34 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,44 @@ 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,
errorDetails,
negotiationId,
callId: this.callId,
role: this.role,
state: this.call.state,
});

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 +298,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');
}
}
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
69 changes: 57 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,19 @@ 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 });
return;
}

await this.deliverSdp({ ...offer, negotiationId });
Expand Down Expand Up @@ -797,12 +811,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: serializeError(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 +950,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 +1053,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 +1100,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 +1188,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