-
-
Notifications
You must be signed in to change notification settings - Fork 978
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
changing tracks, multistreaming and renegotiation #250
Conversation
index.js
Outdated
@@ -98,19 +105,14 @@ function Peer (opts) { | |||
self._pc.onicecandidate = function (event) { | |||
self._onIceCandidate(event) | |||
} | |||
self._pc.onnegotiationneeded = null // wrtc doesn't fire this event, Chromium fires it too often, and Firefox doesn't fire it enough |
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.
If onnegotiationneeded
is not used, why still assigning null
in 2 places?
index.js
Outdated
self._pc.addTrack(track, self.stream) | ||
if (self.streams) { | ||
self.streams.forEach(function (stream) { | ||
self._negotationsToIgnore += stream.getTracks().length |
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.
_negotationsToIgnore
doesn't seem to be used (yet/anymore?)
} | ||
} else { | ||
self._debug('requesting negotiation from initiator') | ||
self.emit('signal', JSON.stringify({ // request initiator to renegotiate |
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.
Can't we open a dedicated RTCDataChannel for internal purposes instead of explicitly requiring user to send this data?
Can be related to #19, though negotiated
only seems to be supported in Chrome for now: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/negotiated
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.
Sending signaling data over a datachannel would be pretty cool... I think we should look at this when we decide on an API for multiplexed datachannels.
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.
I think this is actually ideal, if we do add this via a datachannel it should be a separate api.
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.
@feross - This new signaling event will cause an error on older versions of simple-peer (which destroys them.) It's required since only the initiator can safely start renegotiation.
index.js
Outdated
self.removeSender(sender) | ||
}) | ||
} | ||
Peer.prototype.removeStream = Peer.prototype.removeSenders // for symmetry in the API |
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.
I feel like this method should be a separate method that accepts MediaStream
instance and simple-peer
can use WeakMap
to keep internal mapping from MediaStream
instance to RTCRtpSender[]
.
This would hide more of internal implementation from the user and will potentially make things easier to work with.
If we do that, we can avoid returning RTCRtpSender[]
from addSteam()
. This way user either add stream and remove the same stream or add individual tracks and removes individual tracks (using WeakMap
again and without returning RTCRtpSender
). This would be perfectly symmetric implementation from user's perspective.
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.
Great idea, I didn't like exposing RTCRtpSenders either.
index.js
Outdated
* @param {RTCRtpSender} sender | ||
* @returns {RTCRtpSender} | ||
*/ | ||
Peer.prototype.removeSender = function (sender) { |
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.
Why not call it remoteTrack
and remove removeSender
? Seems like remaining code from earlier prototype.
index.js
Outdated
* @param {MediaStream} stream | ||
* @returns {RTCRtpSender[]} | ||
*/ | ||
Peer.prototype.addStream = function (stream) { |
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.
Duplicated function, already present above.
index.js
Outdated
/** | ||
* Renegotiate the peer connection. | ||
*/ | ||
Peer.prototype.renegotiate = function () { |
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.
What are the use-cases when this needs to be exposed and can't be handled by the library automatically?
I mean, when multiple streams/tracks need to be added, setTimeout(callback, 0)
can be used to batch them into single _negotiate()
call.
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.
I exposed it to allow batching of asynchronous changes to the connection. Typically MediaStreams come from several async sources in parallel (ie one from getUserMedia, one from screenCapture, a synchronous WebAudio input).
The API benefits of abstracting negotiation is something to consider though.
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.
If user expects all of these to be added with one negotiation, it is still possible to do Promise.all()
and then add necessary streams/tracks synchronously so that only one renegotiation is made.
@@ -573,7 +680,7 @@ Peer.prototype._maybeReady = function () { | |||
|
|||
items.forEach(function (item) { | |||
// Spec-compliant | |||
if (item.type === 'transport') { | |||
if (item.type === 'transport' && item.selectedCandidatePairId) { |
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.
selectedCandidatePairId
seems to be required by specification and Chrome supports it. Is there a browser which doesn't, maybe add a comment?
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.
I was getting rare candidates that did not have that property (Chrome). I'll remove this and open a separate bug to look into it.
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.
I've noticed this error only happens in the new tests, so we might want to figure it out before merging:
Here's a sample candidate that causes the failure (they are somewhat rare):
{
bytesReceived:0,
bytesSent:0,
dtlsState:"new",
id:"RTCTransport_audio_1",
localCertificateId:"RTCCertificate_F7:92:F4:5E:99:2C:F6:66:92:6C:DA:21:D7:30:AB:D7:61:E5:E2:A2:82:AF:58:2D:1D:1D:BD:99:D4:7E:D8:92",
timestamp:1520117393473.471,
type:"transport"
}
That's the whole object. Note the absence of selectedCandidatePairId
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.
Interesting, basically another aspect of #215. Then workaround makes sense.
I'm just wondering why Chrome even adds type=transport
without selectedCandidatePairId
property, since it clearly has this property on later stage and not much useful without it.
Might be worth to report as Chrome bug and add a comment with link to bug report (haven't found existing bug report for this).
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 only happens when dtlsState
is "new", which might suggest the selectedCandidatePairId isn't known at that point?
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.
I've just figured that out, dictionary only contains about half of the properties from the spec. Looks like it is necessary to check dtlsState
property.
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.
index.js
Outdated
|
||
if (self._pc.signalingState === 'stable') { | ||
self._isNegotiating = false | ||
self._initialNegotiation = false |
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.
_initialNegotiation
is set, but never used. Any plans for this property?
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 was needed for automatic renegotiation, which was removed.
index.js
Outdated
if (self._remoteStreams.some(function (stream) { | ||
return stream.id === event.streams[0].id // Only fire one 'stream' event, even though there may be multiple tracks per stream | ||
})) return | ||
self._remoteStreams.push(event.streams[0]) |
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.
Spec says one track might be a part of more than 1 stream. Why do we only care about one of them?
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.
So you're saying event.streams[1] could exist?
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.
The comment there is referring to only firing one stream event for multiple track events that reference the same stream. We did that before, I've just refactored it.
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.
The spec says there might be event.streams[1]
, otherwise it wouldn't be called that way. I'm just curious what would happen if we have multiple tracks, some of which are used in multiple streams. I don't think it will happen with high probability, but it is still a valid edge case.
index.js
Outdated
self._debug('addTrack()') | ||
|
||
var sender = self._pc.addTrack(track, stream) | ||
self._senderMap.set(track, sender) |
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.
_senderMap
should be a WeakMap
of WeakMap
s and track should be added/removed to/from specific stream, since the same track theoretically might be also used for another stream. Otherwise it will override previous sender
with new sender
, which corresponds to different stream and previous one will be lost.
index.js
Outdated
* Remove a MediaStreamTrack from the connection. | ||
* @param {MediaStreamTrack} sender | ||
*/ | ||
Peer.prototype.removeTrack = function (track) { |
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.
And stream
argument should be added here accordingly.
index.js
Outdated
if (self._previousStreams.indexOf(id) !== -1) return // Only fire one 'stream' event, even though there may be multiple tracks per stream | ||
self._previousStreams.push(id) | ||
self.emit('stream', event.streams[0]) | ||
self.emit('track', event.track) |
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.
I'm not sure if ontrack
is can be called multiple times (haven't found this explicitly in specification yet), but intuition indicates that it can. Hence track
event and potentially removetrack
might need to expose streams
argument on them to clearly understand from which streams (if not all) track was removed.
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.
Looks good to me 👍
Thanks for the great review @nazar-pc. I'd like to do some more manual testing with real MediaStreams then we can finally get this merged. 🎉 |
After manual testing, everything seems good. Some limitations to note:
|
Merged. This is a breaking change for interop between peers, although the API changes are not breaking. |
@rationalcoding Can you explain how this breaks interop between peers? EDIT: This PR does not break interop: #286 (comment) |
changing tracks, multistreaming and renegotiation
The WebRTC API around tracks and multistreaming is finally stable enough to implement this.
Stream methods are convenience wrappers for the track methods. Native addStream, onAddStream usage has been removed since all major browsers support tracks now.
Exposed API:
addStream(MediaStream)
addTrack(MediaStreamTrack)
removeStream(RTCRtpSender[])
removeTrack(RTCRtpSender)
peer.on('stream')
peer.on('track')
peer.on('removestream')
peer.on('removetrack')
peer.on('negotiate')
Also added the
streams
option for passing an array of MediaStreams into the constructor.Some issues that had to be worked around:
onnegotiationneeded
does not fire more than once in Safari, fires multiple times in Chrome and doesn't fire on removeTrack for Firefox. Removing automatic detection is actually nicer since it allows the user to make multiple asynchronous changes to the connection with a single round of negotiation. Not using the event doesn't break anything.Thanks to @cusspvz for the initial work on this.