Keep the videos already playing when a panel is split
Splitting or closing a panel changes the stride, which renumbers every step, so the slides are rebuilt -- and the rebuild threw away the panes along with them. Every video on screen was fetched and buffered again to show what it was already showing. The panes are now set aside by video id first, and whoever needs that video next takes the pane that already has it: the same element, the same stream, the frames it had already decoded. It all happens in one go without yielding, so a pane is never out of the document long enough to be paused for it. Panes the new layout has no place for are torn down as before -- they are holding a stream open. Pane tools are rebindable for the same reason: a reused pane comes back under a different node in the tree, and its old buttons would otherwise split and close the one it left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
@@ -653,7 +653,7 @@ App.feed = App.feed || {};
|
||||
// around whatever video is playing so the reader keeps their place.
|
||||
const rebuildLayout = function() {
|
||||
const keepId = state.feedActiveVideoId;
|
||||
teardownAllSlides();
|
||||
reserveSlides();
|
||||
const videos = state.loadedVideos || [];
|
||||
let videoIndex = 0;
|
||||
if (keepId != null) {
|
||||
@@ -663,23 +663,66 @@ App.feed = App.feed || {};
|
||||
state.feedActiveIndex = -1;
|
||||
const step = Math.floor(videoIndex / paneCount());
|
||||
setActive(step);
|
||||
releaseReserve();
|
||||
const scroller = getScroller();
|
||||
if (scroller) scroller.scrollTop = step * slideHeight();
|
||||
};
|
||||
|
||||
const teardownAllSlides = function() {
|
||||
// ------------------------------------------------------------------
|
||||
// Reusing panes across a layout change
|
||||
//
|
||||
// Splitting or closing a panel changes the stride, so every step is
|
||||
// renumbered and the slides have to be built again -- but the videos
|
||||
// haven't changed. A pane already holds one: its <video> with the bytes it
|
||||
// has buffered, where it had got to, which quality it was told to play.
|
||||
// Building a fresh one throws all of that away and fetches it again, so
|
||||
// the panes are set aside by video id first and whoever needs that video
|
||||
// next takes the pane that already has it.
|
||||
//
|
||||
// Everything happens in one go, without yielding, so a pane never stops
|
||||
// being in the document long enough to be paused for it.
|
||||
// ------------------------------------------------------------------
|
||||
const paneReserve = new Map();
|
||||
|
||||
const reserveSlides = function() {
|
||||
slidesByIndex.forEach((slide) => {
|
||||
teardownSlide(slide);
|
||||
panesOf(slide).forEach((pane) => {
|
||||
const id = slideVideoId(pane);
|
||||
const key = id == null ? null : String(id);
|
||||
if (key !== null && !paneReserve.has(key)) {
|
||||
paneReserve.set(key, pane);
|
||||
if (pane.parentNode) pane.parentNode.removeChild(pane);
|
||||
return;
|
||||
}
|
||||
teardownPane(pane); // a second pane on the same video
|
||||
});
|
||||
slide._panes = [];
|
||||
slide.remove();
|
||||
});
|
||||
slidesByIndex.clear();
|
||||
forgetPeeks();
|
||||
};
|
||||
|
||||
// Whatever the new layout had no place for -- it is holding a stream open.
|
||||
const releaseReserve = function() {
|
||||
paneReserve.forEach(teardownPane);
|
||||
paneReserve.clear();
|
||||
};
|
||||
|
||||
// Everything one video needs on screen. Identical to what a slide used to
|
||||
// hold: the per-video helpers below take any element shaped like this, so a
|
||||
// pane and the old single-video slide are interchangeable to them.
|
||||
const buildPane = function(v, index) {
|
||||
const held = v && v.id != null ? paneReserve.get(String(v.id)) : null;
|
||||
if (held) {
|
||||
paneReserve.delete(String(v.id));
|
||||
// The stride changed, so this video sits at a different place in
|
||||
// the queue than it did; nothing else about the pane has moved.
|
||||
held.dataset.index = String(index);
|
||||
held._index = index;
|
||||
held._videoData = v;
|
||||
return held;
|
||||
}
|
||||
const pane = document.createElement('div');
|
||||
pane.className = v.isLive ? 'feed-pane is-live' : 'feed-pane';
|
||||
pane.dataset.videoId = v.id;
|
||||
@@ -814,21 +857,36 @@ App.feed = App.feed || {};
|
||||
return split.childElementCount ? split : null;
|
||||
};
|
||||
|
||||
// Rebindable, because a pane outlives the tree it was built for: a reused
|
||||
// pane comes back under a different node, and its old buttons would still
|
||||
// be splitting and closing the one it left.
|
||||
const bindPaneTools = function(pane, node) {
|
||||
if (Array.isArray(pane._toolCleanups)) {
|
||||
pane._toolCleanups.forEach((fn) => { try { fn(); } catch (err) { /* ignore */ } });
|
||||
}
|
||||
const cleanups = [];
|
||||
const on = function(selector, handler) {
|
||||
const el = pane.querySelector(selector);
|
||||
if (!el) return el;
|
||||
el.addEventListener('click', handler);
|
||||
cleanups.push(() => el.removeEventListener('click', handler));
|
||||
return el;
|
||||
};
|
||||
const closeBtn = pane.querySelector('.feed-pane-close');
|
||||
closeBtn.hidden = paneCount() <= 1;
|
||||
pane.querySelector('.feed-pane-split-right').addEventListener('click', (event) => {
|
||||
on('.feed-pane-split-right', (event) => {
|
||||
event.stopPropagation();
|
||||
splitPane(node, 'row');
|
||||
});
|
||||
pane.querySelector('.feed-pane-split-down').addEventListener('click', (event) => {
|
||||
on('.feed-pane-split-down', (event) => {
|
||||
event.stopPropagation();
|
||||
splitPane(node, 'col');
|
||||
});
|
||||
closeBtn.addEventListener('click', (event) => {
|
||||
on('.feed-pane-close', (event) => {
|
||||
event.stopPropagation();
|
||||
closePane(node);
|
||||
});
|
||||
pane._toolCleanups = cleanups;
|
||||
};
|
||||
|
||||
// One screenful: the pane tree, filled with this step's run of videos.
|
||||
|
||||
Reference in New Issue
Block a user