Calls: real full screen, screen sharing, both at once; and multiple live accounts - #2015
Closed
damienheiser wants to merge 1 commit into
Closed
damienheiser wants to merge 1 commit into
damienheiser wants to merge 1 commit into
Conversation
Three changes that were built and verified together. They are independent and can be split into separate PRs on request. 1. Full screen is actually full screen -------------------------------------- The call overlay was absolutely positioned inside the renderer window, so it filled the app, never the display; nothing in the renderer ever called requestFullscreen(). It now does, and reconciles Redux with the 'fullscreenchange' event so the browser's own exit paths - Escape, the OS control, another app taking the display - cannot leave the app believing it is still full screen. The main process no longer persists window.fullscreen while a *page* is what put the window in full screen. Without that guard, quitting during a full screen call made Session reopen full screen forever after. 2. Making it discoverable ------------------------- The control was an unlabelled glyph at opacity 0.4 which only rendered when the remote peer happened to be sending video. It is now always rendered during a call, labelled and captioned, with title and aria-label, at a legible resting opacity, plus a persistent "Exit Full Screen (Esc)" pill inside the overlay. 3. Screen sharing, usable *with* full screen -------------------------------------------- Two things each independently made "share while full screen" impossible: a container-level onClick that exited full screen on any click - including on the call buttons - and an effect that exited full screen whenever the peer's video muted. Both are gone. Sharing reuses the call's single video sender via replaceTrack rather than adding a second video m-line: Session's call signalling has no mid-call renegotiation, so an extra m-line would break calls with unmodified clients. The trade-off, stated rather than hidden: while sharing, the camera is off, and the camera selected before the share is restored when it stops - including when the OS "Stop sharing" affordance ends the track. Electron ships no source picker, so Session has its own, backed by desktopCapturer and setDisplayMediaRequestHandler. display-capture is gated on the same setting calls already require. 4. Multiple accounts, all of them live -------------------------------------- Session holds one account per data directory - database, keys, settings, attachments, poller and store are all rooted there - so each account gets its own directory and its own process. Every account polls and notifies whether or not its window is the one on screen. Switching is launching: Electron's single-instance lock is per data directory, so starting an account that is already running hands its argv to the live process, which focuses its window, and exits. One code path covers "start it" and "bring it to the front". The first account resolves to the directory Session already used, so an existing install keeps its account with no migration step. Removing an account from the switcher never deletes its data; the path is reported instead. This is not a unified inbox - that needs Session's per-process singletons to become per-account, which is a core rewrite. Bugs found and fixed along the way ---------------------------------- - removeVideoEventsListener called splice(index) with one argument, removing every listener from that index onward, so unmounting one call component silently detached the listeners of every component registered after it. - useVideoEventListener evaluated useMountedState() during render, which is false on the first render, so the listener it registered discarded every update it was handed. The in-conversation view got away with it because Redux re-renders it; the full screen overlay mounts once and stayed frozen at its initial defaults - no srcObject, and no idea a share was running. - The single-instance check only quit on a failed lock when NODE_APP_INSTANCE was 0, so two processes could open one account's SQLCipher database. - The account registry was cached per process, so a process worked from its start-up snapshot: a stale switcher, and - because every mutation writes the whole object - an account added in one window erased by an older process's next write. Reads now go to disk. Design and observed verification, including what was not verified, are in docs/TDD-call-fullscreen-and-screenshare.md and docs/TDD-multi-account.md.
damienheiser
force-pushed
the
feature/call-fullscreen-and-screenshare
branch
from
September 21, 2026 02:46
3979253 to
dc6b01d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opening this as a single branch because the pieces were built and tested together, but it is three independent changes and I am happy to split it into three PRs (full screen + discoverability / screen sharing / multi-account) if that is easier to review — CONTRIBUTING asks for small PRs and this is not one. Say the word and I'll break it up.
Everything below was observed in the running app, not asserted; §5 of each design document lists what was not verified as well.
Three pieces of work, each with a design document and results observed in the running app rather than asserted.
1. Call video: real full screen
"Full screen" was an absolutely-positioned overlay inside the renderer window — it filled the app, never the display. There was no call path to
requestFullscreen()anywhere in the renderer.fullscreenchangeso the browser's own exit paths (Escape, the OS control, another app taking the display) cannot leave the app believing it is still full screen.window.fullscreenwhile a page is what put the window in full screen. Without that guard, quitting during a full-screen call made Session reopen full screen forever after.2. Making it obvious
The control was a bare glyph at
opacity: 0.4, with no tooltip, no label — and it only rendered when the remote peer happened to be sending video.titleandaria-label, at a legible resting opacity, plus a persistent exit pill inside the overlay.3. Screen sharing, usable with full screen
There was no screen sharing at all. Two things in the old code each independently made "share while full screen" impossible: a container-level
onClickthat exited full screen on any click (including on the call buttons), and an effect that exited full screen whenever the peer's video muted. Both are gone.replaceTrack. Deliberately not a second video m-line: Session's call signalling has no mid-call renegotiation, so an extra m-line would break calls with unmodified clients. The trade-off, stated rather than hidden: while sharing, your camera is off, and the camera selected before the share is restored when it stops — including when the OS "Stop sharing" affordance ends the track.ScreenSharePicker.tsx), backed bydesktopCapturerandsetDisplayMediaRequestHandler. One portable path, no platform branch.display-captureis gated on the same setting calls already require.4. Multiple accounts, all of them live
Session holds one account per data directory — database, keys, settings, attachments, poller and store are all rooted there. So each account gets its own directory and its own process, which is what makes "all active" true rather than a claim: every account polls and notifies whether or not its window is the one on screen.
Not a unified inbox — that needs Session's per-process singletons to become per-account, which is a rewrite of the core rather than a feature on top of it.
Bugs found and fixed along the way
removeVideoEventsListenercalledsplice(index)with one argument, removing every listener from that index onward — unmounting one call component silently detached the listeners of every component registered after it.useVideoEventListenerevaluateduseMountedState()during render, which isfalseon the first render, so the listener it registered discarded every update it was ever handed. The in-conversation view got away with it because Redux re-renders it; the full-screen overlay mounts once and stayed frozen at its initial defaults — nosrcObject, and no idea a share was running.NODE_APP_INSTANCE === 0, so two processes could open one account's SQLCipher database. Found by testing, not by reading.How this was verified
Two and then three real instances, real Session accounts, a real 1:1 call over the Session network. The app was driven through the Chrome DevTools Protocol and the main process through the Node inspector, so the numbers below are readings.
BrowserWindow.isFullScreen(): true,getSize(): [1920, 1200]— the whole display.<video>became 1920×1200 with real pixel content while sharing; after stopping, a flat frame (min = max = 1), i.e. the peer genuinely stops seeing the screen."fullscreen":truepersisted across the session despite the window really being full screen several times.isVisible: falsetotrue; and with one account on screen and another's window hidden, a message sent to the hidden one arrived (unreadCount: 1).pnpm buildexit 0,pnpm lintclean,pnpm test961 passing / 0 failing (953 before).Full results, including what was not verified, are in
docs/TDD-call-fullscreen-and-screenshare.md§5 anddocs/TDD-multi-account.md§5.Known gaps, stated plainly
NODE_APP_INSTANCEis forced empty) is handled explicitly in the design but unverified.TODO(l10n), becausesession-localizationis a separate repository.Things a reviewer will want to decide
TODO(l10n)becausesession-localizationis a separate repository. Happy to open the matching PR there and swap them fortr()tokens — I just did not want to guess token names.sendVideoStatusViaDataChannelgained ascreenSharefield. It is additive and clients that don't know it simply readvideoas before, but it is a wire-format change and worth a look.NODE_APP_INSTANCE !== 0no longer skips the quit-on-failed-lock path. This was the only way two processes could open one account's database once accounts have their own directories, but it does mean developers can no longer run two copies of the same instance name.