Skip to content

Commit e120706

Browse files
fix(frontend): pair MouseDouble with a synthetic MouseUp release (#10) (#873) (#877)
* fix(frontend): pair MouseDouble with a synthetic MouseUp release (#10) The streamer plugin treats `MouseDouble` as a press-class event (`RoutePointerDoubleClickEvent` / `OnMouseDoubleClick`) but never synthesizes the matching release; the browser's `mouseup` was already consumed by the prior `MouseUp`, leaving UE thinking the button was still held. Send a paired `MouseUp` after `MouseDouble` from both mouse controllers, gated on a new `MouseDoubleClickAutoRelease` flag (default on) so projects that handle the doubleclick release themselves can opt out. * feat(ui): expose MouseDoubleClickAutoRelease in the settings panel Adds the new flag to the Input section of the settings panel so users can toggle it from the UI without having to pass a URL parameter. (cherry picked from commit 06de3f4)
1 parent d932cd9 commit e120706

8 files changed

Lines changed: 71 additions & 9 deletions

File tree

.changeset/double-click-release.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.7": patch
3+
"@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.7": patch
4+
---
5+
6+
Synthesize a `MouseUp` after `MouseDouble` in both mouse controllers so the streamer's pressed-button state stays balanced after a double-click (#10). The plugin treats `MouseDouble` as a press-class event (`RoutePointerDoubleClickEvent` / `IGenericApplicationMessageHandler::OnMouseDoubleClick`) but never synthesizes a release; the browser's preceding `mouseup` was already consumed by the prior `MouseUp`, so UE was left thinking the button was still held — manifesting, for example, as camera pans that latched on after a double-click. Behaviour is gated on the new `MouseDoubleClickAutoRelease` flag (default on); disable it via `?MouseDoubleClickAutoRelease=false` or the settings panel to restore pre-fix behaviour for projects that handle the doubleclick release themselves.

Frontend/library/src/Config/Config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class Flags {
3030
static UseCamera = 'UseCamera' as const;
3131
static KeyboardInput = 'KeyboardInput' as const;
3232
static MouseInput = 'MouseInput' as const;
33+
static MouseDoubleClickAutoRelease = 'MouseDoubleClickAutoRelease' as const;
3334
static TouchInput = 'TouchInput' as const;
3435
static GamepadInput = 'GamepadInput' as const;
3536
static XRControllerInput = 'XRControllerInput' as const;
@@ -506,6 +507,19 @@ export class Config {
506507
)
507508
);
508509

510+
this.flags.set(
511+
Flags.MouseDoubleClickAutoRelease,
512+
new SettingFlag(
513+
Flags.MouseDoubleClickAutoRelease,
514+
'Auto release after double-click',
515+
'After sending a MouseDouble message, also send a matching MouseUp so the streamer’s pressed-button state stays balanced. Disable to restore pre-fix behaviour if your project handles the doubleclick release itself.',
516+
settings && Object.prototype.hasOwnProperty.call(settings, Flags.MouseDoubleClickAutoRelease)
517+
? settings[Flags.MouseDoubleClickAutoRelease]
518+
: true,
519+
useUrlParams
520+
)
521+
);
522+
509523
this.flags.set(
510524
Flags.TouchInput,
511525
new SettingFlag(

Frontend/library/src/Inputs/InputClassesFactory.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,24 @@ export class InputClassesFactory {
5656
* register mouse events based on a control type
5757
* @param controlScheme - if the mouse is either hovering or locked
5858
*/
59-
registerMouse(controlScheme: ControlSchemeType) {
59+
registerMouse(controlScheme: ControlSchemeType, config: Config) {
6060
Logger.Info('Register Mouse Events');
6161
let mouseController: MouseController;
6262
if (controlScheme == ControlSchemeType.HoveringMouse) {
6363
mouseController = new MouseControllerHovering(
6464
this.toStreamerMessagesProvider,
6565
this.videoElementProvider,
6666
this.coordinateConverter,
67-
this.activeKeys
67+
this.activeKeys,
68+
config
6869
);
6970
} else {
7071
mouseController = new MouseControllerLocked(
7172
this.toStreamerMessagesProvider,
7273
this.videoElementProvider,
7374
this.coordinateConverter,
74-
this.activeKeys
75+
this.activeKeys,
76+
config
7577
);
7678
}
7779

Frontend/library/src/Inputs/MouseController.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { InputCoordTranslator } from '../Util/InputCoordTranslator';
55
import { VideoPlayer } from '../VideoPlayer/VideoPlayer';
66
import type { ActiveKeys } from './InputClassesFactory';
77
import { IInputController } from './IInputController';
8+
import { Config } from '../Config/Config';
89

910
/**
1011
* Extra types for Document and WheelEvent
@@ -29,6 +30,7 @@ export class MouseController implements IInputController {
2930
streamMessageController: StreamMessageController;
3031
coordinateConverter: InputCoordTranslator;
3132
activeKeys: ActiveKeys;
33+
config: Config;
3234

3335
// bound listeners
3436
onEnterListener: (event: MouseEvent) => void;
@@ -38,12 +40,14 @@ export class MouseController implements IInputController {
3840
streamMessageController: StreamMessageController,
3941
videoPlayer: VideoPlayer,
4042
coordinateConverter: InputCoordTranslator,
41-
activeKeys: ActiveKeys
43+
activeKeys: ActiveKeys,
44+
config: Config
4245
) {
4346
this.streamMessageController = streamMessageController;
4447
this.coordinateConverter = coordinateConverter;
4548
this.videoPlayer = videoPlayer;
4649
this.activeKeys = activeKeys;
50+
this.config = config;
4751

4852
this.onEnterListener = this.onMouseEnter.bind(this);
4953
this.onLeaveListener = this.onMouseLeave.bind(this);

Frontend/library/src/Inputs/MouseControllerHovering.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { InputCoordTranslator } from '../Util/InputCoordTranslator';
44
import { VideoPlayer } from '../VideoPlayer/VideoPlayer';
55
import type { ActiveKeys } from './InputClassesFactory';
66
import { MouseController } from './MouseController';
7+
import { Config, Flags } from '../Config/Config';
78

89
/**
910
* A mouse controller that allows the mouse to freely float over the video document.
@@ -29,9 +30,10 @@ export class MouseControllerHovering extends MouseController {
2930
streamMessageController: StreamMessageController,
3031
videoPlayer: VideoPlayer,
3132
coordinateConverter: InputCoordTranslator,
32-
activeKeys: ActiveKeys
33+
activeKeys: ActiveKeys,
34+
config: Config
3335
) {
34-
super(streamMessageController, videoPlayer, coordinateConverter, activeKeys);
36+
super(streamMessageController, videoPlayer, coordinateConverter, activeKeys, config);
3537
this.videoElementParent = videoPlayer.getVideoParentElement() as HTMLDivElement;
3638
this.onMouseUpListener = this.onMouseUp.bind(this);
3739
this.onMouseDownListener = this.onMouseDown.bind(this);
@@ -175,5 +177,16 @@ export class MouseControllerHovering extends MouseController {
175177
}
176178
const coord = this.coordinateConverter.translateUnsigned(event.offsetX, event.offsetY);
177179
this.streamMessageController.toStreamerHandlers.get('MouseDouble')([event.button, coord.x, coord.y]);
180+
181+
// The streamer plugin treats `MouseDouble` as a press-class event (it routes to
182+
// Slate's RoutePointerDoubleClickEvent / IGenericApplicationMessageHandler::OnMouseDoubleClick)
183+
// but never synthesizes the matching release. The browser's preceding `mouseup` was
184+
// already consumed by the prior `MouseUp` message, so without this UE is left thinking
185+
// the button is still held — manifesting as e.g. camera pans that latch on after a
186+
// double-click. See issue #10.
187+
// Disable Flags.MouseDoubleClickAutoRelease to restore the pre-fix behaviour.
188+
if (this.config.isFlagEnabled(Flags.MouseDoubleClickAutoRelease)) {
189+
this.streamMessageController.toStreamerHandlers.get('MouseUp')([event.button, coord.x, coord.y]);
190+
}
178191
}
179192
}

Frontend/library/src/Inputs/MouseControllerLocked.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { InputCoordTranslator, TranslatedCoordUnsigned } from '../Util/InputCoor
55
import { VideoPlayer } from '../VideoPlayer/VideoPlayer';
66
import type { ActiveKeys } from './InputClassesFactory';
77
import { MouseController } from './MouseController';
8+
import { Config, Flags } from '../Config/Config';
89

910
/**
1011
* A mouse controller that locks the mouse to the video document and prevents it from leaving the window
@@ -28,9 +29,10 @@ export class MouseControllerLocked extends MouseController {
2829
streamMessageController: StreamMessageController,
2930
videoPlayer: VideoPlayer,
3031
coordinateConverter: InputCoordTranslator,
31-
activeKeys: ActiveKeys
32+
activeKeys: ActiveKeys,
33+
config: Config
3234
) {
33-
super(streamMessageController, videoPlayer, coordinateConverter, activeKeys);
35+
super(streamMessageController, videoPlayer, coordinateConverter, activeKeys, config);
3436
this.videoElementParent = videoPlayer.getVideoParentElement() as HTMLDivElement;
3537
this.x = this.videoElementParent.getBoundingClientRect().width / 2;
3638
this.y = this.videoElementParent.getBoundingClientRect().height / 2;
@@ -191,5 +193,20 @@ export class MouseControllerLocked extends MouseController {
191193
this.normalizedCoord.x,
192194
this.normalizedCoord.y
193195
]);
196+
197+
// The streamer plugin treats `MouseDouble` as a press-class event (it routes to
198+
// Slate's RoutePointerDoubleClickEvent / IGenericApplicationMessageHandler::OnMouseDoubleClick)
199+
// but never synthesizes the matching release. The browser's preceding `mouseup` was
200+
// already consumed by the prior `MouseUp` message, so without this UE is left thinking
201+
// the button is still held — manifesting as e.g. camera pans that latch on after a
202+
// double-click. See issue #10.
203+
// Disable Flags.MouseDoubleClickAutoRelease to restore the pre-fix behaviour.
204+
if (this.config.isFlagEnabled(Flags.MouseDoubleClickAutoRelease)) {
205+
this.streamMessageController.toStreamerHandlers.get('MouseUp')([
206+
event.button,
207+
this.normalizedCoord.x,
208+
this.normalizedCoord.y
209+
]);
210+
}
194211
}
195212
}

Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2020,7 +2020,7 @@ export class WebRtcPlayerController {
20202020
const mouseMode = this.config.isFlagEnabled(Flags.HoveringMouseMode)
20212021
? ControlSchemeType.HoveringMouse
20222022
: ControlSchemeType.LockedMouse;
2023-
this.mouseController = this.inputClassesFactory.registerMouse(mouseMode);
2023+
this.mouseController = this.inputClassesFactory.registerMouse(mouseMode, this.config);
20242024
}
20252025
}
20262026

Frontend/ui-library/src/Config/ConfigUI.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ export class ConfigUI {
222222
if (isSettingEnabled(settingsConfig, Flags.MouseInput))
223223
this.addSettingFlag(inputSettingsSection, this.flagsUi.get(Flags.MouseInput));
224224

225+
if (isSettingEnabled(settingsConfig, Flags.MouseDoubleClickAutoRelease))
226+
this.addSettingFlag(
227+
inputSettingsSection,
228+
this.flagsUi.get(Flags.MouseDoubleClickAutoRelease)
229+
);
230+
225231
if (isSettingEnabled(settingsConfig, Flags.FakeMouseWithTouches))
226232
this.addSettingFlag(inputSettingsSection, this.flagsUi.get(Flags.FakeMouseWithTouches));
227233

0 commit comments

Comments
 (0)