Skip to content

Commit 39f185a

Browse files
committed
Fix Pixel Streaming intermittently fails to make a network connection even with correct ICE candidates (#694)
* Fix connectivity issue where sdpMid and sdpMLineIdx were causing connections to fail, these can be safely dropped as we use bundle by default and therefore these attributes are not used. (cherry picked from commit cbec432)
1 parent 4e15e85 commit 39f185a

File tree

4 files changed

+69
-16
lines changed

4 files changed

+69
-16
lines changed

.changeset/rich-sites-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@epicgames-ps/lib-pixelstreamingfrontend-ue5.6': minor
3+
---
4+
5+
This change fixes an intermittent WebRTC connection failure where even when the appropriate ICE candidates were present the conection would sometimes fail to be made. This was caused due to the order that ICE candidates were being sent (hence the intermittent nature of the issues) and the fact that ICE candidates sent from Pixel Streaming plugin contain sdpMid and sdpMLineIndex. sdpMid and sdpMLineIndex are only necessary in legacy, non bundle, WebRTC streams; however, Pixel Streaming always assumes bundle is used and these attributes can safely be set to empty strings/omitted (respectively). We perform this modification in the frontend library prior to adding the ICE candidate to the peer connection. This change was tested on a wide range of target devices and browsers to ensure there was no adverse side effects prior.

Frontend/library/src/PeerConnectionController/PeerConnectionController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export class PeerConnectionController {
144144
return this.peerConnection?.setLocalDescription(Answer);
145145
})
146146
.then(() => {
147-
this.onSetLocalDescription(this.peerConnection?.currentLocalDescription);
147+
this.onSetLocalDescription(this.peerConnection?.localDescription);
148148
})
149149
.catch((err) => {
150150
Logger.Error(`createAnswer() failed - ${err}`);

Frontend/library/src/PixelStreaming/PixelStreaming.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,14 @@ describe('PixelStreaming', () => {
322322
triggerSdpOfferMessage();
323323
triggerIceCandidateMessage();
324324

325-
expect(rtcPeerConnectionSpyFunctions.addIceCandidateSpy).toHaveBeenCalledWith(iceCandidate)
325+
// Expect ice candidate to be stripped even if passed in with sdpMid and sdpMLineIndex
326+
// as these values are not required when using bundle (which we assume)
327+
const strippedIceCandidate = new RTCIceCandidate({
328+
candidate: iceCandidate.candidate,
329+
sdpMid: ""
330+
});
331+
332+
expect(rtcPeerConnectionSpyFunctions.addIceCandidateSpy).toHaveBeenCalledWith(strippedIceCandidate)
326333
});
327334

328335
it('should emit webRtcConnected event when ICE connection state is connected', () => {

Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,19 +1106,35 @@ export class WebRtcPlayerController {
11061106
this.pixelStreaming._onLatencyCalculated(latencyInfo);
11071107
};
11081108

1109-
/* When the Peer Connection wants to send an offer have it handled */
1109+
/* When our PeerConnection wants to send an offer call our handler */
11101110
this.peerConnectionController.onSendWebRTCOffer = (offer: RTCSessionDescriptionInit) => {
11111111
this.handleSendWebRTCOffer(offer);
11121112
};
11131113

1114-
/* Set event handler for when local answer description is set */
1115-
this.peerConnectionController.onSetLocalDescription = (answer: RTCSessionDescriptionInit) => {
1116-
this.handleSendWebRTCAnswer(answer);
1114+
/* Set event handler for when local description is set */
1115+
this.peerConnectionController.onSetLocalDescription = (sdp: RTCSessionDescriptionInit) => {
1116+
if (sdp.type === 'offer') {
1117+
this.handleSendWebRTCOffer(sdp);
1118+
} else if (sdp.type === 'answer') {
1119+
this.handleSendWebRTCAnswer(sdp);
1120+
} else {
1121+
Logger.Error(
1122+
`PeerConnectionController onSetLocalDescription was called with unexpected type ${sdp.type}`
1123+
);
1124+
}
11171125
};
11181126

1119-
/* Set event handler for when remote offer description is set */
1120-
this.peerConnectionController.onSetRemoteDescription = (offer: RTCSessionDescriptionInit) => {
1121-
this.pixelStreaming._onWebRtcSdpOffer(offer);
1127+
/* Event handler for when PeerConnection's remote description is set */
1128+
this.peerConnectionController.onSetRemoteDescription = (sdp: RTCSessionDescriptionInit) => {
1129+
if (sdp.type === 'offer') {
1130+
this.pixelStreaming._onWebRtcSdpOffer(sdp);
1131+
} else if (sdp.type === 'answer') {
1132+
this.pixelStreaming._onWebRtcSdpAnswer(sdp);
1133+
} else {
1134+
Logger.Error(
1135+
`PeerConnectionController onSetRemoteDescription was called with unexpected type ${sdp.type}`
1136+
);
1137+
}
11221138
};
11231139

11241140
/* When the Peer Connection ice candidate is added have it handled */
@@ -1463,23 +1479,31 @@ export class WebRtcPlayerController {
14631479
}
14641480

14651481
/**
1466-
* When an ice Candidate is received from the Signaling server add it to the Peer Connection Client
1467-
* @param iceCandidate - Ice Candidate from Server
1482+
* Handler for when a remote ICE candidate is received.
1483+
* @param iceCandidateInit - Initialization data used to make the actual ICE Candidate.
14681484
*/
1469-
handleIceCandidate(iceCandidate: RTCIceCandidateInit) {
1470-
Logger.Info('Web RTC Controller: onWebRtcIce');
1485+
handleIceCandidate(iceCandidateInit: RTCIceCandidateInit) {
1486+
Logger.Info(`Remote ICE candidate information received: ${JSON.stringify(iceCandidateInit)}`);
1487+
1488+
// We are using "bundle" policy for media lines so we remove the sdpMid and sdpMLineIndex attributes
1489+
// from ICE candidates as these are legacy attributes for when bundle is not used.
1490+
// If we don't do this the browser may be unable to form a media connection
1491+
// because some browsers are brittle if the bundle master (e.g. commonly mid=0) doesn't get a candidate first.
1492+
const remoteIceCandidate = new RTCIceCandidate({
1493+
candidate: iceCandidateInit.candidate,
1494+
sdpMid: ''
1495+
});
14711496

1472-
const candidate = new RTCIceCandidate(iceCandidate);
1473-
this.peerConnectionController.handleOnIce(candidate);
1497+
this.peerConnectionController.handleOnIce(remoteIceCandidate);
14741498
}
14751499

14761500
/**
14771501
* Send the ice Candidate to the signaling server via websocket
14781502
* @param iceEvent - RTC Peer ConnectionIceEvent) {
14791503
*/
14801504
handleSendIceCandidate(iceEvent: RTCPeerConnectionIceEvent) {
1481-
Logger.Info('OnIceCandidate');
14821505
if (iceEvent.candidate && iceEvent.candidate.candidate) {
1506+
Logger.Info(`Local ICE candidate generated: ` + JSON.stringify(iceEvent.candidate));
14831507
this.protocol.sendMessage(
14841508
MessageHelpers.createMessage(Messages.iceCandidate, { candidate: iceEvent.candidate })
14851509
);
@@ -1504,6 +1528,13 @@ export class WebRtcPlayerController {
15041528
* @param offer - RTC Session Description
15051529
*/
15061530
handleSendWebRTCOffer(offer: RTCSessionDescriptionInit) {
1531+
if (offer.type !== 'offer') {
1532+
Logger.Error(
1533+
`handleSendWebRTCOffer was called with type ${offer.type} - it only expects "offer"`
1534+
);
1535+
return;
1536+
}
1537+
15071538
Logger.Info('Sending the offer to the Server');
15081539

15091540
const extraParams = {
@@ -1513,13 +1544,23 @@ export class WebRtcPlayerController {
15131544
};
15141545

15151546
this.protocol.sendMessage(MessageHelpers.createMessage(Messages.offer, extraParams));
1547+
1548+
// Send offer back to Pixel Streaming main class for event dispatch
1549+
this.pixelStreaming._onWebRtcSdpOffer(offer);
15161550
}
15171551

15181552
/**
15191553
* Send the RTC Offer Session to the Signaling server via websocket
15201554
* @param answer - RTC Session Description
15211555
*/
15221556
handleSendWebRTCAnswer(answer: RTCSessionDescriptionInit) {
1557+
if (answer.type !== 'answer') {
1558+
Logger.Error(
1559+
`handleSendWebRTCAnswer was called with type ${answer.type} - it only expects "answer"`
1560+
);
1561+
return;
1562+
}
1563+
15231564
Logger.Info('Sending the answer to the Server');
15241565

15251566
const extraParams = {

0 commit comments

Comments
 (0)