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

SPARK-532493 webrtc core should expose separate events for ice and connection state #78

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
62 changes: 53 additions & 9 deletions src/connection-state-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ConnectionState, ConnectionStateHandler } from './connection-state-handler';
import {
ConnectionState,
ConnectionStateHandler,
IceConnectionState,
} from './connection-state-handler';

describe('ConnectionStateHandler', () => {
let fakeIceState: RTCIceConnectionState;
Expand All @@ -24,18 +28,25 @@ describe('ConnectionStateHandler', () => {
expect(connStateHandler.getConnectionState()).toStrictEqual(ConnectionState.New);
});

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

expect(connStateHandler.getIceConnectionState()).toStrictEqual(IceConnectionState.New);
});

it('updates ice 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);
connStateHandler.on(ConnectionStateHandler.Events.IceConnectionStateChanged, (state) => {
expect(state).toStrictEqual(IceConnectionState.Checking);
});

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

expect(connStateHandler.getConnectionState()).toStrictEqual(ConnectionState.Connecting);
expect(connStateHandler.getIceConnectionState()).toStrictEqual(IceConnectionState.Checking);
});

it("updates connection state on RTCPeerConnection's connection state change", () => {
Expand All @@ -52,6 +63,42 @@ describe('ConnectionStateHandler', () => {
expect(connStateHandler.getConnectionState()).toStrictEqual(ConnectionState.Connecting);
});

[
{ iceState: 'new', expected: IceConnectionState.New },
{ iceState: 'checking', expected: IceConnectionState.Checking },
{ iceState: 'connected', expected: IceConnectionState.Connected },
{ iceState: 'completed', expected: IceConnectionState.Completed },
{ iceState: 'failed', expected: IceConnectionState.Failed },
{ iceState: 'disconnected', expected: IceConnectionState.Disconnected },
].forEach(({ iceState, expected }) => {
it(`evaluates iceConnectionState to ${expected} when ice state = ${iceState}`, () => {
expect.assertions(1);
const connStateHandler = new ConnectionStateHandler(fakeCallback);

fakeIceState = iceState as RTCIceConnectionState;

expect(connStateHandler.getIceConnectionState()).toStrictEqual(expected);
});
});

[
{ connState: 'new', expected: ConnectionState.New },
{ connState: 'connecting', expected: ConnectionState.Connecting },
{ connState: 'connected', expected: ConnectionState.Connected },
{ connState: 'disconnected', expected: ConnectionState.Disconnected },
{ connState: 'failed', expected: ConnectionState.Failed },
{ connState: 'closed', expected: ConnectionState.Closed },
].forEach(({ connState, expected }) => {
it(`evaluates ConnectionState to ${expected} when connection state = ${connState}`, () => {
expect.assertions(1);
const connStateHandler = new ConnectionStateHandler(fakeCallback);

fakeConnectionState = connState as RTCPeerConnectionState;

expect(connStateHandler.getConnectionState()).toStrictEqual(expected);
});
});

// 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
Expand Down Expand Up @@ -118,10 +165,7 @@ describe('ConnectionStateHandler', () => {
fakeConnectionState = connState;
fakeIceState = iceState;

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

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

// Overall connection state (based on the ICE and DTLS connection states)
export enum OverallConnectionState {
New = 'New',
Connecting = 'Connecting',
Connected = 'Connected',
Disconnected = 'Disconnected',
Failed = 'Failed',
Closed = 'Closed',
}

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
Expand All @@ -11,12 +19,24 @@ export enum ConnectionState {
Failed = 'Failed', // connection failed, an ICE restart is required
}

export enum IceConnectionState {
New = 'New',
Checking = 'Checking',
Connected = 'Connected',
Completed = 'Completed',
Failed = 'Failed',
Disconnected = 'Disconnected',
Closed = 'Closed',
}

enum ConnectionStateEvents {
ConnectionStateChanged = 'ConnectionStateChanged',
IceConnectionStateChanged = 'IceConnectionStateChanged',
}

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

type GetCurrentStatesCallback = () => {
Expand All @@ -31,8 +51,6 @@ type GetCurrentStatesCallback = () => {
export class ConnectionStateHandler extends EventEmitter<ConnectionStateEventHandlers> {
static Events = ConnectionStateEvents;

private mediaConnectionState: ConnectionState;

private getCurrentStatesCallback: GetCurrentStatesCallback;

/**
Expand All @@ -44,33 +62,24 @@ export class ConnectionStateHandler extends EventEmitter<ConnectionStateEventHan
constructor(getCurrentStatesCallback: GetCurrentStatesCallback) {
super();
this.getCurrentStatesCallback = getCurrentStatesCallback;
this.mediaConnectionState = this.evaluateMediaConnectionState();
}

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

this.emit(ConnectionStateEvents.ConnectionStateChanged, state);
}

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

/**
* 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);
}
this.emit(ConnectionStateEvents.IceConnectionStateChanged, state);
}

/**
Expand All @@ -79,25 +88,25 @@ export class ConnectionStateHandler extends EventEmitter<ConnectionStateEventHan
*
* @returns Current overall connection state.
*/
private evaluateMediaConnectionState() {
private evaluateMediaConnectionState(): OverallConnectionState {
const { connectionState, iceState } = this.getCurrentStatesCallback();

const connectionStates = [connectionState, iceState];

let mediaConnectionState;
let mediaConnectionState: OverallConnectionState;

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

logger.log(
Expand All @@ -113,6 +122,32 @@ export class ConnectionStateHandler extends EventEmitter<ConnectionStateEventHan
* @returns Current connection state.
*/
public getConnectionState(): ConnectionState {
return this.mediaConnectionState;
const { connectionState } = this.getCurrentStatesCallback();

const state = connectionState[0].toUpperCase() + connectionState.slice(1);

return state as ConnectionState;
}

/**
* Gets current ice connection state.
*
* @returns Current ice connection state.
*/
public getIceConnectionState(): IceConnectionState {
const { iceState } = this.getCurrentStatesCallback();

const state = iceState[0].toUpperCase() + iceState.slice(1);

return state as IceConnectionState;
marcin-bazyl marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Gets current overall connection state.
*
* @returns Current overall connection state.
*/
public getOverallConnectionState(): OverallConnectionState {
k-wasniowski marked this conversation as resolved.
Show resolved Hide resolved
return this.evaluateMediaConnectionState();
}
}
23 changes: 22 additions & 1 deletion src/peer-connection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { BrowserInfo } from '@webex/web-capabilities';
import { MockedObjectDeep } from 'ts-jest';
import { ConnectionState, ConnectionStateHandler } from './connection-state-handler';
import {
ConnectionState,
ConnectionStateHandler,
IceConnectionState,
} from './connection-state-handler';
import { mocked } from './mocks/mock';
import { RTCPeerConnectionStub } from './mocks/rtc-peer-connection-stub';
import { PeerConnection } from './peer-connection';
Expand Down Expand Up @@ -246,6 +250,23 @@ describe('PeerConnection', () => {
const connectionStateHandlerListener = connectionStateHandler.on.mock.calls[0][1];
connectionStateHandlerListener(ConnectionState.Connecting);
});
it("listens on ConnectionStateHandler's IceConnectionStateChange event and emits it", () => {
expect.assertions(2);
const connectionStateHandler = getInstantiatedConnectionStateHandler();

pc.on(PeerConnection.Events.IceConnectionStateChange, (state) => {
expect(state).toStrictEqual(IceConnectionState.Checking);
});

// verify that PeerConnection listens for the right event
expect(connectionStateHandler.on.mock.calls[1][0]).toStrictEqual(
ConnectionStateHandler.Events.IceConnectionStateChanged
);

// trigger the fake event from ConnectionStateHandler
const connectionStateHandlerListener = connectionStateHandler.on.mock.calls[1][1];
connectionStateHandlerListener(IceConnectionState.Checking);
});
});
describe('createAnswer', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
Expand Down
40 changes: 38 additions & 2 deletions src/peer-connection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { BrowserInfo } from '@webex/web-capabilities';
import { ConnectionState, ConnectionStateHandler } from './connection-state-handler';
import {
ConnectionState,
ConnectionStateHandler,
IceConnectionState,
OverallConnectionState,
} from './connection-state-handler';
import { EventEmitter, EventMap } from './event-emitter';
import { createRTCPeerConnection } from './rtc-peer-connection-factory';
import { logger } from './util/logger';
Expand Down Expand Up @@ -29,6 +34,7 @@ enum PeerConnectionEvents {
IceGatheringStateChange = 'icegatheringstatechange',
IceCandidate = 'icecandidate',
ConnectionStateChange = 'connectionstatechange',
IceConnectionStateChange = 'iceconnectionstatechange',
CreateOfferOnSuccess = 'createofferonsuccess',
CreateAnswerOnSuccess = 'createansweronsuccess',
SetLocalDescriptionOnSuccess = 'setlocaldescriptiononsuccess',
Expand All @@ -39,6 +45,7 @@ interface PeerConnectionEventHandlers extends EventMap {
[PeerConnectionEvents.IceGatheringStateChange]: (ev: IceGatheringStateChangeEvent) => void;
[PeerConnectionEvents.IceCandidate]: (ev: RTCPeerConnectionIceEvent) => void;
[PeerConnectionEvents.ConnectionStateChange]: (state: ConnectionState) => void;
[PeerConnectionEvents.IceConnectionStateChange]: (state: IceConnectionState) => void;
[PeerConnectionEvents.CreateOfferOnSuccess]: (offer: RTCSessionDescriptionInit) => void;
[PeerConnectionEvents.CreateAnswerOnSuccess]: (answer: RTCSessionDescriptionInit) => void;
[PeerConnectionEvents.SetLocalDescriptionOnSuccess]: (
Expand Down Expand Up @@ -85,6 +92,13 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
}
);

this.connectionStateHandler.on(
ConnectionStateHandler.Events.IceConnectionStateChanged,
(state: IceConnectionState) => {
this.emit(PeerConnection.Events.IceConnectionStateChange, state);
}
);

// Forward the connection state related events to connection state handler
// eslint-disable-next-line jsdoc/require-jsdoc
this.pc.oniceconnectionstatechange = () =>
Expand Down Expand Up @@ -118,10 +132,28 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
*
* @returns The underlying connection's overall state.
*/
getOverallConnectionState(): OverallConnectionState {
return this.connectionStateHandler.getOverallConnectionState();
}

/**
* Gets the connection state of the underlying RTCPeerConnection.
*
* @returns The underlying RTCPeerConnection connection state.
*/
getConnectionState(): ConnectionState {
return this.connectionStateHandler.getConnectionState();
}

/**
* Gets the ICE connection state of the underlying RTCPeerConnection.
*
* @returns The underlying RTCPeerConnection ICE connection state.
*/
getIceConnectionState(): IceConnectionState {
return this.connectionStateHandler.getIceConnectionState();
}

/**
* Adds a new media track to the set of tracks which will be transmitted to the other peer.
*
Expand Down Expand Up @@ -374,5 +406,9 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
}
}

export { ConnectionState } from './connection-state-handler';
export {
ConnectionState,
IceConnectionState,
OverallConnectionState,
} from './connection-state-handler';
export { MediaStreamTrackKind, PeerConnection, RTCDataChannelOptions };
Loading