Members
(static, constant) PLAYBACK_INFO_REFRESH_SECONDS
How often the playback report re-reads the session while it is open.
5s rather than the 10s Jellyfin's web client uses, because that client's panel is a cache TTL on an always-open overlay while this is a dialog someone opened deliberately, usually because something looked wrong. The figure that matters — transcode speed — is a level to be confirmed, and halving the wait for the first confirmation is the whole benefit.
TWO GATES keep that from becoming an open-ended poll, and neither invents a number:
- The timer only STARTS when the report has live rows — i.e. a transcode is running. On direct play nothing in the report can change, so it never polls at all, which is most sessions.
- A tick makes no request once the device has been idle longer than the screensaver timeout THE USER CONFIGURED. That is their own statement about how long counts as away; a device with the screensaver switched off has said the opposite, and is never suspended.
Gate 2 is an IDLE threshold, not a claim about what is on screen. Roku suppresses the screensaver during video playback, so it may never actually appear here — the reason to lean on that number anyway is that the user picked it, not that the screen is covered.
Being wrong is cheap in exactly one direction, which is why the threshold can be loose: TimeSinceLastKeypress resets itself, so the first tick after any keypress fetches again — at most 5s of staleness, no state, no resume path, no key handling. Polling forever has no such recovery.
- Default Value
- 5
Methods
(static) destroyPlayer() → {void}
Tear down the current player child (stop -> report stop to Jellyfin -> onDestroy -> remove).
- Type:
- void
(static) exitPlayback() → {void}
Leave the play route: pops this host off the router history (beforeViewClose -> onScreenHidden + onDestroy tear the player down) and resumes the view beneath (the item's details, or Home). Called on queue-exhaustion, playback error, and voice "stop". Replaces ViewCreator/VideoPlayerView's SceneManager.popScene.
- Type:
- void
(static) handleFocus(_dataopt) → {boolean}
Router: take remote focus. Hand it to the player child if mounted; the player also grabs focus itself once content loads (VideoPlayerView.onVideoContentLoaded).
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
_data | object | <optional> | {} |
- Type:
- boolean
(static) handleTransport(evt) → {object}
handleTransport: voice transport delegate. main.bs forwards transport commands here (the router-active view is this host, not the inner player); forward to the child player's own handler.
| Name | Type | Description |
|---|---|---|
evt | object |
- Type:
- object
(static) init() → {void}
- Type:
- void
(static) mountPlayer() → {void}
Build + mount the VideoPlayerView as a child of this host. Ported from ViewCreator.CreateVideoPlayerView; the only behavioral change is appendChild to the host instead of SceneManager.pushScene.
- Type:
- void
(static) onAudioTrackSelected() → {void}
Audio track selection handler
- Type:
- void
(static) onDestroy() → {void}
Router: this view is being destroyed (goBack / sign-out resetRouter). Tear down the player child. abandonApiPromises() is injected here by the auto-abandon-promises BSC plugin (JRScreen.onDestroy's floor doesn't chain to SG-component overrides).
- Type:
- void
(static) onPlaybackInfoDialogClosed() → {void}
The dialog closed — stop polling into it and let it go.
It reports that on its own closed field. It USED to be observed through parentNode, on the reasoning that a dialog which dismisses itself by removing itself from the scene has a detached parent — but parentNode is not a field on Node (its observable fields are id, focusable, focusedChild and change), so that observer never fired even once. Nothing cleared m.playbackDialog, so the NEXT press of the "i" button found a dialog it believed was still open and quietly refreshed a node that was no longer in the scene: the report opened the first time and never again.
- Type:
- void
(static) onPlaybackInfoLoaded() → {void}
The session arrived. Compose the report and either open it or refresh the one already open.
- Type:
- void
(static) onPlaybackInfoRefreshDue() → {void}
- Type:
- void
(static) onPlayerStateChange() → {void}
=========================================================================== End-of-playback / queue advancement state machine. Ported from ViewCreator.onStateChange; the pop/push has become host-internal destroy/remount, and the queue-exhausted pop has become exitPlayback (goBack).
- Type:
- void
(static) onScreenHidden() → {void}
- Type:
- void
(static) onScreenShown() → {void}
Router lifecycle (via JRScreen's onViewOpen bridge): mount the player for the current queue item. The launching code (QueueManager.playQueue -> JRScene) has already built the queue, so the host just reads getCurrentItem. Guard against a redundant call (e.g. screensaver-exit re-fires onScreenShown).
- Type:
- void
(static) onScreensaverTimeoutLoaded() → {void}
- Type:
- void
(static) onSelectAudioPressed() → {void}
onSelectAudioPressed: Display audio selection dialog
- Type:
- void
(static) onSelectPlaybackInfoPressed() → {void}
User requested playback info.
Always re-fetches. The report used to be built once by the task and cached for the life of the player, which meant a DoVi buffer-overflow fallback (transcode -> direct play) left the "i" button confidently describing a transcode that had already stopped. Composition is cheap now that it happens here rather than on the Task thread, so a fresh answer costs one request.
- Type:
- void
(static) onSelectSubtitlePressed() → {void}
User requested subtitle selection popup
- Type:
- void
(static) onSelectVideoSourcePressed() → {void}
onSelectVideoSourcePressed: Display video source selection dialog
- Type:
- void
(static) onSubtitleTrackSelected() → {void}
Subtitle selection handler. selected is { index, isEncoded, trackName } — the "None" row is the one whose index is SubtitleSelection.NONE, which is why this no longer compares the row's LABEL against the literal string "none" (that comparison is why the None row could never be translated).
- Type:
- void
(static) onVideoSourceSelected() → {void}
Video source selection handler. Writing mediaSourceId triggers a video reload, so only write it when the source actually changed.
- Type:
- void
(static) playCurrentQueueItem() → {void}
Host-internal advancement: remount the player for the CURRENT queue position. Callers (onPlayerStateChange next-item / Live TV restart; VideoPlayerView channel switch) set the position + backdrop first. Replaces ViewCreator's clearPreviousScene + playQueue (which created a new pushed scene).
- Type:
- void
(static) playbackReportInputs(session) → {object}
Everything the pure report builder needs, gathered on the RENDER thread where an m.global read costs ~2 µs rather than the ~93 µs it cost inside the task.
| Name | Type | Description |
|---|---|---|
session | dynamic |
- Type:
- object
(static) releasePlaybackInfoRefresh() → {void}
Stop the timer AND let it go. Kept distinct from stopPlaybackInfoRefresh because closing the dialog only pauses polling — the same timer is reused the next time the report is opened — while onDestroy has to release the observer.
- Type:
- void
(static) reportHasLiveRows(report) → {boolean}
True when the report contains something that can change while it is open.
Only the transcode section is live (speed, progress, output bitrate); every other row is fixed for the session and would re-resolve to identical text. A direct-play report therefore has nothing to poll FOR, which is the honest reason not to poll rather than a budget someone picked.
| Name | Type | Description |
|---|---|---|
report | object |
- Type:
- boolean
(static) reportNeedsPolling(report, session) → {boolean}
True when the report still has an unanswered question — either something that changes while you watch it, or something we have not been told yet.
Two ways to qualify, and the second is what makes a failed first fetch recoverable. Live rows exist only during a transcode. A MISSING SESSION means the whole transcode half of the report is unknown rather than absent, so the dialog is showing a provisional answer and has to keep asking; the first tick that returns a session fills in the status line and the transcode section, and if that session turns out to be a direct play this same gate then stops the timer on the next pass.
A direct play with a session in hand qualifies as neither, which is the case that must never poll — and it is most sessions.
| Name | Type | Description |
|---|---|---|
report | object | |
session | dynamic |
- Type:
- boolean
(static) resolvePickedValue() → {object}
The option the user picked, or invalid if they backed out. Clears the dialog slot — every result handler starts here.
- Type:
- object
(static) showTrackPicker(title, options, onResult) → {void}
Present a picker built by source/utils/trackPickerOptions.bs. Holding the option set is what makes the result usable: JRListDialog answers with an INDEX, and values is what that index means.
| Name | Type | Description |
|---|---|---|
title | string | |
options | object | |
onResult | string |
- Type:
- void
(static) startPlaybackInfoRefresh() → {void}
- Type:
- void
(static) stopPlaybackInfoRefresh() → {void}
- Type:
- void
(static) teardownForDeepLink() → {void}
A deep link arrived while this player is active. Stop + report the player NOW (synchronously) WITHOUT navigating — the caller (JRScene.replayDeepLinkReplacingPlayer) then goBacks, whose beforeViewClose -> onDestroy does the full teardown. destroyPlayer is idempotent (guards m.view), so that second teardown is a safe no-op — NOT a full onDestroy here, which would run twice and double-tear-down.
- Type:
- void
(static) userIsAdministrator() → {boolean}
True only when the signed-in user is a Jellyfin administrator.
Every hop is checked because this decides whether a filesystem path is shown: the failure mode of a wrong true is disclosing something, and the failure mode of a wrong false is one missing row.
- Type:
- boolean