Skip to content

Commit

Permalink
feat: add the api for fetching the connection type (#20)
Browse files Browse the repository at this point in the history
* feat: add the api for fetching the connection type

* fix: add some more test cases & minor changes

Co-authored-by: Brown Zhang <[email protected]>
  • Loading branch information
x-epoch and Brown Zhang authored Dec 20, 2022
1 parent bcc0cbe commit 4e5fe55
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 0 deletions.
3 changes: 3 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,9 @@ class RTCPeerConnectionStub {
createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
return new Promise(() => {});
}
getStats(): Promise<any> {
return new Promise(() => {});
}
onconnectionstatechange: () => void = () => {};
oniceconnectionstatechange: () => void = () => {};
}
Expand Down
148 changes: 148 additions & 0 deletions src/peer-connection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,155 @@ jest.mock('./connection-state-handler');

const mockCreateRTCPeerConnection = mocked(createRTCPeerConnection, true);

// eslint-disable-next-line jsdoc/require-jsdoc
function constructCandidatePairStats(id: string, localCandidateId: string, state: string) {
return {
id: `${id}`,
timestamp: 1671091266890.878,
type: 'candidate-pair',
transportId: 'T11',
localCandidateId: `${localCandidateId}`,
remoteCandidateId: 'I+UUFv24B',
state: `${state}`,
};
}

// eslint-disable-next-line jsdoc/require-jsdoc
function constructLocalCandidateStats(
id: string,
protocol: string,
relayProtocol: string | undefined
) {
return {
id,
timestamp: 1671091266890.878,
type: 'local-candidate',
transportId: 'T21',
isRemote: false,
networkType: 'vpn',
ip: '2001:420:c0c8:1005::456',
address: '2001:420:c0c8:1005::456',
port: 53906,
protocol,
candidateType: 'host',
priority: 2122197247,
relayProtocol,
};
}

