Skip to content

fix(call): fullscreen video layout — auto-fit, centre last row, self-view toggle, camera + screenshare coexist#609

Open
HexaField wants to merge 1 commit into
devfrom
fix/fullscreen-video-layout
Open

fix(call): fullscreen video layout — auto-fit, centre last row, self-view toggle, camera + screenshare coexist#609
HexaField wants to merge 1 commit into
devfrom
fix/fullscreen-video-layout

Conversation

@HexaField

Copy link
Copy Markdown
Contributor

Summary

Four related issues with the in-call video grid landed in one PR because each one was a slice of the same per-participant layout pipeline.

1. Tiles overflowed in fullscreen

The grid used grid-auto-rows: min-content and capped each tile via max-height: 100%, but 100% of an overflowing parent is the overflowing height itself, so trailing rows scrolled off-screen. Added a `fullscreen` class that switches the grid to `grid-template-rows: repeat(var(--number-of-rows), 1fr)` with `overflow: hidden` and per-tile `align/justify-self: center`. Every tile now fits on screen while preserving 16/9.

2. Odd-numbered trailing rows left-aligned

Computed `lastRowTiles` from `allParticipants.length % numberOfColumns` and emit `grid-column-start: <offset+1>` on the first tile of an incomplete trailing row. Subsequent tiles flow naturally — no per-row wrapper required.

3. Self-view couldn't be hidden

Added `selfViewVisible` / `toggleSelfViewVisible` on the UI store and a desktop-only "Hide my video / Show my video" button in `MainCallControls.vue`. When off, the local camera tile is dropped from `allParticipants`. The user remains in the call and is still heard; the local screenshare tile (when present) is still shown so they can verify what they're broadcasting.

4. Screenshare overrode the user's camera tile

Three connected changes let camera + screenshare run side-by-side for the same user on both sides of the call:

  • `mediaDevicesStore.turnOnScreenShare` no longer removes the camera track from `stream`. The screenshare is captured into its own `screenShareStream` ref and sent to peers via a new `webrtcStore.addScreenShareTrack` (a `peer.addTrack(track, dedicatedStream)` instead of a `replaceTrack` swap), so the receiving side fires a fresh `peer.on('track')` event with a distinct stream id.
  • `webrtcStore.ts` peer.on('track') handler used to wipe the per-peer `streams` array on every new stream (`streams = [stream]`), which silently dropped the camera tile the moment a screenshare track arrived. Now appends new streams and updates in place when extra tracks land on an existing stream. Tracks that end clear their stream from the array.
  • `useVideoLayout.allParticipants` splits both the local user and each remote peer into separate camera-tile / screenshare-tile entries (`streamKind: 'camera' | 'screenshare'`). Remote screenshares are identified as streams with video tracks but no audio tracks — the camera stream always carries the mic track.
  • Cleaned up the old `savedVideoTrack` swap-back bookkeeping in `toggleVideo` / `resetMediaDevices` — no longer needed now that camera and screenshare run on independent streams.

Composite `did:streamKind` participant keys replace bare DIDs in `focusOnVideo` / focused-layout. Bare DIDs from older persisted state still match (first tile for that user, which is the camera tile).

Test plan

  • `vue-tsc --noEmit` clean for all changed files (same pre-existing errors as dev — none introduced)
  • Manual: 1-tile / 2-tile / 4-tile / 7-tile grids in fullscreen — every tile visible, trailing-row tiles centred
  • Manual: toggle "Hide my video" → local camera tile disappears, others still rendered, can re-show
  • Manual: toggle screenshare while camera is on → both tiles render side-by-side locally
  • Manual: peer toggles screenshare while their camera is on → both tiles render side-by-side in our grid
  • Manual: peer stops sharing → screenshare tile disappears, camera tile remains

…view toggle, camera + screenshare coexist

Four related issues with the in-call video grid landed in one PR because
each one was a slice of the same per-participant layout pipeline:

1. **Tiles overflowed in fullscreen.**  The grid used
   `grid-auto-rows: min-content` and capped each tile via `max-height:
   100%`, but `100%` of an overflowing parent is itself the overflowing
   height, so the trailing rows scrolled off-screen.  Added a
   `fullscreen` class on `.video-grid` that switches to
   `grid-template-rows: repeat(var(--number-of-rows), 1fr)` with
   `overflow: hidden` and per-tile `align/justify-self: center`, so every
   tile fits the available space while preserving the 16/9 aspect
   ratio.

2. **Odd-numbered trailing rows were left-aligned.**  Added a computed
   `lastRowTiles` count and emit a `grid-column-start: <offset+1>` style
   on the first tile of an incomplete trailing row.  Subsequent tiles
   flow naturally; no per-row wrapper or `:has(...)` selector required.

3. **Self-view couldn't be hidden.**  Added `selfViewVisible` /
   `toggleSelfViewVisible` on the UI store (persisted across sessions)
   and a desktop-only "Hide my video / Show my video" button in
   `MainCallControls.vue`.  When off, the local camera tile is dropped
   from `allParticipants` — the user remains in the call and is still
   heard, the local screenshare tile (when present) is still shown so
   the user can verify what they're broadcasting.

4. **Screenshare overrode the user's camera tile on both sides of the
   call.**  Three connected changes:
   - `mediaDevicesStore.turnOnScreenShare` no longer removes the camera
     track from `stream`.  The screenshare is captured into its own
     `screenShareStream` ref and sent to peers via a new
     `webrtcStore.addScreenShareTrack` (a `peer.addTrack(track,
     dedicatedStream)` instead of a `replaceTrack` swap), so the
     receiving side fires a fresh `peer.on('track')` event with a
     distinct stream id.
   - `webrtcStore.ts` peer.on('track') handler used to wipe the per-peer
     `streams` array on every new stream (`streams = [stream]`), which
     silently dropped the camera tile the moment a screenshare track
     arrived.  Now appends a new stream entry and updates in place when
     extra tracks land on an existing stream.  Tracks that end clear
     their stream from the array.
   - `useVideoLayout.allParticipants` splits both the local user and
     each remote peer into separate camera-tile / screenshare-tile
     entries (`streamKind: 'camera' | 'screenshare'`).  Remote
     screenshares are identified as streams with video tracks but no
     audio tracks — the camera stream always carries the mic track.
   - Cleaned up the old `savedVideoTrack` swap-back bookkeeping in
     `toggleVideo` / `resetMediaDevices` — superfluous now that camera
     and screenshare run on independent streams.

Composite `did:streamKind` participant keys replace bare DIDs in
`focusOnVideo` / focused-layout, so the focus model handles the
camera-vs-screenshare split too.  Bare DIDs from older persisted state
still match (first tile for that user, which is the camera tile).
@netlify

netlify Bot commented Jun 11, 2026

Copy link
Copy Markdown

Deploy Preview for fluxsocial-dev ready!

Name Link
🔨 Latest commit f2f4928
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/6a2a751e6641fb00087761c7
😎 Deploy Preview https://deploy-preview-609--fluxsocial-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@HexaField, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 46 minutes and 1 second. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 98471e9e-e17e-45c5-a4b4-8e59bad9d9e1

📥 Commits

Reviewing files that changed from the base of the PR and between bc3bae2 and f2f4928.

📒 Files selected for processing (6)
  • app/src/components/call/composables/useVideoLayout.ts
  • app/src/components/call/controls/MainCallControls.vue
  • app/src/components/call/window/VideoGrid.vue
  • app/src/stores/mediaDevicesStore.ts
  • app/src/stores/uiStore.ts
  • app/src/stores/webrtcStore.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/fullscreen-video-layout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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