Two things, both about playing several videos at once.
Capping the resolution per panel wasn't enough, because pixel count isn't
the only cost. A split panel now also prefers a progressive file over HLS
-- every HLS panel runs its own JavaScript demuxer over every segment, so
four panels means four media pipelines doing work a plain MP4 skips
entirely -- and H.264 over AV1 or VP9, which are often decoded in software
and are a cliff rather than a gradient, and 30fps over 60. The height
ceiling still comes first, so cheapness cannot argue a panel into a bigger
picture than it should have, and every format stays reachable as fallback.
The preloaded step's hls.js instances now park after buffering one
fragment and resume when the reader swipes to them, instead of fetching
and demuxing ahead for a step nobody reached.
Auto picture-in-picture had been implemented since the custom player was
written and had never worked. requestPictureInPicture() from a
visibilitychange handler carries no user activation, browsers refuse those,
and .catch(() => {}) swallowed the refusal -- so it failed silently every
time, in the reels feed and the standalone player alike. The declarative
autoPictureInPicture attribute is the form made for this: the browser is
told in advance which video should follow the reader out. The imperative
call stays as a fallback.
With panels there are several candidates and only one window, so binding
every pane made them race for it. The feed picks one deliberately -- the
panel you can hear, or the first if they are all muted -- re-picks when the
step or a mute switch changes, and releases it on close.
Whether a window actually opens is browser policy, not ours: Safari honours
the attribute, Chrome honours it for installed apps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
Four panels stutter, and the reason isn't scheduling: a quarter-screen
panel was still being handed a full-screen stream. Decoding 1080p into a
quarter of the screen costs exactly what decoding it full size costs, and
four of those at once is past what most GPUs will decode in hardware --
after which it falls back to software and the wheels come off.
So a split panel now caps by its own height in device pixels, rounded up
to the next standard rendition, and hls.js is told the same thing through
capLevelToPlayerSize since an adaptive stream picks its own. Four panels
on a 1080p screen land near 480p each: roughly a quarter of the pixels to
decode. Its buffers shrink too -- several instances each holding a minute
of video is memory and demuxing for footage nobody has reached.
The preloaded step keeps its guarantee but gets cheaper with it: those
panes use preload=metadata rather than auto, so every panel still has its
next video ready to start instantly without four more streams competing
for bandwidth with the four being watched.
The floors that make that preload guarantee hold -- one step, in both
windowBounds and preloadAhead -- now say so. Both are divided by the pane
count, and dropping either below one would leave a panel with nothing
buffered to swipe to.
Two tests. tests/unit_formats.js runs the rendition maths in node with no
browser, server or network, in under a second: picking a format is a list
in and a URL out, and it is the cheapest thing in the repo to assert.
tests/smoke_reels.py covers the panels themselves -- splitting, nesting,
controls staying inside short panes, one swipe advancing every panel,
per-panel audio surviving re-activation, and the preload guarantee.
What none of this establishes is whether four streams now play smoothly
on real hardware. Headless Chromium has no GPU decode, so it cannot say.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd