Skip to content

fix: NDI output aspect ratio when started without Live#2

Open
CodeKing12 wants to merge 1 commit into
mainfrom
fix/ndi-aspect-ratio-without-live
Open

fix: NDI output aspect ratio when started without Live#2
CodeKing12 wants to merge 1 commit into
mainfrom
fix/ndi-aspect-ratio-without-live

Conversation

@CodeKing12

Copy link
Copy Markdown
Collaborator

Root cause

When NDI is started without first going Live, the projection window is rendered as a hidden off-screen BrowserWindow (not fullscreen). In that state the CSS height: 100% chain breaks silently:

  1. Astro 5 inserts <astro-island> as a block element between <body> and the SolidJS component root. That element has no explicit height, so height: 100% on <main> (inside the component) resolves against a zero-height ancestor — producing a collapsed or partial-height render.
  2. Astro's scoped <style> does not reliably apply height: 100% to html/body in Astro 5 because Astro cannot attach its scoping attribute to root elements; the rules may be silently dropped.

The visible Live path avoids both issues because OS-level fullscreen forces the viewport to fill the display regardless of the CSS height chain.

A secondary contributing factor: AppSettingsDialog stored display.workArea (which excludes the Windows taskbar, ~40–60 px shorter) instead of display.bounds. For users with useCustomProjectionBounds: true this meant the NDI window was sized shorter than the full display, producing equal top/bottom letterbox bars in Streamlabs's 16:9 canvas. resolveProjectionBounds() in main.ts already overrides this for the default (useCustomBounds: false) path, but the stored value was still wrong at the source.

Changes

src/components/app/projection/RenderProjection.tsx

  • Changed root <main> from h="full" (height: 100%) to h="vh" (height: 100vh).
    100vh is viewport-relative and does not depend on the ancestor chain, so it fills the full window frame captured by beginFrameSubscription regardless of <astro-island> wrapping.
  • Removed the now-redundant maxH="vh" (max-height is redundant when height is already 100vh).

src/layouts/Layout.astro

  • Changed <style> to <style is:global> so html, body { margin: 0; width: 100%; height: 100%; } is guaranteed to be emitted without Astro's scoping transform.
    Belt-and-suspenders: keeps the height chain intact for any future change that might rely on percentage heights.

src/components/modals/AppSettingsDialog.tsx

  • handleDisplaysUpdate: externalDisplay.workAreaexternalDisplay.bounds
  • handleDisplayChange: details.items[0].workAreadetails.items[0].bounds
    Removes the stored-value discrepancy at the source so projectionBounds always reflects the full display dimensions.

What was ruled out

  • ndi-sender.ts: Frame dimensions come from image.getSize() (actual captured frame), not from DEFAULT_CONFIG width/height — no change needed there.
  • main.ts / resolveProjectionBounds(): Already correctly uses display.bounds at runtime for the default path — no change needed there.

Test plan

  • Start NDI without going Live first. Open Streamlabs (or any NDI receiver) and verify the projection fills the 16:9 canvas with no top/bottom black bars.
  • Go Live first, then start NDI. Verify no regression — projection still fills the canvas correctly.
  • Toggle between Logo mode and clear display; verify visibility transitions still work.
  • On a multi-monitor setup, change the projection display in Settings and restart NDI; verify correct display dimensions are used.
  • Enable useCustomProjectionBounds in Settings and set custom bounds; verify NDI window respects the custom size (not overridden by the bounds fix).

Generated by Claude Code

Three related changes that together eliminate the ~30px black margins
visible in Streamlabs/OBS when NDI is started before going Live:

1. RenderProjection: h="full" → h="vh"
   The projection page root <main> used height:100%, which requires every
   ancestor element to carry an explicit height. In Astro 5 the SolidJS
   island is wrapped in an <astro-island> block element with no explicit
   height, silently breaking the percentage chain. height:100vh is
   viewport-relative and resolves correctly regardless of ancestor sizing,
   so the content always fills the full window frame captured by NDI.

2. Layout.astro: <style> → <style is:global>
   Astro 5 scopes component styles by default. The html,body{height:100%}
   rule in a scoped block can be silently dropped because Astro cannot
   add its scoping attribute to the root elements. is:global makes the
   intent explicit and guarantees the rule applies.

3. AppSettingsDialog: workArea → bounds for stored projectionBounds
   Auto-detected projection bounds were stored as display.workArea
   (excludes the taskbar). resolveProjectionBounds() in main.ts already
   overrides this at runtime when useCustomProjectionBounds is false, but
   users who enable custom bounds received the workArea dimensions verbatim,
   creating a window shorter than the display and causing NDI to stream at
   the reduced height. Storing display.bounds removes the discrepancy.

https://claude.ai/code/session_0117ULWZhnNJFTTkGnba69Eg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant