Skip to content

Commit

Permalink
feat: set/create events (#71)
Browse files Browse the repository at this point in the history
* feat(webrtc-core): sdpchange event

* feat(webrtc-core): updated package.json version

* fix(webrtc-core): reverted unnecessary package.json change

* feat(webrtc-core): events for createOffer, setLocalDescription and setRemoteDescription

* test(webrtc-core): added tests for new events

* fix(webrtc-core): added onSuccess suffix to new events

* test(webrtc-core): shouldNThrow param for tests

* test(webrtc-core): throw only in specific test cases

* feat(webrtc-core): create answer on success event

---------

Co-authored-by: Filip Nowakowski <[email protected]>
  • Loading branch information
Filip Nowakowski and fnowakow authored Feb 14, 2024
1 parent 4efe11e commit 92e01d9
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 7 deletions.
6 changes: 5 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
"Wcme",
"WCME",
"webex",
"webrtc"
"webrtc",
"createofferonsuccess",
"createansweronsuccess",
"setlocaldescriptiononsuccess",
"setremotedescriptiononsuccess"
],
"flagWords": [],
"ignorePaths": [
Expand Down
13 changes: 12 additions & 1 deletion src/mocks/rtc-peer-connection-stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@
* This stub exists to act as a scaffold for creating a mock.
*/
class RTCPeerConnectionStub {
createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit> {
return new Promise(() => {});
}
createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
return new Promise(() => {});
}
getStats(): Promise<any> {
return new Promise(() => {});
}
setLocalDescription(): Promise<any> {
setLocalDescription(
description?: RTCSessionDescription | RTCSessionDescriptionInit
): Promise<void> {
return new Promise(() => {});
}

setRemoteDescription(
description?: RTCSessionDescription | RTCSessionDescriptionInit
): Promise<void> {
return new Promise(() => {});
}
onconnectionstatechange: () => void = () => {};
Expand Down
164 changes: 163 additions & 1 deletion src/peer-connection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrowserInfo } from '@webex/web-capabilities';
import { MockedObjectDeep } from 'ts-jest';
import { ConnectionState, ConnectionStateHandler } from './connection-state-handler';
import { mocked } from './mocks/mock';
import { RTCPeerConnectionStub } from './mocks/rtc-peer-connection-stub';
Expand Down Expand Up @@ -246,18 +247,112 @@ describe('PeerConnection', () => {
connectionStateHandlerListener(ConnectionState.Connecting);
});
});
describe('createAnswer', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let createAnswerSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;
const mockedReturnedAnswer: RTCSessionDescriptionInit = {
sdp: 'blah',
type: 'offer',
};

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockPc.createAnswer.mockImplementation(() => {
return Promise.resolve(mockedReturnedAnswer);
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
pc = new PeerConnection();
createAnswerSpy = jest.spyOn(pc, 'createAnswer');
pc.on(PeerConnection.Events.CreateAnswerOnSuccess, callback);
});

it('should emit event when createAnswer called', async () => {
expect.hasAssertions();
const options: RTCAnswerOptions = {
iceRestart: true,
};
const answer = await pc.createAnswer(options);
expect(answer).toStrictEqual(mockedReturnedAnswer);
expect(createAnswerSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(mockedReturnedAnswer);
});

it('should not emit event when createAnswer failed', async () => {
expect.hasAssertions();
mockPc.createAnswer.mockImplementation(() => {
return Promise.reject(new Error());
});
const answerPromise = pc.createAnswer(null as unknown as RTCOfferOptions);
await expect(answerPromise).rejects.toThrow(Error);
expect(createAnswerSpy).toHaveBeenCalledWith(null);
expect(callback).toHaveBeenCalledTimes(0);
});
});

describe('createOffer', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let createOfferSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;
const mockedReturnedOffer: RTCSessionDescriptionInit = {
sdp: 'blah',
type: 'offer',
};

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockPc.createOffer.mockImplementation(() => {
return Promise.resolve(mockedReturnedOffer);
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
pc = new PeerConnection();
createOfferSpy = jest.spyOn(pc, 'createOffer');
pc.on(PeerConnection.Events.CreateOfferOnSuccess, callback);
});

it('should emit event when createOffer called', async () => {
expect.hasAssertions();
const options: RTCOfferOptions = {
iceRestart: true,
};
const offer = await pc.createOffer(options);
expect(offer).toStrictEqual(mockedReturnedOffer);
expect(createOfferSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(mockedReturnedOffer);
});

it('should not emit event when createOffer failed', async () => {
expect.hasAssertions();
mockPc.createOffer.mockImplementation(() => {
return Promise.reject(new Error());
});
const offerPromise = pc.createOffer(null as unknown as RTCOfferOptions);
await expect(offerPromise).rejects.toThrow(Error);
expect(createOfferSpy).toHaveBeenCalledWith(null);
expect(callback).toHaveBeenCalledTimes(0);
});
});

describe('setLocalDescription', () => {
let mockPc: RTCPeerConnectionStub;
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let setLocalDescriptionSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.setLocalDescription.mockImplementation(() => {
return Promise.resolve();
});
setLocalDescriptionSpy = jest.spyOn(mockPc, 'setLocalDescription');
pc = new PeerConnection();
pc.on(PeerConnection.Events.SetLocalDescriptionOnSuccess, callback);
});

