Skip to content

Commit

Permalink
Reconnect on network change (#53)
Browse files Browse the repository at this point in the history
## Description

This PR:
- Introduces new events: `reconnectionStarted`, `reconnected`,
`reconnectionFailed`
- Exposes the `ongoingReconnection` status
- Waits for all tracks to be added to `RTCPeerConnection` after
successful reconnection
- Removes the outdated `peer already connected` auth join reason

## Motivation and Context

It fixes the reconnection mechanism when the user changes the network
(e.g., from Wi-Fi to a hotspot).

## Types of changes

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
  • Loading branch information
kamil-stasiak authored Jun 26, 2024
1 parent c9756c6 commit e71e738
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 24 deletions.
20 changes: 18 additions & 2 deletions src/FishjamClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EventEmitter } from 'events';
import { PeerMessage } from './protos';
import { ReconnectConfig, ReconnectManager } from './reconnection';
import { AuthErrorReason, isAuthError } from './auth';
import { Deferred } from './webrtc/deferred';

export type Peer<PeerMetadata, TrackMetadata> = Endpoint<
PeerMetadata,
Expand Down Expand Up @@ -74,6 +75,15 @@ export interface MessageEvents<PeerMetadata, TrackMetadata> {
/** Emitted when the connection is closed */
disconnected: () => void;

/** Emitted when the process of reconnection starts */
reconnectionStarted: () => void;

/** Emitted on successful reconnection */
reconnected: () => void;

/** Emitted when the maximum number of reconnection retries is reached */
reconnectionRetriesLimitReached: () => void;

/**
* Called when peer was accepted.
*/
Expand Down Expand Up @@ -387,7 +397,7 @@ export class FishjamClient<
this.initConnection(config.peerMetadata);
}

private initConnection(peerMetadata: PeerMetadata): void {
private async initConnection(peerMetadata: PeerMetadata): Promise<void> {
if (this.status === 'initialized') {
this.disconnect();
}
Expand Down Expand Up @@ -558,7 +568,7 @@ export class FishjamClient<

this.webrtc?.on(
'connected',
(
async (
peerId: string,
endpointsInRoom: Endpoint<PeerMetadata, TrackMetadata>[],
) => {
Expand All @@ -572,6 +582,8 @@ export class FishjamClient<
(component) => component as Component<PeerMetadata, TrackMetadata>,
);

await this.reconnectManager.handleReconnect();

this.emit('joined', peerId, peers, components);
},
);
Expand Down Expand Up @@ -1033,6 +1045,10 @@ export class FishjamClient<
this.webrtc.updateTrackMetadata(trackId, trackMetadata);
};

public isReconnecting() {
return this.reconnectManager.isReconnecting();
}

/**
* Leaves the room. This function should be called when user leaves the room in a clean way e.g. by clicking a
* dedicated, custom button `disconnect`. As a result there will be generated one more media event that should be sent
Expand Down
1 change: 0 additions & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export const AUTH_ERROR_REASONS = [
'expired token',
'room not found',
'peer not found',
'peer already connected',
] as const;

export type AuthErrorReason = (typeof AUTH_ERROR_REASONS)[number];
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type {
SignalingUrl,
} from './FishjamClient';

export type { ReconnectConfig } from './reconnection';
export type { ReconnectConfig, ReconnectionStatus } from './reconnection';

export type { AuthErrorReason } from './auth.js';

Expand Down
44 changes: 24 additions & 20 deletions src/reconnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Endpoint } from './webrtc';
import { FishjamClient, MessageEvents } from './FishjamClient';
import { isAuthError } from './auth';

export type ReconnectionStatus = 'reconnecting' | 'idle' | 'error';

export type ReconnectConfig = {
/*
+ default: 3
Expand Down Expand Up @@ -47,8 +49,7 @@ export class ReconnectManager<PeerMetadata, TrackMetadata> {

private reconnectAttempt: number = 0;
private reconnectTimeoutId: NodeJS.Timeout | null = null;
private reconnectFailedNotificationSend: boolean = false;
private ongoingReconnection: boolean = false;
private status: ReconnectionStatus = 'idle';
private lastLocalEndpoint: Endpoint<PeerMetadata, TrackMetadata> | null =
null;
private removeEventListeners: () => void = () => {};
Expand Down Expand Up @@ -96,23 +97,18 @@ export class ReconnectManager<PeerMetadata, TrackMetadata> {
};
this.client.on('authSuccess', onAuthSuccess);

const onJoined: MessageEvents<
PeerMetadata,
TrackMetadata
>['joined'] = () => {
this.handleReconnect();
};
this.client.on('joined', onJoined);

this.removeEventListeners = () => {
this.client.off('socketError', onSocketError);
this.client.off('connectionError', onConnectionError);
this.client.off('socketClose', onSocketClose);
this.client.off('authSuccess', onAuthSuccess);
this.client.off('joined', onJoined);
};
}

public isReconnecting(): boolean {
return this.status === 'reconnecting';
}

public reset(initialMetadata: PeerMetadata) {
this.initialMetadata = initialMetadata;
this.reconnectAttempt = 0;
Expand All @@ -128,14 +124,19 @@ export class ReconnectManager<PeerMetadata, TrackMetadata> {
if (this.reconnectTimeoutId) return;

if (this.reconnectAttempt >= this.reconnectConfig.maxAttempts) {
if (!this.reconnectFailedNotificationSend) {
this.reconnectFailedNotificationSend = true;
if (this.status === 'reconnecting') {
this.status = 'error';

this.client.emit('reconnectionRetriesLimitReached');
}
return;
}

if (!this.ongoingReconnection) {
this.ongoingReconnection = true;
if (this.status !== 'reconnecting') {
this.status = 'reconnecting';

this.client.emit('reconnectionStarted');

this.lastLocalEndpoint = this.client.getLocalEndpoint() || null;
}

Expand All @@ -152,11 +153,12 @@ export class ReconnectManager<PeerMetadata, TrackMetadata> {
}, timeout);
}

private handleReconnect() {
if (!this.ongoingReconnection) return;
public async handleReconnect() {
if (this.status !== 'reconnecting') return;

if (this.lastLocalEndpoint && this.reconnectConfig.addTracksOnReconnect) {
this.lastLocalEndpoint.tracks.forEach(async (track) => {
for await (const element of this.lastLocalEndpoint.tracks) {
const [_, track] = element;
if (!track.track || track.track.readyState !== 'live') return;

await this.client.addTrack(
Expand All @@ -165,11 +167,13 @@ export class ReconnectManager<PeerMetadata, TrackMetadata> {
track.simulcastConfig,
track.maxBandwidth,
);
});
}
}

this.lastLocalEndpoint = null;
this.ongoingReconnection = false;
this.status = 'idle';

this.client.emit('reconnected');
}

public cleanup() {
Expand Down

0 comments on commit e71e738

Please sign in to comment.