From 9e5259d1abbc50050237b10f3695cd3b79f56f90 Mon Sep 17 00:00:00 2001 From: shinyoshiaki Date: Fri, 20 Sep 2024 19:26:40 +0900 Subject: [PATCH] breaking change: support null candidates --- packages/webrtc/src/peerConnection.ts | 40 ++++++++++++------- packages/webrtc/src/transport/ice.ts | 9 +++-- .../webrtc/tests/integrate/trickle.test.ts | 14 ++++++- packages/webrtc/tests/utils.ts | 3 ++ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/webrtc/src/peerConnection.ts b/packages/webrtc/src/peerConnection.ts index c65f4593..0876c62f 100644 --- a/packages/webrtc/src/peerConnection.ts +++ b/packages/webrtc/src/peerConnection.ts @@ -105,7 +105,7 @@ export class RTCPeerConnection extends EventTarget { readonly onDataChannel = new Event<[RTCDataChannel]>(); readonly onRemoteTransceiverAdded = new Event<[RTCRtpTransceiver]>(); readonly onTransceiverAdded = new Event<[RTCRtpTransceiver]>(); - readonly onIceCandidate = new Event<[RTCIceCandidate]>(); + readonly onIceCandidate = new Event<[RTCIceCandidate | undefined]>(); readonly onNegotiationneeded = new Event<[]>(); readonly onTrack = new Event<[MediaStreamTrack]>(); @@ -227,11 +227,13 @@ export class RTCPeerConnection extends EventTarget { return this._remoteDescription.toJSON(); } - private get _localDescription() { + /**@private */ + get _localDescription() { return this.pendingLocalDescription || this.currentLocalDescription; } - private get _remoteDescription() { + /**@private */ + get _remoteDescription() { return this.pendingRemoteDescription || this.currentRemoteDescription; } @@ -512,6 +514,16 @@ export class RTCPeerConnection extends EventTarget { log("localDescription not found when ice candidate was gathered"); return; } + if (!candidate) { + this.setLocal(this._localDescription!); + this.onIceCandidate.execute(undefined); + if (this.onicecandidate) { + this.onicecandidate({ candidate: undefined }); + } + this.emit("icecandidate", { candidate: undefined }); + return; + } + if (this.config.bundlePolicy === "max-bundle" || this.remoteIsBundled) { candidate.sdpMLineIndex = 0; const media = this._localDescription?.media[0]; @@ -678,16 +690,6 @@ export class RTCPeerConnection extends EventTarget { }); } - description.media - .filter((m) => ["audio", "video"].includes(m.kind)) - .forEach((m, i) => { - addTransportDescription(m, this.transceivers[i].dtlsTransport); - }); - const sctpMedia = description.media.find((m) => m.kind === "application"); - if (this.sctpTransport && sctpMedia) { - addTransportDescription(sctpMedia, this.sctpTransport.dtlsTransport); - } - this.setLocal(description); if (this.shouldNegotiationneeded) { @@ -698,6 +700,16 @@ export class RTCPeerConnection extends EventTarget { } private setLocal(description: SessionDescription) { + description.media + .filter((m) => ["audio", "video"].includes(m.kind)) + .forEach((m, i) => { + addTransportDescription(m, this.transceivers[i].dtlsTransport); + }); + const sctpMedia = description.media.find((m) => m.kind === "application"); + if (this.sctpTransport && sctpMedia) { + addTransportDescription(sctpMedia, this.sctpTransport.dtlsTransport); + } + if (description.type === "answer") { this.currentLocalDescription = description; this.pendingLocalDescription = undefined; @@ -1743,7 +1755,7 @@ export interface RTCDataChannelEvent { } export interface RTCPeerConnectionIceEvent { - candidate: RTCIceCandidate; + candidate?: RTCIceCandidate; } type Media = "audio" | "video"; diff --git a/packages/webrtc/src/transport/ice.ts b/packages/webrtc/src/transport/ice.ts index e13e1383..9036905b 100644 --- a/packages/webrtc/src/transport/ice.ts +++ b/packages/webrtc/src/transport/ice.ts @@ -113,7 +113,7 @@ export const IceGathererStates = ["new", "gathering", "complete"] as const; export type IceGathererState = (typeof IceGathererStates)[number]; export class RTCIceGatherer { - onIceCandidate: (candidate: IceCandidate) => void = () => {}; + onIceCandidate: (candidate: IceCandidate | undefined) => void = () => {}; gatheringState: IceGathererState = "new"; readonly onGatheringStateChange = new Event<[IceGathererState]>(); @@ -126,9 +126,10 @@ export class RTCIceGatherer { async gather() { if (this.gatheringState === "new") { this.setState("gathering"); - await this.connection.gatherCandidates((candidate) => - this.onIceCandidate(candidateFromIce(candidate)), - ); + await this.connection.gatherCandidates((candidate) => { + this.onIceCandidate(candidateFromIce(candidate)); + }); + this.onIceCandidate(undefined); this.setState("complete"); } } diff --git a/packages/webrtc/tests/integrate/trickle.test.ts b/packages/webrtc/tests/integrate/trickle.test.ts index 09d612d1..53bef59c 100644 --- a/packages/webrtc/tests/integrate/trickle.test.ts +++ b/packages/webrtc/tests/integrate/trickle.test.ts @@ -19,6 +19,10 @@ describe("trickle", () => { }); pcOffer.onIceCandidate.subscribe((candidate) => { + if (!candidate) { + expect(pcOffer._localDescription?.media.length).toBe(1); + return; + } pcAnswer.addIceCandidate(candidate); }); @@ -30,7 +34,9 @@ describe("trickle", () => { }); const offer = await pcOffer.createOffer(); - pcOffer.setLocalDescription(offer); + pcOffer.setLocalDescription(offer).then(() => { + expect(pcOffer._localDescription?.media.length).toBe(1); + }); await pcAnswer.setRemoteDescription(offer); await pcAnswer.setLocalDescription(await pcAnswer.createAnswer()); @@ -58,9 +64,15 @@ describe("trickle", () => { }); pcOffer.onIceCandidate.subscribe((candidate) => { + if (!candidate) { + return; + } pcAnswer.addIceCandidate(candidate); }); pcAnswer.onIceCandidate.subscribe((candidate) => { + if (!candidate) { + return; + } pcOffer.addIceCandidate(candidate); }); diff --git a/packages/webrtc/tests/utils.ts b/packages/webrtc/tests/utils.ts index 00661f24..4862d433 100644 --- a/packages/webrtc/tests/utils.ts +++ b/packages/webrtc/tests/utils.ts @@ -70,6 +70,9 @@ function exchangeIceCandidates(pc1: RTCPeerConnection, pc2: RTCPeerConnection) { // private function function doExchange(localPc: RTCPeerConnection, remotePc: RTCPeerConnection) { localPc.onIceCandidate.subscribe((candidate) => { + if (!candidate) { + return; + } if (remotePc.signalingState !== "closed") { remotePc.addIceCandidate(candidate); }