mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:16:43 -08:00
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>
This commit is contained in:
1 parent
5af6058134
commit
d7753f7401
7 files changed
+42
-13
No files matched your search
@@ -80,6 +80,17 @@ describe('toNativeViewBounds', () => {
|
||||
expect(result).toEqual({ x: 150, y: 75, width: 960, height: 540 });
|
||||
});
|
||||
|
||||
it('rounds fractional CSS edges only after scaling', () => {
|
||||
// A 10.49px CSS edge at 200% renders at 21 physical pixels; edges
|
||||
// rounded before scaling would send 20 and shift the video by 1px.
|
||||
const result = toNativeViewBounds(
|
||||
{ x: 10.49, y: 0.5, width: 100.02, height: 50 },
|
||||
context({ platform: 'win32', displayScaleFactor: 2 })
|
||||
);
|
||||
|
||||
expect(result).toEqual({ x: 21, y: 1, width: 200, height: 100 });
|
||||
});
|
||||
|
||||
it('keeps vertically adjacent rects seamless under fractional scales', () => {
|
||||
// 42 × 1.25 and 153 × 1.25 both land on .5/.25 fractions: rounding
|
||||
// x/y/width/height independently would misplace the shared edge by
|
||||
|
||||
@@ -19,8 +19,11 @@ export interface NativeViewBoundsContext {
|
||||
* every platform scales by the zoom factor and win32/linux additionally by
|
||||
* the display scale factor (#1145).
|
||||
*
|
||||
* Edges are scaled before deriving width/height so rounding cannot open
|
||||
* 1px seams between the native video window and the surrounding DOM UI.
|
||||
* 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,
|
||||
|
||||
@@ -487,6 +487,7 @@ describe('EmbeddedMpvNativeService power blocker', () => {
|
||||
it('applies page zoom but not the display scale on macOS', () => {
|
||||
// NSView frames are in points (device-independent pixels): only
|
||||
// the webContents zoom factor separates them from CSS pixels.
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
screenGetDisplayMatchingMock.mockReturnValue({ scaleFactor: 2 });
|
||||
mainWindowGetZoomFactorMock.mockReturnValue(1.25);
|
||||
addon.createSession.mockReturnValueOnce('s-zoom');
|
||||
|
||||
@@ -607,9 +607,11 @@ and match physical pixels only at 100% page zoom AND 100% display scale.
|
||||
`EmbeddedMpvNativeService` therefore converts every native-view bounds payload
|
||||
in the main process (`toNativeViewBounds` in `embedded-mpv-bounds.util.ts`):
|
||||
all platforms scale by the webContents zoom factor, win32/linux additionally
|
||||
by the scale factor of the display hosting the window. Edges are scaled
|
||||
before deriving width/height so rounding cannot open 1px seams against the
|
||||
surrounding DOM UI. Skipping this conversion is issue #1145: on scaled
|
||||
by the scale factor of the display hosting the window. The renderer sends
|
||||
unrounded CSS edges (`measureBounds` does not round) and the conversion
|
||||
rounds exactly once, after scaling — edges first, width/height derived from
|
||||
them — so fractional CSS layouts and fractional scales cannot open 1px
|
||||
seams against the surrounding DOM UI. Skipping this conversion is issue #1145: on scaled
|
||||
displays (Windows 125%, Linux fractional scaling, HiDPI TVs) the video landed
|
||||
toward the window's top-left corner at `1/scale` of its size, in windowed and
|
||||
fullscreen mode alike.
|
||||
|
||||
@@ -59,7 +59,10 @@ describe('embedded MPV format utilities', () => {
|
||||
expect(volumeLabel(0.755)).toBe('Volume 76%');
|
||||
});
|
||||
|
||||
it('rounds host bounds and keeps minimum native view dimensions', () => {
|
||||
it('preserves fractional host edges and keeps minimum native view dimensions', () => {
|
||||
// Rounding happens once in the main process, after CSS→native
|
||||
// scaling — pre-rounded edges would drift by up to 1px per scale
|
||||
// factor on scaled displays.
|
||||
const host = {
|
||||
getBoundingClientRect: () => ({
|
||||
left: 10.4,
|
||||
@@ -70,8 +73,8 @@ describe('embedded MPV format utilities', () => {
|
||||
} as HTMLElement;
|
||||
|
||||
expect(measureBounds(host)).toEqual({
|
||||
x: 10,
|
||||
y: 21,
|
||||
x: 10.4,
|
||||
y: 20.6,
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
@@ -120,12 +120,19 @@ export function persistVolume(value: number): void {
|
||||
localStorage.setItem('volume', String(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the host element in CSS pixels without rounding. The main process
|
||||
* converts these bounds to native units (page zoom × display scale) and
|
||||
* rounds exactly once, after scaling — pre-rounding here would bake up to
|
||||
* ±0.5px of CSS error that the scale factor then amplifies into visible
|
||||
* off-by-one seams (e.g. a 10.49px edge at 200% renders at 21px, not 20px).
|
||||
*/
|
||||
export function measureBounds(host: HTMLElement): EmbeddedMpvBounds {
|
||||
const rect = host.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(rect.left),
|
||||
y: Math.round(rect.top),
|
||||
width: Math.max(1, Math.round(rect.width)),
|
||||
height: Math.max(1, Math.round(rect.height)),
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: Math.max(1, rect.width),
|
||||
height: Math.max(1, rect.height),
|
||||
};
|
||||
}
|
||||
+3
-1
@@ -141,8 +141,10 @@ describe('EmbeddedMpvSessionController', () => {
|
||||
);
|
||||
|
||||
expect(electron.prepareEmbeddedMpv).toHaveBeenCalled();
|
||||
// Fractional CSS edges stay unrounded: the main process rounds once,
|
||||
// after converting them to native units.
|
||||
expect(electron.createEmbeddedMpvSession).toHaveBeenCalledWith(
|
||||
{ x: 11, y: 21, width: 640, height: 360 },
|
||||
{ x: 10.6, y: 20.5, width: 640, height: 360 },
|
||||
'Example Movie',
|
||||
0.7
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user