The repeated-subtitle pause leak was only fixed for ordinary subtitle changes. Startup autoplay priming and visible-overlay priming pause the same way and also ignored whether any tokenization was scheduled, so a cache miss on text the controller already holds (mining a card while the line is on screen) left prefetching idle for the rest of the cue. Pause and release are now one operation via pausePrefetchUntilEmit, and both controller entry points report whether an emit is expected. A repeat arriving while a run is already in flight keeps the pause, since that run still emits. Also invalidate the character dictionary lookups centrally from the sync completion handler instead of at three manager call sites. Ordinary selection sync never invalidated them, so a stale non-null name candidate list could skip a newly added name for up to five seconds; a missing list falls back to the exhaustive scan, but a stale one does not. Ordering matters: the invalidation runs before the subtitle refreshes so they re-tokenize against the new dictionary content. Docs: subtitle-overlay-priming no longer claims every subtitle change calls onSeek().
8.9 KiB
Subtitle Overlay Priming
Status: active Last verified: 2026-08-04 Owner: Kyle Yasuda Read when: debugging subtitle state or blank Linux/X11 overlay windows when the visible overlay is shown or recreated
Visible-overlay subtitle priming fills the overlay from mpv's current subtitle properties before waiting for the next live mpv subtitle event. This avoids a stale or blank overlay when the user manually shows the visible overlay while playback is already sitting on a subtitle.
On Linux/X11, visible-overlay show and later mpv bounds refreshes restore the Electron window shape
to the full current overlay bounds. Electron's BrowserWindow.setShape() applies a bounding shape,
not an input-only region; stale shapes can leave a mapped 1920x1080 overlay with smaller X11 shape
extents such as 800x600+0+0, so renderer and websocket subtitle state are correct while bottom
subtitles do not draw.
Entry Points
src/main.tscallsprimeCurrentSubtitleForVisibleOverlay()when manual visible-overlay show paths run.src/main.tscallsrestoreVisibleOverlayWindowShapeForShow()before visible-overlay show actions on Linux, andresetVisibleOverlayInputState()restores a full shape instead of applying an empty shape.src/main.tsalso restores the Linux/X11 shape after applying mpv overlay bounds, so a newly created 800x600 hidden Electron window cannot keep clipping after it is resized to mpv geometry.primeCurrentSubtitleForVisibleOverlay()delegates toprimeVisibleOverlaySubtitleFromMpv()insrc/main/runtime/current-subtitle-snapshot.ts.restoreVisibleOverlayWindowShapeForShow()delegates torestoreLinuxOverlayWindowShape()insrc/main/runtime/linux-overlay-window-shape.ts.- Inputs are callback deps, not globals:
getMpvClient,setCurrentSubText,getCurrentSubtitleData,consumeCachedSubtitle,onSubtitleChange,refreshCurrentSubtitle,emitSubtitle, optional secondary-subtitle callbacks, andlogDebug.
Primary Subtitle Flow
- Read the connected mpv client through
getMpvClient(). Exit if no connected client. - Request mpv
sub-text. On failure, log a[visible-overlay-subtitle-prime] failed to read sub-textdebug line and exit. - Normalize non-string
sub-textto'', then callsetCurrentSubText(text)so app state matches mpv before any overlay emission. - Empty text: call
onSubtitleChange(text), emit{ text, tokens: null }, then prime secondary subtitles. - Current cached payload: if
getCurrentSubtitleData()?.text === text, callemitSubtitle(payload)andrefreshCurrentSubtitle(text), then prime secondary subtitles. - Tokenization cache hit: call
consumeCachedSubtitle(text),onSubtitleChange(text), andemitSubtitle(cachedPayload), then prime secondary subtitles. - Cache miss: call
refreshCurrentSubtitle(text). Normal processing emits a plain payload synchronously, then replaces it with the tokenized payload when ready.
Both onSubtitleChange and refreshCurrentSubtitle pause subtitlePrefetchService and then call
the matching subtitleProcessingController method, giving the visible overlay priority over
background prefetch work. Prefetch is not re-centered here: restarting the run per line
(onSeek) discarded the in-flight tokenization every time the subtitle changed, so only real
seeks restart it (see onTimePosUpdate in src/main.ts).
The pause is released by the emit that carries the tokenized payload. Both controller methods return whether an emit is expected, and the caller resumes immediately when it is not — otherwise a repeated subtitle (which schedules no work) would leave prefetching idle for the rest of the cue.
Live Cue Delivery
- A tokenization cache miss emits the plain cue synchronously. Tokenization remains serialized so live work does not contend for Yomitan state.
- If a newer cue arrives while an older line is still tokenizing, the newer plain cue or empty clear payload is emitted immediately. The older tokenization result is dropped before it can replace the current cue.
- The current cue upgrades in place when its tokens and annotations are ready. This can reflow text or character images, but cue visibility does not wait for that work.
Emitted State
emitSubtitle(payload)maps toemitSubtitlePayload(payload). Overlay windows and annotation websocket listeners receive both the immediate plain cue and its later annotation upgrade.- The basic subtitle websocket receives the immediate plain cue only. Because its serialized payload discards annotations, the later upgrade would be an identical duplicate and is skipped when text and cue timing match.
- Secondary priming reads mpv
secondary-sub-text, stores it inmpvClient.currentSecondarySubText, and broadcastssecondary-subtitle:setto overlay windows. - If secondary
requestPropertyfails, the primary flow stays complete and only a debug line is written.
Startup Ready Release
mpv.pauseUntilOverlayReadywaits for tokenization warmup plus visible-overlay readiness before releasing the mpv startup gate.- Visible-overlay startup creates the tray and visible overlay shell before tokenization and
annotation warmups continue. Cold
--start --background --managed-playbacklaunches still handle initial args before the deferred Yomitan wait. - Overlay-routed startup notifications are queued in the main process until an overlay window has finished loading. Progress notifications with the same id are upserted so spinner ticks do not flood a cold-start overlay, while events with distinct history ids are retained for phase-level history such as character dictionary checking/building/importing.
- The mpv plugin has a 30-second fallback for cold starts; app-side retry/release budgets match that window so readiness can still arrive before fallback resumes playback.
- If mpv is already on a subtitle, SubMiner still prefers the resolved current subtitle payload and waits for a fresh measured subtitle rectangle before signaling readiness.
- If the startup subtitle has no cached annotations yet, autoplay priming emits a raw first-paint subtitle payload before background tokenization. The tokenized payload replaces it when ready, but the visible overlay can paint and measure the line before the mpv startup gate resumes playback.
- If startup
sub-textis temporarily empty, autoplay priming refreshes the active subtitle source and then awaits cue-based priming before synthetic warm readiness can proceed. A parsed current or imminent cue is treated as the startup subtitle so the visible overlay can paint and measure it before playback resumes. - If mpv is before the first subtitle, SubMiner sends a synthetic warm readiness payload after tokenization warmup and visible overlay content-ready. This releases playback without waiting for a later subtitle event that cannot happen while mpv is paused.
- After a synthetic warm readiness release, SubMiner briefly polls/refreshes the current subtitle
again. This covers Linux/mpv startup cases where
sub-textis still empty while paused but becomes available right after playback resumes, without waiting for the next subtitle property change.
Linux/X11 Window Shape
restoreLinuxOverlayWindowShape()readsBrowserWindow.getBounds()and callssetShape()with one full-window rectangle:{ x: 0, y: 0, width, height }.- Restore the shape after
setBounds()/mpv geometry updates, not only before showing the overlay. Manual startup can create the hidden overlay at Electron's default 800x600 size before the window tracker applies the real mpv bounds. - Do not use
setShape([])as a passive reset for the visible overlay. On the tested X11/XWayland path, empty or stale bounding shapes produced invisible or clipped subtitles even though the overlay window remained mapped above mpv. - Pointer pass-through should continue to use
setIgnoreMouseEvents(true, { forward: true })and the Linux cursor-poll fallback, not bounding-shape clipping. - Visible-overlay show/reset marks Linux pointer passthrough state dirty even when the logical
interaction state is already inactive. The next cursor-poll tick must still reapply
setIgnoreMouseEvents(true, { forward: true }); otherwise a newly shown Electron overlay can keep full-window input capture and block both mpv and overlay controls before the first subtitle measurement. - Visible-overlay show also starts a short Linux input grace before the first content measurement.
Native Wayland surfaces can become inert while
setIgnoreMouseEvents(true)is active; keeping the overlay interactive during this startup gap lets notifications and overlay mouse bindings work until subtitle/sidebar/notification rectangles are reported.
Config And Migration
No config or schema migration. This workflow reuses existing mpv properties, overlay IPC events, subtitle tokenization cache, and prefetch controls.