<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/video-player.js, branch main</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=main</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-20T22:43:14Z</updated>
<entry>
<title>fix(hub): keep the video read-ahead across a reconnection</title>
<updated>2026-09-20T22:43:14Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-20T22:43:14Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=90e26beff212e4994b8e4f18c8260f2aa8a9b40c'/>
<id>urn:sha1:90e26beff212e4994b8e4f18c8260f2aa8a9b40c</id>
<content type='text'>
A reconnection restarted the stream at the playhead, and that empties the
source buffer — spending the whole read-ahead at the one moment it was the
thing carrying the film. It carries on from the end of the buffer instead,
except where the buffer is short, the stream already ended, or a cast is
active: the receiver is fed from the wire and never had that buffer, so
resuming there would skip it forward by the lot.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>perf(hub): bound video read-ahead by bytes, not by seconds</title>
<updated>2026-09-20T22:21:50Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-20T22:21:50Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f7d33c299ae0d0c4f406779b8f5324d80637adc8'/>
<id>urn:sha1:f7d33c299ae0d0c4f406779b8f5324d80637adc8</id>
<content type='text'>
A browser limits bytes, so a bound in seconds had to be sized for the
highest-bitrate file and every ordinary one then held a fraction of what
the same buffer would have taken. Floored at the old 90 s so nothing
pulls less than before, and walked up rather than declared. A refused
append now waits for an eviction instead of retrying on every tick.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(hub): a reconnect re-reads the index, not just the handshake</title>
<updated>2026-09-19T15:13:17Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T15:13:17Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2eaf6887295614509f8d0bc24a9b77ccd915ef88'/>
<id>urn:sha1:2eaf6887295614509f8d0bc24a9b77ccd915ef88</id>
<content type='text'>
_reconnectLoop re-did the handshake and nothing else, so a page kept
whatever it last saw until someone reloaded it. That is invisible until
the node restarts: it rebuilds its index from index_cache.db, which
holds no enrichment, and Music is the one app whose enrichment is
persisted nowhere — for the length of the re-read pass it serves tracks
with no artist, and the album grid drew nothing.

onReconnected was one slot the video player took on open and cleared on
close; it is a listener set now, and carries the fresh ack.

docs/MESHBAY_DESIGN.md §15.3 records the two defects found alongside and
not fixed here.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(cast): carry subtitles to a Chromecast, on the relay's clock</title>
<updated>2026-09-18T07:49:27Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-18T07:47:01Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c03512aeab576a06f8d5026e5eb484897ec45f99'/>
<id>urn:sha1:c03512aeab576a06f8d5026e5eb484897ec45f99</id>
<content type='text'>
The relay forwards the node's fragments untouched, and those begin at zero
at the seek point. The player never notices because its SourceBuffer is given
`timestampOffset = start`; a receiver has no equivalent, so the cues are
shifted by `-start` before they leave, recomputed at every restart of the
relay. Sent as they are, a subtitle would be out by the whole seek.

The document is served from the relay's own port at /subs.vtt, behind the same
token as the stream and with CORS: a receiver fetches a side-loaded track with
XHR from its own origin, and without the headers it fails as a network error
with nothing on screen to say so. The URL carries a version because a track is
cached by address — changing the cues behind a fixed URL leaves the previous
language showing.

Cues that end before the stream begins are dropped rather than clamped, so a
line from before the seek cannot appear over the first frames after it.

The relay is plain Node, so the tests start it and fetch from it rather than
reading its source.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>feat: tell a forced subtitle track from a full one</title>
<updated>2026-09-17T13:06:57Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T13:06:57Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d472725167c1d335996059d24b7b74f3730768fb'/>
<id>urn:sha1:d472725167c1d335996059d24b7b74f3730768fb</id>
<content type='text'>
Reported as "I click a subtitle and nothing appears", on three films. Nothing
was broken. The track selected was the container's forced track, which carries
signage and foreign dialogue only: measured on the film in question, 30 cues
and 77 seconds of text across 2h32 — 0.8% of the running time, against 1559
cues and 41.8% for the full track sitting beside it under the same language
tag. At all three positions tested there was genuinely no cue to show; the
full track would have shown one at two of them.

So the defect is that the menu could not say which was which. The label used
the container's title tag, which said "Forced" on that film and says nothing
at all on most, and no other field was carried. The disposition is the half
that is always there: `probe_video` now reads `forced` and `hearing_impaired`,
`stream_init` carries them, and the label states them in the reader's own
language rather than repeating an English word a muxer happened to type.

The node fixture grows a forced track with no title, because a title would
let the old code pass. The label harness's `t` stub took a parameters object
unconditionally and threw on a key that has none — a fixture narrower than
production, fixed here rather than worked around.

Also removes the activeCues probe that found this. It answered its question:
mode showing, cues 30, active 0, none due at that instant.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>fix(hub): sample activeCues, the number that decides whether a subtitle shows</title>
<updated>2026-09-17T12:44:57Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T12:44:57Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=30980ba0994d3e99d41c5252555320d6cbb1f5a9'/>
<id>urn:sha1:30980ba0994d3e99d41c5252555320d6cbb1f5a9</id>
<content type='text'>
Attach-time state — one track, showing, cues parsed — was all correct while
nothing appeared on screen, so it was measuring the wrong thing. A re-render
can replace the &lt;track&gt; and reset a mode nothing sets again, and a cue list
that does not cover the playhead looks identical to one that does.

The probe samples activeCues for ten seconds alongside the playhead, the mode,
and the cue that ought to be on screen at that instant, so "no cue is active"
and "a cue is active and is not painted" stop being the same observation.

Verified beforehand that the mechanism itself is sound: a real Chrome driven
over CDP, fed through MediaSource with timestampOffset 2690 and given a track
appended after playback started, reports activeCues 1 on the cue bracketing
the playhead.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>fix(hub): trace the subtitle path end to end in the player</title>
<updated>2026-09-17T12:36:25Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T12:36:25Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=4134729543139f4934c4ec4e4c6a2cda0606bcdf'/>
<id>urn:sha1:4134729543139f4934c4ec4e4c6a2cda0606bcdf</id>
<content type='text'>
A subtitle button that spins for ever and never shows a track could not be
told apart from a node that answered, a blob that never arrived, or a track
attached with no cues in it: the whole path was silent. Every step of it
happens on someone else's machine, over a link, against a file that may be
gigabytes, so the only question worth asking when it does not finish is which
step did not — and nothing recorded that.

It now logs the request, the node's answer with hash, size and elapsed time,
the fetched blob and its chunk count, the attachment, and the live TextTrack's
mode and cue count. A failure says how long it took before failing.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>docs(hub): say what the subtitle track's mode effect was measured to do</title>
<updated>2026-09-17T11:41:57Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T11:41:57Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=36401ac2038c03c78f6f5bfea90d05f7493ccd0c'/>
<id>urn:sha1:36401ac2038c03c78f6f5bfea90d05f7493ccd0c</id>
<content type='text'>
The comment claimed a &lt;track&gt; appended after playback started is not shown by
its `default` attribute. Chrome shows it: in a headless run the TextTrack read
back "showing" before the effect had touched it, with its cues already parsed.

The effect stays, for the two reasons that are true — `default` says nothing
about turning subtitles off again, and an explicitly assigned mode means the
same thing in every engine — but the reason written beside it was not one of
them.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>feat: embedded subtitles in the video player (MNP 3.3)</title>
<updated>2026-09-17T11:39:23Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T11:39:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ad4ca3229002997934ccb5b2eaeb553c13b8888f'/>
<id>urn:sha1:ad4ca3229002997934ccb5b2eaeb553c13b8888f</id>
<content type='text'>
MSE decodes no in-band text track, so a subtitle cannot ride inside the
fragmented MP4 the player is fed. The node extracts one track whole, converts
it to WebVTT and caches it under its own hash; the client pulls that blob
through the ordinary file_req/chunk path and hangs a &lt;track&gt; on the video
element — the same indirection as a TMDB poster or an audio transcode, which
is what makes a film's subtitles extracted once in the life of the file rather
than once per viewing. Whole-file also makes the cues absolute, so a seek and
an audio-language change both leave the track untouched.

**The ordinal counts every subtitle stream, including the ones never listed.**
Only text codecs are offered: a bitmap track (PGS, VOBSUB — about a fifth of a
real library) has no path to WebVTT without OCR, and one extracted anyway
yields a header with no cues, which is a menu entry that shows nothing and
reports no error. Numbering the survivors of that filter would give a
PGS/SRT/SRT file the ordinals 0 and 1 for its text tracks and `-map 0:s:0`
would then extract the PGS — the same trap `AudioTrack.ordinal` exists for,
one level deeper. A fixture whose first subtitle stream cannot be decoded pins
it, and the handler checks membership of the probed list, never a range.

Additive and MINOR: the selector is drawn from `subtitle_tracks` in the node's
own `stream_init` and from no version number, so `subtitle_req` is never sent
to a peer that would not answer it. The floor stays at 3.0.

Also here: a failed extraction never touches playback, a superseded reply
cannot install its blob over a newer choice, and `_languageName` is shared
with the audio labels — lifted by both label harnesses, since a lift that
names one function stops covering the rule the moment logic moves out of it.

Tests: 9 node (tracks told apart by the words in the extracted cues, not by
tags), 10 client. Full suite green: 1545 node/common, 1252 hub.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
<entry>
<title>feat(hub): give the audio-language button a glyph of its own</title>
<updated>2026-09-17T10:56:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-17T10:56:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3e6d514663a5df1be3b2f0286c5f67f669d9c1d6'/>
<id>urn:sha1:3e6d514663a5df1be3b2f0286c5f67f669d9c1d6</id>
<content type='text'>
The audio-track selector was drawn with `volume` — the same glyph the music
player uses for its level control, and the same one the browser draws a few
dozen pixels below it, since the video player mounts `&lt;video controls&gt;` and
gets the native volume speaker with it. One drawing, two meanings, in one
window.

`speech-pair` is two offset bubbles: the same line, said twice, which is what
the button chooses. Nothing crosses anything — the set is stroked with no
fill, and two overlapping outlines read as a tangle at 20 px — and it leaves
the conventional subtitle glyph free for the button planned beside it.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
</content>
</entry>
</feed>
