Skip to content

Commit 6e6acf2

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 621f65c commit 6e6acf2

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
@@ -1103,19 +1103,35 @@ export class WebRtcPlayerController {
11031103
this.pixelStreaming._onLatencyCalculated(latencyInfo);
11041104
};
11051105

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

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

1116-
/* Set event handler for when remote offer description is set */
1117-
this.peerConnectionController.onSetRemoteDescription = (offer: RTCSessionDescriptionInit) => {
1118-
this.pixelStreaming._onWebRtcSdpOffer(offer);
1124+
/* Event handler for when PeerConnection's remote description is set */
1125+
this.peerConnectionController.onSetRemoteDescription = (sdp: RTCSessionDescriptionInit) => {
1126+
if (sdp.type === 'offer') {
1127+
this.pixelStreaming._onWebRtcSdpOffer(sdp);
1128+
} else if (sdp.type === 'answer') {
1129+
this.pixelStreaming._onWebRtcSdpAnswer(sdp);
1130+
} else {
1131+
Logger.Error(
1132+
`PeerConnectionController onSetRemoteDescription was called with unexpected type ${sdp.type}`
1133+
);
1134+
}
11191135
};
11201136

11211137
/* When the Peer Connection ice candidate is added have it handled */
@@ -1447,23 +1463,31 @@ export class WebRtcPlayerController {
14471463
}
14481464

14491465
/**
1450-
* When an ice Candidate is received from the Signaling server add it to the Peer Connection Client
1451-
* @param iceCandidate - Ice Candidate from Server
1466+
* Handler for when a remote ICE candidate is received.
1467+
* @param iceCandidateInit - Initialization data used to make the actual ICE Candidate.
14521468
*/
1453-
handleIceCandidate(iceCandidate: RTCIceCandidateInit) {
1454-
Logger.Info('Web RTC Controller: onWebRtcIce');
1469+
handleIceCandidate(iceCandidateInit: RTCIceCandidateInit) {
1470+
Logger.Info(`Remote ICE candidate information received: ${JSON.stringify(iceCandidateInit)}`);
1471+
1472+
// We are using "bundle" policy for media lines so we remove the sdpMid and sdpMLineIndex attributes
1473+
// from ICE candidates as these are legacy attributes for when bundle is not used.
1474+
// If we don't do this the browser may be unable to form a media connection
1475+
// because some browsers are brittle if the bundle master (e.g. commonly mid=0) doesn't get a candidate first.
1476+
const remoteIceCandidate = new RTCIceCandidate({
1477+
candidate: iceCandidateInit.candidate,
1478+
sdpMid: ''
1479+
});
14551480

1456-
const candidate = new RTCIceCandidate(iceCandidate);
1457-
this.peerConnectionController.handleOnIce(candidate);
1481+
this.peerConnectionController.handleOnIce(remoteIceCandidate);
14581482
}
14591483

14601484
/**
14611485
* Send the ice Candidate to the signaling server via websocket
14621486
* @param iceEvent - RTC Peer ConnectionIceEvent) {
14631487
*/
14641488
handleSendIceCandidate(iceEvent: RTCPeerConnectionIceEvent) {
1465-
Logger.Info('OnIceCandidate');
14661489
if (iceEvent.candidate && iceEvent.candidate.candidate) {
1490+
Logger.Info(`Local ICE candidate generated: ` + JSON.stringify(iceEvent.candidate));
14671491
this.protocol.sendMessage(
14681492
MessageHelpers.createMessage(Messages.iceCandidate, { candidate: iceEvent.candidate })
14691493
);
@@ -1488,6 +1512,13 @@ export class WebRtcPlayerController {
14881512
* @param offer - RTC Session Description
14891513
*/
14901514
handleSendWebRTCOffer(offer: RTCSessionDescriptionInit) {
1515+
if (offer.type !== 'offer') {
1516+
Logger.Error(
1517+
`handleSendWebRTCOffer was called with type ${offer.type} - it only expects "offer"`
1518+
);
1519+
return;
1520+
}
1521+
14911522
Logger.Info('Sending the offer to the Server');
14921523

14931524
const extraParams = {
@@ -1497,13 +1528,23 @@ export class WebRtcPlayerController {
14971528
};
14981529

14991530
this.protocol.sendMessage(MessageHelpers.createMessage(Messages.offer, extraParams));
1531+
1532+
// Send offer back to Pixel Streaming main class for event dispatch
1533+
this.pixelStreaming._onWebRtcSdpOffer(offer);
15001534
}
15011535

15021536
/**
15031537
* Send the RTC Offer Session to the Signaling server via websocket
15041538
* @param answer - RTC Session Description
15051539
*/
15061540
handleSendWebRTCAnswer(answer: RTCSessionDescriptionInit) {
1541+
if (answer.type !== 'answer') {
1542+
Logger.Error(
1543+
`handleSendWebRTCAnswer was called with type ${answer.type} - it only expects "answer"`
1544+
);
1545+
return;
1546+
}
1547+
15071548
Logger.Info('Sending the answer to the Server');
15081549

15091550
const extraParams = {

0 commit comments

Comments
 (0)