it('sets the local description with an SDP offer', async () => {
Expand All @@ -278,5 +373,72 @@ describe('PeerConnection', () => {
pc.setLocalDescription({ type: 'offer', sdp: 'm=video 9 UDP/TLS/RTP' })
).rejects.toThrow(Error);
});

it('should emit event when setLocalDescription called', async () => {
expect.hasAssertions();
const options = {
sdp: 'blah',
};
await pc.setLocalDescription(options as unknown as RTCSessionDescriptionInit);
expect(setLocalDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(options);
});

it('should not emit event when setLocalDescription failed', async () => {
expect.hasAssertions();
mockPc.setLocalDescription.mockImplementation(() => {
return Promise.reject(new Error());
});
const options = {
sdp: 'reject',
};
const offerPromise = pc.setLocalDescription(options as unknown as RTCSessionDescriptionInit);
await expect(offerPromise).rejects.toThrow(Error);
expect(setLocalDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledTimes(0);
});
});

describe('setRemoteDescription', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let setRemoteDescriptionSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockPc.setRemoteDescription.mockImplementation(() => {
return Promise.resolve();
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
pc = new PeerConnection();
setRemoteDescriptionSpy = jest.spyOn(pc, 'setRemoteDescription');
pc.on(PeerConnection.Events.SetRemoteDescriptionOnSuccess, callback);
});

it('should emit event when setRemoteDescription called', async () => {
expect.hasAssertions();
const options = {
sdp: 'blah',
};
await pc.setRemoteDescription(options as unknown as RTCSessionDescriptionInit);
expect(setRemoteDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(options);
});

it('should not emit event when setRemoteDescription failed', async () => {
expect.hasAssertions();
mockPc.setRemoteDescription.mockImplementation(() => {
return Promise.reject(new Error());
});
const options = {
sdp: 'reject',
};
const offerPromise = pc.setRemoteDescription(options as unknown as RTCSessionDescriptionInit);
await expect(offerPromise).rejects.toThrow(Error);
expect(setRemoteDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledTimes(0);
});
});
});
33 changes: 29 additions & 4 deletions src/peer-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConnectionState, ConnectionStateHandler } from './connection-state-hand
import { EventEmitter, EventMap } from './event-emitter';
import { createRTCPeerConnection } from './rtc-peer-connection-factory';
import { logger } from './util/logger';

/**
* A type-safe form of the DOMString used in the MediaStreamTrack.kind field.
*/
Expand All @@ -27,11 +28,23 @@ type IceGatheringStateChangeEvent = {
enum PeerConnectionEvents {
IceGatheringStateChange = 'icegatheringstatechange',
ConnectionStateChange = 'connectionstatechange',
CreateOfferOnSuccess = 'createofferonsuccess',
CreateAnswerOnSuccess = 'createansweronsuccess',
SetLocalDescriptionOnSuccess = 'setlocaldescriptiononsuccess',
SetRemoteDescriptionOnSuccess = 'setremotedescriptiononsuccess',
}

interface PeerConnectionEventHandlers extends EventMap {
[PeerConnectionEvents.IceGatheringStateChange]: (ev: IceGatheringStateChangeEvent) => void;
[PeerConnectionEvents.ConnectionStateChange]: (state: ConnectionState) => void;
[PeerConnectionEvents.CreateOfferOnSuccess]: (offer: RTCSessionDescriptionInit) => void;
[PeerConnectionEvents.CreateAnswerOnSuccess]: (answer: RTCSessionDescriptionInit) => void;
[PeerConnectionEvents.SetLocalDescriptionOnSuccess]: (
description: RTCSessionDescription | RTCSessionDescriptionInit
) => void;
[PeerConnectionEvents.SetRemoteDescriptionOnSuccess]: (
description: RTCSessionDescription | RTCSessionDescriptionInit
) => void;
}

type ConnectionType = 'UDP' | 'TCP' | 'TURN-TLS' | 'TURN-TCP' | 'TURN-UDP' | 'unknown';
Expand Down Expand Up @@ -172,7 +185,10 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
* other peer.
*/
async createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit> {
return this.pc.createAnswer(options);
return this.pc.createAnswer(options).then((answer) => {
this.emit(PeerConnection.Events.CreateAnswerOnSuccess, answer);
return answer;
});
}

/**
Expand All @@ -186,7 +202,10 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
* That received offer should be delivered through the signaling server to a remote peer.
*/
async createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
return this.pc.createOffer(options);
return this.pc.createOffer(options).then((offer) => {
this.emit(PeerConnection.Events.CreateOfferOnSuccess, offer);
return offer;
});
}

/**
Expand Down Expand Up @@ -215,7 +234,11 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
});
}

return this.pc.setLocalDescription(description);
return this.pc.setLocalDescription(description).then(() => {
if (description) {
this.emit(PeerConnection.Events.SetLocalDescriptionOnSuccess, description);
}
});
}

/**
Expand All @@ -230,7 +253,9 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
async setRemoteDescription(
description: RTCSessionDescription | RTCSessionDescriptionInit
): Promise<void> {
return this.pc.setRemoteDescription(description);
return this.pc.setRemoteDescription(description).then(() => {
this.emit(PeerConnection.Events.SetRemoteDescriptionOnSuccess, description);
});
}

/**
Expand Down

0 comments on commit 92e01d9

Please sign in to comment.