Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #195 from Belchy06/master
Browse files Browse the repository at this point in the history
Alter reconnection flow
  • Loading branch information
lukehb authored Apr 12, 2023
2 parents d465f2e + 034eb4e commit c280c06
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Frontend/Docs/Settings Panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This page will be updated with new features and commands as they become availabl
| **Suppress browser keys** | Suppress or allow certain keys we use in UE, for example F5 to show shader complexity instead of refreshing the page. |
| **AFK if Idle** | Timeout the connection if no input is detected for a period of time. |
| **AFK timeout** | Allows you to specify the AFK timeout period. |

| **Max Reconnects** | The maximum number of reconnects the application will attempt when a streamer disconnects. |

### UI
| **Setting** | **Description** |
Expand Down
18 changes: 16 additions & 2 deletions Frontend/library/src/Config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class NumericParameters {
static WebRTCFPS = 'WebRTCFPS' as const;
static WebRTCMinBitrate = 'WebRTCMinBitrate' as const;
static WebRTCMaxBitrate = 'WebRTCMaxBitrate' as const;
static MaxReconnectAttempts = 'MaxReconnectAttempts' as const;
}

export type NumericParametersKeys = Exclude<
Expand Down Expand Up @@ -240,7 +241,7 @@ export class Config {
})(),
useUrlParams
)
);
);

/**
* Boolean parameters
Expand Down Expand Up @@ -386,7 +387,7 @@ export class Config {
'Either locked mouse, where the pointer is consumed by the video and locked to it, or hovering mouse, where the mouse is not consumed.',
false,
useUrlParams,
(isHoveringMouse: boolean, setting: SettingBase) => {
(isHoveringMouse: boolean, setting: SettingBase) => {
setting.label = `Control Scheme: ${isHoveringMouse ? 'Hovering' : 'Locked'} Mouse`;
}
)
Expand Down Expand Up @@ -475,6 +476,19 @@ export class Config {
)
);

this.numericParameters.set(
NumericParameters.MaxReconnectAttempts,
new SettingNumber(
NumericParameters.MaxReconnectAttempts,
'Max Reconnects',
'Maximum number of reconnects the application will attempt when a streamer disconnects.',
0 /*min*/,
999 /*max*/,
3 /*value*/,
useUrlParams
)
);

this.numericParameters.set(
NumericParameters.MinQP,
new SettingNumber(
Expand Down
53 changes: 52 additions & 1 deletion Frontend/library/src/PixelStreaming/PixelStreaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ describe('PixelStreaming', () => {
});

it('should disconnect and reconnect to signalling server if reconnect is called and connection is up', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
// We explicitly set the max reconnect attempts to 0 to stop the auto-reconnect flow as that is tested separate
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 0}});
const autoconnectedSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy);
Expand All @@ -197,6 +198,56 @@ describe('PixelStreaming', () => {
expect(autoconnectedSpy).toHaveBeenCalled();
});

it('should automatically reconnect and request streamer list N times on websocket close', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 3}});
const autoconnectedSpy = jest.fn();

const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy);

expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1);
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();

pixelStreaming.disconnect();

expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();

// wait 2 seconds
jest.advanceTimersByTime(2000);

// we should have attempted a reconnection
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(2);
// Reconnect triggers the first list streamer message
triggerWebSocketOpen();
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
// We don't have a signalling server to respond with data so lets just fake a response with no streamers
triggerStreamerListMessage([]);
// Wait 2 seconds. This delay waits for the WebRtcPlayerController to realise the previously received list doesn't contain
// the streamer is was preiously subscribed to, so it'll request the list again
jest.advanceTimersByTime(2000);

// Same as above but repeated for the second call
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
triggerStreamerListMessage([]);
jest.advanceTimersByTime(2000);

// Expect the third call
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
triggerStreamerListMessage([]);
jest.advanceTimersByTime(2000);

// We should expect only 3 calls based on our config
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledTimes(3);
});

it('should request streamer list when connected to the signalling server', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const pixelStreaming = new PixelStreaming(config);
Expand Down
123 changes: 86 additions & 37 deletions Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
Flags,
ControlSchemeType,
TextParameters,
OptionParameters
OptionParameters,
NumericParameters
} from '../Config/Config';
import {
EncoderSettings,
Expand Down Expand Up @@ -101,6 +102,10 @@ export class WebRtcPlayerController {
preferredCodec: string;
peerConfig: RTCConfiguration;
videoAvgQp: number;
shouldReconnect: boolean;
isReconnecting: boolean;
reconnectAttempt: number;
subscribedStream: string | null;
signallingUrlBuilder: () => string;

// if you override the disconnection message by calling the interface method setDisconnectMessageOverride
Expand Down Expand Up @@ -221,6 +226,12 @@ export class WebRtcPlayerController {
this.setMouseInputEnabled(false);
this.setKeyboardInputEnabled(false);
this.setGamePadInputEnabled(false);

if(this.shouldReconnect && this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts) > 0) {
this.isReconnecting = true;
this.reconnectAttempt++;
this.restartStreamAutomatically();
}
});

// set up the final webRtc player controller methods from within our application so a connection can be activated
Expand All @@ -247,16 +258,24 @@ export class WebRtcPlayerController {
this.isUsingSFU = false;
this.isQualityController = false;
this.preferredCodec = '';
this.shouldReconnect = true;
this.isReconnecting = false;
this.reconnectAttempt = 0;

this.config._addOnOptionSettingChangedListener(
OptionParameters.StreamerId,
(streamerid) => {
if(streamerid === "") {
return;
}

// close the current peer connection and create a new one
this.peerConnectionController.peerConnection.close();
this.peerConnectionController.createPeerConnection(
this.peerConfig,
this.preferredCodec
);
this.subscribedStream = streamerid;
this.webSocketController.sendSubscribe(streamerid);
}
);
Expand Down Expand Up @@ -1304,45 +1323,75 @@ export class WebRtcPlayerController {
6
);

const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
settingOptions.unshift(''); // add an empty option at the top
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
settingOptions
);

const urlParams = new URLSearchParams(window.location.search);
let autoSelectedStreamerId: string | null = null;
if (messageStreamerList.ids.length == 1) {
// If there's only a single streamer, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = messageStreamerList.ids[0];
} else if (
this.config.isFlagEnabled(Flags.PreferSFU) &&
messageStreamerList.ids.includes('SFU')
) {
// If the SFU toggle is on and there's an SFU connected, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = 'SFU';
} else if (
urlParams.has(OptionParameters.StreamerId) &&
messageStreamerList.ids.includes(
urlParams.get(OptionParameters.StreamerId)
)
) {
// If there's a streamer ID in the URL and a streamer with this ID is connected, set it as the selected streamer
autoSelectedStreamerId = urlParams.get(OptionParameters.StreamerId);
}
if (autoSelectedStreamerId !== null) {
this.config.setOptionSettingValue(
if(this.isReconnecting) {
if(messageStreamerList.ids.includes(this.subscribedStream)) {
// If we're reconnecting and the previously subscribed stream has come back, resubscribe to it
this.isReconnecting = false;
this.reconnectAttempt = 0;
this.webSocketController.sendSubscribe(this.subscribedStream);
} else if(this.reconnectAttempt < this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts)) {
// Our previous stream hasn't come back, wait 2 seconds and request an updated stream list
this.reconnectAttempt++;
setTimeout(() => {
this.webSocketController.requestStreamerList();
}, 2000)
} else {
// We've exhausted our reconnect attempts, return to main screen
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.shouldReconnect = false;
this.webSocketController.close();

this.config.setOptionSettingValue(
OptionParameters.StreamerId,
""
);
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
[]
);
}
} else {
const settingOptions = [...messageStreamerList.ids]; // copy the original messageStreamerList.ids
settingOptions.unshift(''); // add an empty option at the top
this.config.setOptionSettingOptions(
OptionParameters.StreamerId,
autoSelectedStreamerId
settingOptions
);

const urlParams = new URLSearchParams(window.location.search);
let autoSelectedStreamerId: string | null = null;
if (messageStreamerList.ids.length == 1) {
// If there's only a single streamer, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = messageStreamerList.ids[0];
} else if (
this.config.isFlagEnabled(Flags.PreferSFU) &&
messageStreamerList.ids.includes('SFU')
) {
// If the SFU toggle is on and there's an SFU connected, subscribe to it regardless of what is in the URL
autoSelectedStreamerId = 'SFU';
} else if (
urlParams.has(OptionParameters.StreamerId) &&
messageStreamerList.ids.includes(
urlParams.get(OptionParameters.StreamerId)
)
) {
// If there's a streamer ID in the URL and a streamer with this ID is connected, set it as the selected streamer
autoSelectedStreamerId = urlParams.get(OptionParameters.StreamerId);
}
if (autoSelectedStreamerId !== null) {
this.config.setOptionSettingValue(
OptionParameters.StreamerId,
autoSelectedStreamerId
);
}
this.pixelStreaming.dispatchEvent(
new StreamerListMessageEvent({
messageStreamerList,
autoSelectedStreamerId
})
);
}
this.pixelStreaming.dispatchEvent(
new StreamerListMessageEvent({
messageStreamerList,
autoSelectedStreamerId
})
);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions Frontend/ui-library/src/Config/ConfigUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ export class ConfigUI {
psSettingsSection,
this.numericParametersUi.get(NumericParameters.AFKTimeoutSecs)
);
this.addSettingNumeric(
psSettingsSection,
this.numericParametersUi.get(NumericParameters.MaxReconnectAttempts)
);

/* Setup all view/ui related settings under this section */
const viewSettingsSection = this.buildSectionWithHeading(
Expand Down

0 comments on commit c280c06

Please sign in to comment.