Files
iptvnator/apps/electron-backend/src/app/services/embedded-mpv-bounds.util.ts
T
4grayandClaude Fable 5 b719ed23cd fix(playback): position embedded MPV native view correctly on scaled displays (#1206)
* fix(playback): position embedded MPV native view correctly on scaled displays

Renderer bounds are measured in CSS pixels, but the native-view engines
position OS windows: SetWindowPos (win32) and XMoveResizeWindow (linux)
expect physical pixels, NSView setFrame (macOS) expects points. The raw
values landed the video toward the window's top-left corner at 1/scale of
its size on any display scale or page zoom other than 100%, windowed and
fullscreen alike.

The main process now converts native-view bounds (x page zoom everywhere,
x display scale factor on win32/linux) with edge-based rounding; frame-copy
bounds stay unscaled because the adapter owns its render scale. The session
controller re-syncs bounds when devicePixelRatio changes, covering moves to
a display with a different scale that keep the CSS layout identical.

Closes #1145

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): keep CSS bounds unrounded until native scaling

Review feedback on #1206: measureBounds() rounded the CSS edges in the
renderer, before the main-process CSS-to-native conversion, so fractional
layout positions could drift by a pixel per scale factor (a 10.49px edge
at 200% must land on 21 physical px, not 20). The renderer now sends raw
getBoundingClientRect() edges and rounding happens exactly once, after
scaling. Also pins process.platform explicitly in the macOS wiring test
instead of relying on the suite default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 07:48:44 +02:00

61 lines
2.2 KiB
TypeScript

import { EmbeddedMpvBounds } from '@iptvnator/shared/interfaces';
export interface NativeViewBoundsContext {
platform: NodeJS.Platform;
/** Page zoom factor of the main window's webContents (1 = 100%). */
zoomFactor: number;
/** Scale factor of the display hosting the main window (1 = 96 dpi). */
displayScaleFactor: number;
}
/**
* Converts renderer-measured bounds (CSS pixels from
* getBoundingClientRect()) into the coordinate space the native-view
* engines position their OS windows in: physical pixels for the win32
* child HWND (SetWindowPos) and the linux child X11 window
* (XMoveResizeWindow), points — device-independent pixels — for the macOS
* NSView (setFrame). CSS pixels match points only at 100% page zoom and
* match physical pixels only at 100% page zoom AND 100% display scale, so
* every platform scales by the zoom factor and win32/linux additionally by
* the display scale factor (#1145).
*
* Bounds arrive with unrounded CSS edges and are rounded exactly once here,
* after scaling: edges first, then width/height derived from them. Rounding
* any earlier (or per-field) lets fractional CSS layouts drift by a pixel
* per scale factor and open 1px seams between the native video window and
* the surrounding DOM UI.
*/
export function toNativeViewBounds(
bounds: EmbeddedMpvBounds,
context: NativeViewBoundsContext
): EmbeddedMpvBounds {
const scale = resolveNativeViewScale(context);
if (scale === 1) {
return bounds;
}
const left = Math.round(bounds.x * scale);
const top = Math.round(bounds.y * scale);
const right = Math.round((bounds.x + bounds.width) * scale);
const bottom = Math.round((bounds.y + bounds.height) * scale);
return {
x: left,
y: top,
width: Math.max(1, right - left),
height: Math.max(1, bottom - top),
};
}
function resolveNativeViewScale(context: NativeViewBoundsContext): number {
const zoomFactor = sanitizeFactor(context.zoomFactor);
if (context.platform === 'darwin') {
return zoomFactor;
}
return zoomFactor * sanitizeFactor(context.displayScaleFactor);
}
function sanitizeFactor(value: number): number {
return Number.isFinite(value) && value > 0 ? value : 1;
}