Skip to content
This repository was archived by the owner on Oct 25, 2024. It is now read-only.

Commit 69f10a4

Browse files
authored
Implement perfect negotiation. (#478)
* Add session ID for P2P. * Add doc for perfect negotiation. * Store only one PeerConnection for each remote endpoint. * Implement polite peer and impolite peer for signaling. * Implement perfect negotiation for WebRTC collision. * Code cleanup. * Listen to negotiation needed event. Negotiation needed works good now, the SDK doesn't need to handle negotiation needed flag manually. * Address comments.
1 parent 342984b commit 69f10a4

File tree

5 files changed

+140
-67
lines changed

5 files changed

+140
-67
lines changed

docs/design/perfect_negotiation.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Perfect Negotiation
2+
3+
This document describes how perfect negotiation is implemented in OWT P2P SDK to avoid collision.
4+
5+
**Perfect negotiation**, **polite peer**, **impolite peer** are defined in [Perfect negotiation example of WebRTC 1.0](https://w3c.github.io/webrtc-pc/#perfect-negotiation-example).
6+
7+
## Determining polite peer and impolite peer
8+
9+
OWT client SDKs determines polite peer and impolite peer by comparing client IDs. Sorting these two client IDs alphabetically in increasing order, the first one is polite order, the second one is impolite peer.
10+
11+
Signaling server is required to return a client ID assigned to the client connected after authentication. OWT doesn't define how authentication works and how client IDs are assigned. They depend on application's design. But every client gets a unique client ID after connecting to the signaling server.
12+
13+
## Connections
14+
15+
We expect only one `PeerConnection` between two endpoints. A random ID is generated when creating a new `PeerConnection`. All messages (offers, answers, ICE candidates) for this `PeerConnection` have a `connectionId` property with the connection ID as its value. The connection ID is shared by both clients.
16+
17+
## Collision
18+
19+
When WebRTC collision occurs, it basically follows the perfect negotiation example in WebRTC 1.0. This section only describes the implementation for signaling collision.
20+
21+
A connection typically ends by calling `stop` at local side or receiving a `chat-closed` message from the remote side. Client SDKs stores the most recent connection IDs with all remote endpoints, and clean one of them when a connection ends. If the connection ID of a signaling message received is different from the one stored locally, collision happens.
22+
23+
An example is both sides call `send` to create a data channel and send a message to the remote endpoint at the same time. Each side creates a new `PeerConnection`, and a connection ID. These two connection IDs are different. Then each of them will receive signaling messages with connection ID differ from the local one.
24+
25+
### Polite peer
26+
27+
The polite peer is the controlled side. When a signaling message with a new connection ID is received, it stops current PeerConnection, create a new one, and associate it with the remote connection ID.
28+
29+
### Impolite peer
30+
31+
The polite peer is the controlling side. It ignores remote messages conflicting with its own state, and continues its own process.

src/sdk/p2p/p2pclient.js

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,26 +85,36 @@ const P2PClient = function(configuration, signalingChannel) {
8585
const config = configuration;
8686
const signaling = signalingChannel;
8787
const channels = new Map(); // Map of PeerConnectionChannels.
88-
const self=this;
88+
const connectionIds = new Map(); // Key is remote user ID, value is current session ID.
89+
const self = this;
8990
let state = ConnectionState.READY;
9091
let myId;
9192

9293
signaling.onMessage = function(origin, message) {
9394
Logger.debug('Received signaling message from ' + origin + ': ' + message);
9495
const data = JSON.parse(message);
96+
const connectionId = data.connectionId;
97+
if (self.allowedRemoteIds.indexOf(origin) < 0) {
98+
sendSignalingMessage(
99+
origin, data.connectionId, 'chat-closed',
100+
ErrorModule.errors.P2P_CLIENT_DENIED);
101+
return;
102+
}
103+
if (connectionIds.has(origin) &&
104+
connectionIds.get(origin) !== connectionId && !isPolitePeer(origin)) {
105+
Logger.warning(
106+
// eslint-disable-next-line max-len
107+
'Collision detected, ignore this message because current endpoint is impolite peer.');
108+
return;
109+
}
95110
if (data.type === 'chat-closed') {
96111
if (channels.has(origin)) {
97-
getOrCreateChannel(origin, false).onMessage(data);
112+
getOrCreateChannel(origin, connectionId).onMessage(data);
98113
channels.delete(origin);
99114
}
100115
return;
101116
}
102-
if (self.allowedRemoteIds.indexOf(origin) >= 0) {
103-
getOrCreateChannel(origin, false).onMessage(data);
104-
} else {
105-
sendSignalingMessage(origin, 'chat-closed',
106-
ErrorModule.errors.P2P_CLIENT_DENIED);
107-
}
117+
getOrCreateChannel(origin, connectionId).onMessage(data);
108118
};
109119

110120
signaling.onServerDisconnected = function() {
@@ -158,7 +168,7 @@ const P2PClient = function(configuration, signalingChannel) {
158168
if (state == ConnectionState.READY) {
159169
return;
160170
}
161-
channels.forEach((channel)=>{
171+
channels.forEach((channel) => {
162172
channel.stop();
163173
});
164174
channels.clear();
@@ -184,7 +194,7 @@ const P2PClient = function(configuration, signalingChannel) {
184194
return Promise.reject(new ErrorModule.P2PError(
185195
ErrorModule.errors.P2P_CLIENT_NOT_ALLOWED));
186196
}
187-
return Promise.resolve(getOrCreateChannel(remoteId, true).publish(stream));
197+
return Promise.resolve(getOrCreateChannel(remoteId).publish(stream));
188198
};
189199

190200
/**
@@ -206,7 +216,7 @@ const P2PClient = function(configuration, signalingChannel) {
206216
return Promise.reject(new ErrorModule.P2PError(
207217
ErrorModule.errors.P2P_CLIENT_NOT_ALLOWED));
208218
}
209-
return Promise.resolve(getOrCreateChannel(remoteId, true).send(message));
219+
return Promise.resolve(getOrCreateChannel(remoteId).send(message));
210220
};
211221

212222
/**
@@ -247,9 +257,11 @@ const P2PClient = function(configuration, signalingChannel) {
247257
return channels.get(remoteId).getStats();
248258
};
249259

250-
const sendSignalingMessage = function(remoteId, type, message) {
260+
const sendSignalingMessage = function(
261+
remoteId, connectionId, type, message) {
251262
const msg = {
252-
type: type,
263+
type,
264+
connectionId,
253265
};
254266
if (message) {
255267
msg.data = message;
@@ -261,21 +273,47 @@ const P2PClient = function(configuration, signalingChannel) {
261273
});
262274
};
263275

264-
const getOrCreateChannel = function(remoteId, isInitializer) {
276+
// Return true if current endpoint is an impolite peer, which controls the
277+
// session.
278+
const isPolitePeer = function(remoteId) {
279+
return myId < remoteId;
280+
};
281+
282+
// If a connection with remoteId with a different session ID exists, it will
283+
// be stopped and a new connection will be created.
284+
const getOrCreateChannel = function(remoteId, connectionId) {
285+
// If `connectionId` is not defined, use the latest one or generate a new
286+
// one.
287+
if (!connectionId && connectionIds.has(remoteId)) {
288+
connectionId = connectionIds.get(remoteId);
289+
}
290+
// Delete old channel if connection doesn't match.
291+
if (connectionIds.has(remoteId) &&
292+
connectionIds.get(remoteId) != connectionId) {
293+
self.stop(remoteId);
294+
}
295+
if (!connectionId) {
296+
const connectionIdLimit = 100000;
297+
connectionId = Math.round(Math.random() * connectionIdLimit);
298+
}
299+
connectionIds.set(remoteId, connectionId);
265300
if (!channels.has(remoteId)) {
266301
// Construct an signaling sender/receiver for P2PPeerConnection.
267302
const signalingForChannel = Object.create(EventDispatcher);
268303
signalingForChannel.sendSignalingMessage = sendSignalingMessage;
269-
const pcc = new P2PPeerConnectionChannel(config, myId, remoteId,
270-
signalingForChannel, isInitializer);
304+
const pcc = new P2PPeerConnectionChannel(
305+
config, myId, remoteId, connectionId, signalingForChannel);
271306
pcc.addEventListener('streamadded', (streamEvent)=>{
272307
self.dispatchEvent(streamEvent);
273308
});
274309
pcc.addEventListener('messagereceived', (messageEvent)=>{
275310
self.dispatchEvent(messageEvent);
276311
});
277-
pcc.addEventListener('ended', ()=>{
278-
channels.delete(remoteId);
312+
pcc.addEventListener('ended', () => {
313+
if (channels.has(remoteId)) {
314+
channels.delete(remoteId);
315+
}
316+
connectionIds.delete(remoteId);
279317
});
280318
channels.set(remoteId, pcc);
281319
}

src/sdk/p2p/peerconnection-channel.js

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,22 @@ const sysInfo = Utils.sysInfo();
5454

5555
/**
5656
* @class P2PPeerConnectionChannel
57-
* @desc A P2PPeerConnectionChannel handles all interactions between this endpoint and a remote endpoint.
57+
* @desc A P2PPeerConnectionChannel manages a PeerConnection object, handles all
58+
* interactions between this endpoint (local) and a remote endpoint. Only one
59+
* PeerConnectionChannel is alive for a local - remote endpoint pair at any
60+
* given time.
5861
* @memberOf Owt.P2P
5962
* @private
6063
*/
6164
class P2PPeerConnectionChannel extends EventDispatcher {
6265
// |signaling| is an object has a method |sendSignalingMessage|.
6366
/* eslint-disable-next-line require-jsdoc */
64-
constructor(config, localId, remoteId, signaling, isInitializer) {
67+
constructor(
68+
config, localId, remoteId, connectionId, signaling) {
6569
super();
6670
this._config = config;
67-
this._localId = localId;
6871
this._remoteId = remoteId;
72+
this._connectionId = connectionId;
6973
this._signaling = signaling;
7074
this._pc = null;
7175
this._publishedStreams = new Map(); // Key is streams published, value is its publication.
@@ -91,13 +95,9 @@ class P2PPeerConnectionChannel extends EventDispatcher {
9195
this._dataSeq = 1; // Sequence number for data channel messages.
9296
this._sendDataPromises = new Map(); // Key is data sequence number, value is an object has |resolve| and |reject|.
9397
this._addedTrackIds = []; // Tracks that have been added after receiving remote SDP but before connection is established. Draining these messages when ICE connection state is connected.
94-
this._isCaller = true;
95-
this._infoSent = false;
98+
this._isPolitePeer = localId < remoteId;
9699
this._disposed = false;
97100
this._createPeerConnection();
98-
if (isInitializer) {
99-
this._sendSignalingMessage(SignalingType.CLOSED);
100-
}
101101
this._sendSignalingMessage(SignalingType.UA, sysInfo);
102102
}
103103

@@ -126,7 +126,6 @@ class P2PPeerConnectionChannel extends EventDispatcher {
126126
for (const track of stream.mediaStream.getTracks()) {
127127
this._pc.addTrack(track, stream.mediaStream);
128128
}
129-
this._onNegotiationneeded();
130129
this._publishingStreams.push(stream);
131130
const trackIds = Array.from(stream.mediaStream.getTracks(),
132131
(track) => track.id);
@@ -227,11 +226,12 @@ class P2PPeerConnectionChannel extends EventDispatcher {
227226

228227
_sendSdp(sdp) {
229228
return this._signaling.sendSignalingMessage(
230-
this._remoteId, SignalingType.SDP, sdp);
229+
this._remoteId, this._connectionId, SignalingType.SDP, sdp);
231230
}
232231

233232
_sendSignalingMessage(type, message) {
234-
return this._signaling.sendSignalingMessage(this._remoteId, type, message);
233+
return this._signaling.sendSignalingMessage(
234+
this._remoteId, this._connectionId, type, message);
235235
}
236236

237237
_SignalingMesssageHandler(message) {
@@ -416,6 +416,15 @@ class P2PPeerConnectionChannel extends EventDispatcher {
416416
_onOffer(sdp) {
417417
Logger.debug('About to set remote description. Signaling state: ' +
418418
this._pc.signalingState);
419+
if (this._pc.signalingState !== 'stable') {
420+
if (this._isPolitePeer) {
421+
Logger.debug('Rollback.');
422+
this._pc.setLocalDescription();
423+
} else {
424+
Logger.debug('Collision detected. Ignore this offer.');
425+
return;
426+
}
427+
}
419428
sdp.sdp = this._setRtpSenderOptions(sdp.sdp, this._config);
420429
// Firefox only has one codec in answer, which does not truly reflect its
421430
// decoding capability. So we set codec preference to remote offer, and let
@@ -539,12 +548,6 @@ class P2PPeerConnectionChannel extends EventDispatcher {
539548
}
540549

541550
_onNegotiationneeded() {
542-
// This is intented to be executed when onnegotiationneeded event is fired.
543-
// However, onnegotiationneeded may fire mutiple times when more than one
544-
// track is added/removed. So we manually execute this function after
545-
// adding/removing track and creating data channel.
546-
Logger.debug('On negotiation needed.');
547-
548551
if (this._pc.signalingState === 'stable') {
549552
this._doNegotiate();
550553
} else {
@@ -689,43 +692,23 @@ class P2PPeerConnectionChannel extends EventDispatcher {
689692
this._pc.oniceconnectionstatechange = (event) => {
690693
this._onIceConnectionStateChange.apply(this, [event]);
691694
};
692-
/*
693-
this._pc.oniceChannelStatechange = function(event) {
694-
_onIceChannelStateChange(peer, event);
695-
};
696-
= function() {
697-
onNegotiationneeded(peers[peer.id]);
695+
this._pc.onnegotiationneeded = () => {
696+
this._onNegotiationneeded();
698697
};
699-
700-
//DataChannel
701-
this._pc.ondatachannel = function(event) {
702-
Logger.debug(myId + ': On data channel');
703-
// Save remote created data channel.
704-
if (!peer.dataChannels[event.channel.label]) {
705-
peer.dataChannels[event.channel.label] = event.channel;
706-
Logger.debug('Save remote created data channel.');
707-
}
708-
bindEventsToDataChannel(event.channel, peer);
709-
};*/
710698
}
711699

712700
_drainPendingStreams() {
713-
let negotiationNeeded = false;
714701
Logger.debug('Draining pending streams.');
715702
if (this._pc && this._pc.signalingState === 'stable') {
716703
Logger.debug('Peer connection is ready for draining pending streams.');
717704
for (let i = 0; i < this._pendingStreams.length; i++) {
718705
const stream = this._pendingStreams[i];
719-
// OnNegotiationNeeded event will be triggered immediately after adding stream to PeerConnection in Firefox.
720-
// And OnNegotiationNeeded handler will execute drainPendingStreams. To avoid add the same stream multiple times,
721-
// shift it from pending stream list before adding it to PeerConnection.
722706
this._pendingStreams.shift();
723707
if (!stream.mediaStream) {
724708
continue;
725709
}
726710
for (const track of stream.mediaStream.getTracks()) {
727711
this._pc.addTrack(track, stream.mediaStream);
728-
negotiationNeeded = true;
729712
}
730713
Logger.debug('Added stream to peer connection.');
731714
this._publishingStreams.push(stream);
@@ -736,17 +719,13 @@ class P2PPeerConnectionChannel extends EventDispatcher {
736719
continue;
737720
}
738721
this._pc.removeStream(this._pendingUnpublishStreams[j].mediaStream);
739-
negotiationNeeded = true;
740722
this._unpublishPromises.get(
741723
this._pendingUnpublishStreams[j].mediaStream.id).resolve();
742724
this._publishedStreams.delete(this._pendingUnpublishStreams[j]);
743725
Logger.debug('Remove stream.');
744726
}
745727
this._pendingUnpublishStreams.length = 0;
746728
}
747-
if (negotiationNeeded) {
748-
this._onNegotiationneeded();
749-
}
750729
}
751730

752731
_drainPendingRemoteIceCandidates() {
@@ -867,7 +846,6 @@ class P2PPeerConnectionChannel extends EventDispatcher {
867846
return;
868847
}
869848
this._isNegotiationNeeded = false;
870-
this._isCaller = true;
871849
let localDesc;
872850
this._pc.createOffer().then((desc) => {
873851
desc.sdp = this._setRtpReceiverOptions(desc.sdp);
@@ -878,7 +856,7 @@ class P2PPeerConnectionChannel extends EventDispatcher {
878856
});
879857
}
880858
}).catch((e) => {
881-
Logger.error(e.message + ' Please check your codec settings.');
859+
Logger.error(e.message);
882860
const error = new ErrorModule.P2PError(ErrorModule.errors.P2P_WEBRTC_SDP,
883861
e.message);
884862
this._stop(error, true);
@@ -888,7 +866,6 @@ class P2PPeerConnectionChannel extends EventDispatcher {
888866
_createAndSendAnswer() {
889867
this._drainPendingStreams();
890868
this._isNegotiationNeeded = false;
891-
this._isCaller = false;
892869
let localDesc;
893870
this._pc.createAnswer().then((desc) => {
894871
desc.sdp = this._setRtpReceiverOptions(desc.sdp);
@@ -963,7 +940,6 @@ class P2PPeerConnectionChannel extends EventDispatcher {
963940
const dc = this._pc.createDataChannel(label);
964941
this._bindEventsToDataChannel(dc);
965942
this._dataChannels.set(DataChannelLabel.MESSAGE, dc);
966-
this._onNegotiationneeded();
967943
}
968944

969945
_bindEventsToDataChannel(dc) {

test/unit/resources/scripts/fake-p2p-signaling.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export default class FakeP2PSignalingChannel {
3939
}
4040

4141
send(targetId, message) {
42-
Logger.debug(this.userId + ' -> ' + targetId + ': ' + message);
4342
return new Promise((resolve, reject) => {
4443
messageQueue.push({ target: targetId, message, resolve, reject, sender: this.userId });
4544
setTimeout(() => {

0 commit comments

Comments
 (0)