describe('PeerConnection', () => {
describe('getCurrentConnectionType', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const mockPc = mocked(new RTCPeerConnectionStub(), true);
Object.defineProperty(mockPc, 'iceConnectionState', {
value: 'connected',
writable: true,
configurable: true,
});

it('normal case', async () => {
expect.hasAssertions();
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.getStats.mockImplementation(() => {
return new Promise((resolve) => {
resolve([
constructCandidatePairStats('001', 'localCandidateId1', 'succeeded'),
constructCandidatePairStats('002', 'localCandidateId2', 'succeeded'),
constructLocalCandidateStats('localCandidateId1', 'udp', undefined),
constructLocalCandidateStats('localCandidateId1', 'udp', undefined),
]);
});
});
const pc = new PeerConnection();
const connectionType = await pc.getCurrentConnectionType();
expect(connectionType).toBe('UDP');
});
it('first candidate pair state is not succeeded', async () => {
expect.hasAssertions();
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.getStats.mockImplementation(() => {
return new Promise((resolve) => {
resolve([
constructCandidatePairStats('001', 'localCandidateId1', 'failed'),
constructCandidatePairStats('002', 'localCandidateId2', 'succeeded'),
constructLocalCandidateStats('localCandidateId2', 'udp', undefined),
constructLocalCandidateStats('localCandidateId1', 'udp', undefined),
]);
});
});
const pc = new PeerConnection();
const connectionType = await pc.getCurrentConnectionType();
expect(connectionType).toBe('UDP');
});
it('no candidate matched', async () => {
expect.hasAssertions();
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.getStats.mockImplementation(() => {
return new Promise((resolve) => {
resolve([
constructCandidatePairStats('001', 'localCandidateId1', 'failed'),
constructCandidatePairStats('002', 'localCandidateId2', 'succeeded'),
constructLocalCandidateStats('localCandidateId1', 'udp', undefined),
constructLocalCandidateStats('localCandidateId1', 'udp', undefined),
]);
});
});
const pc = new PeerConnection();
const connectionType = await pc.getCurrentConnectionType();
expect(connectionType).toBe('unknown');
});
it('no candidate matched caused by all failed', async () => {
expect.hasAssertions();
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.getStats.mockImplementation(() => {
return new Promise((resolve) => {
resolve([
constructCandidatePairStats('001', 'localCandidateId1', 'failed'),
constructCandidatePairStats('002', 'localCandidateId2', 'failed'),
constructLocalCandidateStats('localCandidateId2', 'udp', undefined),
constructLocalCandidateStats('localCandidateId2', 'udp', undefined),
]);
});
});
const pc = new PeerConnection();
const connectionType = await pc.getCurrentConnectionType();
expect(connectionType).toBe('unknown');
});
it('relay candidate case', async () => {
expect.hasAssertions();
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.getStats.mockImplementation(() => {
return new Promise((resolve) => {
resolve([
constructCandidatePairStats('001', 'localCandidateId1', 'failed'),
constructCandidatePairStats('002', 'localCandidateId2', 'succeeded'),
constructLocalCandidateStats('localCandidateId1', 'udp', 'tls'),
constructLocalCandidateStats('localCandidateId2', 'udp', 'tls'),
]);
});
});
const pc = new PeerConnection();
const connectionType = await pc.getCurrentConnectionType();
expect(connectionType).toBe('TURN-TLS');
});

it('ice state is not connected/completed', async () => {
expect.hasAssertions();
Object.defineProperty(mockPc, 'iceConnectionState', {
// eslint-disable-next-line jsdoc/require-jsdoc
get() {
return 'disconnected';
},
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
const pc = new PeerConnection();
await expect(pc.getCurrentConnectionType()).rejects.toThrow(
'Ice connection is not established'
);
});
});
it('should pass the correct options through when calling createOffer', async () => {
expect.hasAssertions();
const mockPc = mocked(new RTCPeerConnectionStub(), true);
Expand Down
40 changes: 40 additions & 0 deletions src/peer-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface PeerConnectionEventHandlers extends EventMap {
[PeerConnectionEvents.ConnectionStateChange]: (state: ConnectionState) => void;
}

type ConnectionType = 'UDP' | 'TCP' | 'TURN-TLS' | 'TURN-TCP' | 'TURN-UDP' | 'unknown';
/**
* Manages a single RTCPeerConnection with the server.
*/
Expand Down Expand Up @@ -285,6 +286,45 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
get iceGatheringState(): RTCIceGathererState {
return this.pc.iceGatheringState;
}

/**
* Returns the type of a connection that has been established.
*
* @returns The connection type which would be `ConnectionType`.
*/
async getCurrentConnectionType(): Promise<ConnectionType> {
// make sure this method only can be called when the ice connection is established;
const isIceConnected =
this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed';
if (!isIceConnected) {
throw new Error('Ice connection is not established');
}
const succeededLocalCandidateIds = new Set();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const localCandidateStatsReports: any[] = [];

(await this.pc.getStats()).forEach((report) => {
// collect all local candidate ids from `candidate-pair` stats reports with `succeeded` state.
if (report.type === 'candidate-pair' && report.state?.toLowerCase() === 'succeeded') {
succeededLocalCandidateIds.add(report.localCandidateId);
}
// collect all `local-candidate` stats.
if (report.type === 'local-candidate') {
localCandidateStatsReports.push(report);
}
});
// find the `local-candidate` stats which report id contains in `succeededLocalCandidateIds`.
const localCandidate = localCandidateStatsReports.find((report) =>
succeededLocalCandidateIds.has(report.id)
);
if (!localCandidate) {
return 'unknown';
}
if (localCandidate.relayProtocol) {
return `TURN-${localCandidate.relayProtocol.toUpperCase()}` as ConnectionType;
}
return localCandidate.protocol?.toUpperCase();
}
}

export { ConnectionState } from './connection-state-handler';
Expand Down

0 comments on commit 4e5fe55

Please sign in to comment.