-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added connection state handling (#8)
also fixed various warnings that are currently on master
- Loading branch information
1 parent
4f8d27b
commit d2debac
Showing
10 changed files
with
389 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,8 @@ | |
"transpiled", | ||
"typedoc", | ||
"untracked", | ||
"WCME", | ||
"Wcme", | ||
"webrtc" | ||
], | ||
"flagWords": [], | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}) | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
|
||
/** | ||
* 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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.