Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added connection state handling #8

Merged
merged 1 commit into from
Nov 14, 2022
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
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"transpiled",
"typedoc",
"untracked",
"WCME",
"Wcme",
"webrtc"
],
"flagWords": [],
Expand Down
127 changes: 127 additions & 0 deletions src/connection-state-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { ConnectionState, ConnectionStateHandler } from './connection-state-handler';

describe('ConnectionStateHandler', () => {
let fakeIceState: RTCIceConnectionState;
let fakeConnectionState: RTCPeerConnectionState;

// eslint-disable-next-line jsdoc/require-jsdoc
const fakeCallback = () => {
return {
connectionState: fakeConnectionState,
iceState: fakeIceState,
};
};

beforeEach(() => {
fakeIceState = 'new';
fakeConnectionState = 'new';
});

it('reads initial connection state', () => {
expect.assertions(1);
const connStateHandler = new ConnectionStateHandler(fakeCallback);

expect(connStateHandler.getConnectionState()).toStrictEqual(ConnectionState.New);
});

it('updates connection state on ice connection state change and emits the event', () => {
expect.assertions(2);
const connStateHandler = new ConnectionStateHandler(fakeCallback);

connStateHandler.on(ConnectionStateHandler.Events.ConnectionStateChanged, (state) => {
expect(state).toStrictEqual(ConnectionState.Connecting);
});

fakeIceState = 'checking';
connStateHandler.onIceConnectionStateChange();

expect(connStateHandler.getConnectionState()).toStrictEqual(ConnectionState.Connecting);
});

it("updates connection state on RTCPeerConnection's connection state change", () => {
expect.assertions(2);
const connStateHandler = new ConnectionStateHandler(fakeCallback);

connStateHandler.on(ConnectionStateHandler.Events.ConnectionStateChanged, (state) => {
expect(state).toStrictEqual(ConnectionState.Connecting);
});

fakeConnectionState = 'connecting';
connStateHandler.onConnectionStateChange();

expect(connStateHandler.getConnectionState()).toStrictEqual(ConnectionState.Connecting);
});

// test matrix for all possible combinations of iceConnectionState and connectionState
// some of these cases theoretically should never happen (like iceState: 'closed', connState: 'connected' )
// but we test them anyway for completeness
const testCases: Array<{
iceState: RTCIceConnectionState;
connState: RTCPeerConnectionState;
expected: ConnectionState;
}> = [
{ iceState: 'new', connState: 'new', expected: ConnectionState.New },
{ iceState: 'new', connState: 'connecting', expected: ConnectionState.Connecting },
{ iceState: 'new', connState: 'connected', expected: ConnectionState.Connecting },
{ iceState: 'new', connState: 'disconnected', expected: ConnectionState.Disconnected },
{ iceState: 'new', connState: 'failed', expected: ConnectionState.Failed },
{ iceState: 'new', connState: 'closed', expected: ConnectionState.Closed },

{ iceState: 'checking', connState: 'new', expected: ConnectionState.Connecting },
{ iceState: 'checking', connState: 'connecting', expected: ConnectionState.Connecting },
{ iceState: 'checking', connState: 'connected', expected: ConnectionState.Connecting },
{ iceState: 'checking', connState: 'disconnected', expected: ConnectionState.Disconnected },
{ iceState: 'checking', connState: 'failed', expected: ConnectionState.Failed },
{ iceState: 'checking', connState: 'closed', expected: ConnectionState.Closed },

{ iceState: 'connected', connState: 'new', expected: ConnectionState.Connecting },
{ iceState: 'connected', connState: 'connecting', expected: ConnectionState.Connecting },
{ iceState: 'connected', connState: 'connected', expected: ConnectionState.Connected },
{ iceState: 'connected', connState: 'disconnected', expected: ConnectionState.Disconnected },
{ iceState: 'connected', connState: 'failed', expected: ConnectionState.Failed },
{ iceState: 'connected', connState: 'closed', expected: ConnectionState.Closed },

{ iceState: 'completed', connState: 'new', expected: ConnectionState.Connecting },
{ iceState: 'completed', connState: 'connecting', expected: ConnectionState.Connecting },
{ iceState: 'completed', connState: 'connected', expected: ConnectionState.Connected },
{ iceState: 'completed', connState: 'disconnected', expected: ConnectionState.Disconnected },
{ iceState: 'completed', connState: 'failed', expected: ConnectionState.Failed },
{ iceState: 'completed', connState: 'closed', expected: ConnectionState.Closed },

{ iceState: 'failed', connState: 'new', expected: ConnectionState.Failed },
{ iceState: 'failed', connState: 'connecting', expected: ConnectionState.Failed },
{ iceState: 'failed', connState: 'connected', expected: ConnectionState.Failed },
{ iceState: 'failed', connState: 'disconnected', expected: ConnectionState.Failed },
{ iceState: 'failed', connState: 'failed', expected: ConnectionState.Failed },
{ iceState: 'failed', connState: 'closed', expected: ConnectionState.Closed },

{ iceState: 'disconnected', connState: 'new', expected: ConnectionState.Disconnected },
{ iceState: 'disconnected', connState: 'connecting', expected: ConnectionState.Disconnected },
{ iceState: 'disconnected', connState: 'connected', expected: ConnectionState.Disconnected },
{ iceState: 'disconnected', connState: 'disconnected', expected: ConnectionState.Disconnected },
{ iceState: 'disconnected', connState: 'failed', expected: ConnectionState.Failed },
{ iceState: 'disconnected', connState: 'closed', expected: ConnectionState.Closed },

{ iceState: 'closed', connState: 'new', expected: ConnectionState.Closed },
{ iceState: 'closed', connState: 'connecting', expected: ConnectionState.Closed },
{ iceState: 'closed', connState: 'connected', expected: ConnectionState.Closed },
{ iceState: 'closed', connState: 'disconnected', expected: ConnectionState.Closed },
{ iceState: 'closed', connState: 'failed', expected: ConnectionState.Closed },
{ iceState: 'closed', connState: 'closed', expected: ConnectionState.Closed },
];

testCases.forEach(({ iceState, connState, expected }) =>
it(`evaluates overall state to ${expected} when iceConnectionState=${iceState} and connectionState=${connState}`, () => {
expect.assertions(1);
const connStateHandler = new ConnectionStateHandler(fakeCallback);

fakeConnectionState = connState;
fakeIceState = iceState;

// it's sufficient to trigger just one of the callbacks
connStateHandler.onConnectionStateChange();

expect(connStateHandler.getConnectionState()).toStrictEqual(expected);
})
);
});
118 changes: 118 additions & 0 deletions src/connection-state-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { EventEmitter, EventMap } from './event-emitter';
import { logger } from './util/logger';

