Skip to content

refactor: the two-feed limit, the compositor's silent third-feed drop, and how many streams the wall sustains #261

Description

@BernardJen

Split out of #242, #244 and #245, none of which mentions it.

What

The app can show exactly two capture feeds. Three open features each need more than two, and
each is written as though the feeds were the easy part. This is the work underneath all three, and
it is almost certainly larger than any of them.

Current state

Two <video> elements, #left-video and #right-video
(index.html:17 and
:41), and "left or right" is not a parameter —
it is the shape of the renderer:

Count
state fields keyed by side 10 (leftDeviceId/rightDeviceId, leftStream, leftAudioGain, leftAudioSource, leftVolume, and their right-hand twins)
elements entries keyed by side 10 (leftFeed, leftVideo, leftBorder, leftInputList, leftLabel, ×2)
functions taking a side argument 9
side === 'left' ? a : b ternaries 15
objects literally shaped { left, right } 3 (mockStreams, noSignalBoards, swapToken)

getUniqueActiveDevices() hand-enumerates left then right rather than iterating feeds, so
detection, the no-signal overlays, the split-flap boards and the swap-fade tokens all inherit the
same two-slot shape.

The GPU compositor is a partial head start, with a trap in it

gpu-compositor.js already takes an array — draw(feeds), each with its own offset and scale
in normalised target space (gpu-compositor.js:139).
That is the right shape and it did not have to be.

But it allocates exactly two uniform buffers:

const uniforms = [0, 1].map(() => device.createBuffer({ size: 32, ... }))

and then, per feed:

if (i >= uniforms.length) return

A third feed is silently dropped. No error, no warning — it just does not draw. Anyone wiring
up multi-view against this compositor would get two panels working and a third that is black for no
stated reason. Worth fixing on its own merits even if none of the three features are built:
either allocate from feeds.length or log what was discarded.

Note this is behind gpuCompositing in settings and off by default, so the CSS path is the one
that actually runs on the wall today.

What is genuinely unknown

Whether the hardware can do it at all. Four simultaneous getUserMedia streams from four
capture devices is not obviously fine: capture cards sharing a USB controller contend for
bandwidth, and several will renegotiate to a lower resolution or drop frames rather than fail
loudly. Nobody has tried it here. This should be measured before any of #242/#244/#245 is designed
around it, because the answer could change what they look like — a thumbnails panel that has to
poll inputs one at a time is a different feature from one showing four live streams.

#248 makes that measurement cheap on the software side. --mock=8 synthesises eight inputs as
real MediaStreams from canvas.captureStream(), so the renderer work can be built and driven
with no capture hardware present. It cannot answer the USB-bandwidth question, which needs real
cards, but it separates "does our code handle N feeds" from "does the hardware".

Why this is one issue rather than three

#242 (thumbnails panel), #244 (multi-view) and #245 (picture-in-picture) are genuinely different
features — a transient picker, a per-panel layout, and a persistent inset. They are not duplicates
and this is not a proposal to merge them.

They just share one dependency, and doing it three times would produce three incompatible ways to
hold more than two feeds. #243 (auto-switch) is orthogonal and does not need this.

Acceptance criteria

  • Feeds held in a collection rather than as left/right fields, with the count not fixed in
    the markup
  • Detection, no-signal overlays, split-flap boards and the swap-fade tokens all derive from that
    collection instead of enumerating two sides
  • D/S and the existing dual/single behaviour unchanged — this is a refactor with no
    user-visible change on its own
  • The WebGPU compositor allocates uniforms from the feed count, or logs what it drops
  • Measured: how many simultaneous real capture streams the wall's host actually sustains, and
    what happens at the limit — recorded here
  • Verified with --mock=8 that the renderer handles more feeds than there are number keys
  • Audio mixing decided for more than two feeds, or explicitly deferred with the reason

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions