-
Notifications
You must be signed in to change notification settings - Fork 18
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done