// Overall connection state (based on the ICE and DTLS connection states)
export enum ConnectionState {
New = 'New', // connection attempt has not been started
Closed = 'Closed', // connection closed, there is no way to move out of this state
Connected = 'Connected', // both ICE and DTLS connections are established, media is flowing
Connecting = 'Connecting', // initial connection attempt in progress
Disconnected = 'Disconnected', // connection lost temporarily, the browser is trying to re-establish it automatically
Failed = 'Failed', // connection failed, an ICE restart is required
}

enum ConnectionStateEvents {
ConnectionStateChanged = 'ConnectionStateChanged',
}

interface ConnectionStateEventHandlers extends EventMap {
[ConnectionStateEvents.ConnectionStateChanged]: (state: ConnectionState) => void;
}

type GetCurrentStatesCallback = () => {
connectionState: RTCPeerConnectionState;
iceState: RTCIceConnectionState;
};

/**
* Listens on the connection's ICE and DTLS state changes and emits a single
* event that summarizes all the internal states into a single overall connection state.
*/
export class ConnectionStateHandler extends EventEmitter<ConnectionStateEventHandlers> {
static Events = ConnectionStateEvents;

private mediaConnectionState: ConnectionState;

private getCurrentStatesCallback: GetCurrentStatesCallback;

/**
* Creates an instance of ConnectionStateHandler.
*
* @param getCurrentStatesCallback - Callback for getting the connection state information
* from the peer connection.
*/
constructor(getCurrentStatesCallback: GetCurrentStatesCallback) {
super();
this.getCurrentStatesCallback = getCurrentStatesCallback;
this.mediaConnectionState = this.evaluateMediaConnectionState();
}

/**
* Handler for connection state change.
*/
public onConnectionStateChange(): void {
this.handleAnyConnectionStateChange();
}

/**
* Handler for ice connection state change.
*/
public onIceConnectionStateChange(): void {
this.handleAnyConnectionStateChange();
}

/**
* Method to be called whenever ice connection or dtls connection state is changed.
*/
private handleAnyConnectionStateChange() {
const newConnectionState = this.evaluateMediaConnectionState();

if (newConnectionState !== this.mediaConnectionState) {
this.mediaConnectionState = newConnectionState;
this.emit(ConnectionStateEvents.ConnectionStateChanged, this.mediaConnectionState);
}
}
Comment on lines +68 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this and onConnectionStateChange could both leverage some private helper method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


/**
* Evaluates the overall connection state based on peer connection's
* connectionState and iceConnectionState.
*
* @returns Current overall connection state.
*/
private evaluateMediaConnectionState() {
const { connectionState, iceState } = this.getCurrentStatesCallback();

const connectionStates = [connectionState, iceState];

let mediaConnectionState;

if (connectionStates.every((value) => value === 'new')) {
mediaConnectionState = ConnectionState.New;
} else if (connectionStates.some((value) => value === 'closed')) {
mediaConnectionState = ConnectionState.Closed;
} else if (connectionStates.some((value) => value === 'failed')) {
mediaConnectionState = ConnectionState.Failed;
} else if (connectionStates.some((value) => value === 'disconnected')) {
mediaConnectionState = ConnectionState.Disconnected;
} else if (connectionStates.every((value) => value === 'connected' || value === 'completed')) {
mediaConnectionState = ConnectionState.Connected;
} else {
mediaConnectionState = ConnectionState.Connecting;
}

logger.log(
`iceConnectionState=${iceState} connectionState=${connectionState} => ${this.mediaConnectionState}`
);

return mediaConnectionState;
}

/**
* Gets current connection state.
*
* @returns Current connection state.
*/
public getConnectionState(): ConnectionState {
return this.mediaConnectionState;
}
}
4 changes: 3 additions & 1 deletion src/event-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import TypedEmitter, { EventMap } from 'typed-emitter';
/**
* Typed event emitter class.
*/
export default class EventEmitter<T extends EventMap> extends (EE as {
export class EventEmitter<T extends EventMap> extends (EE as {
new <TT extends EventMap>(): TypedEmitter<TT>;
})<T> {}

export { EventMap } from 'typed-emitter';
6 changes: 3 additions & 3 deletions src/media/local-track.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle */
import { EventMap } from 'typed-emitter';
import EventEmitter from '../event-emitter';
import { EventEmitter, EventMap } from '../event-emitter';
import { MediaStreamTrackKind } from '../peer-connection';
import { logger } from '../util/logger';

Expand Down Expand Up @@ -47,8 +46,9 @@ export interface TrackEvents extends EventMap {
[LocalTrackEvents.UnderlyingTrackChange]: () => void;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TrackEffect = any;
// TBD: Fix this once types are published seperatly
// TBD: Fix this once types are published separately
// export type TrackEffect = BaseMicrophoneEffect | BaseCameraEffect;

/**
Expand Down
31 changes: 13 additions & 18 deletions src/mocks/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,23 @@ type MaybeMockedConstructor<T> = T extends new (...args: any[]) => infer R
? jest.MockInstance<R, ConstructorArgumentsOf<T>>
: T;

type MockedFunction<T extends MockableFunction> = MockWithArgs<T> &
{
[K in keyof T]: T[K];
};
type MockedFunction<T extends MockableFunction> = MockWithArgs<T> & {
[K in keyof T]: T[K];
};

type MockedFunctionDeep<T extends MockableFunction> = MockWithArgs<T> & MockedObjectDeep<T>;

type MockedObject<T> = MaybeMockedConstructor<T> &
{
[K in MethodKeysOf<T>]: T[K] extends MockableFunction ? MockedFunction<T[K]> : T[K];
} &
{
[K in PropertyKeysOf<T>]: T[K];
};
type MockedObject<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction ? MockedFunction<T[K]> : T[K];
} & {
[K in PropertyKeysOf<T>]: T[K];
};

type MockedObjectDeep<T> = MaybeMockedConstructor<T> &
{
[K in MethodKeysOf<T>]: T[K] extends MockableFunction ? MockedFunctionDeep<T[K]> : T[K];
} &
{
[K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]>;
};
type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction ? MockedFunctionDeep<T[K]> : T[K];
} & {
[K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]>;
};

export type MaybeMockedDeep<T> = T extends MockableFunction
? MockedFunctionDeep<T>
Expand Down
2 changes: 2 additions & 0 deletions src/mocks/rtc-peer-connection-stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class RTCPeerConnectionStub {
createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
return new Promise(() => {});
}
onconnectionstatechange: () => void = () => {};
oniceconnectionstatechange: () => void = () => {};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/peer-connection-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function getLocalDescriptionWithIceCandidates(
}
};
peerConnection.on(PeerConnection.Events.IceGatheringStateChange, (e) => {
if (e.target.iceGatheringState === 'complete') {
if ((e.target as RTCPeerConnection).iceGatheringState === 'complete') {
getLocalDescAndResolve();
}
// TODO(brian): throw an error if we see an error iceGatheringState
Expand Down
Loading