Members
(static, constant) MERIDIAN_INFERENCE_HORIZON_SECONDS
How far ahead a reader can still resolve a bare 12h time by comparing it to the clock. Dropping the suffix asserts "the nearest clock face showing these digits", which stops being the time we meant once the meridian comes back around: at 13:00 a 23-hour runtime ends at 12:00 pm — same half of the day, so the suffix rule alone would omit it and "Ends at 12:00" would read as noon today. Past this horizon the suffix always earns its place.
- Default Value
- 43200
- Source
Methods
(static) AssocArrayEqual(Array1, Array2) → {boolean}
| Name | Type | Description |
|---|---|---|
Array1 | object | |
Array2 | object |
- Source
- Type:
- boolean
(static) addOrdinalSuffix(day) → {string}
| Name | Type | Description |
|---|---|---|
day | integer |
- Source
- Type:
- string
(static) appScene() → {object}
The app's Scene node, cached on first use.
Reaching the scene means walking up from m.top, so every caller that needed it had grown its own copy of this three-line cache. m is per-component-scope, so the cache is per component — which is what we want: each component resolves the scene once and reuses it.
- Source
- Type:
- object
(static) arrayHasValue(arr, value) → {boolean}
Check if a specific value is inside of an array
| Name | Type | Description |
|---|---|---|
arr | object | |
value | dynamic |
- Source
- Type:
- boolean
(static) computeProgramBroadcastProgress(playStart, playDuration, nowSeconds) → {float}
Computes a broadcast program's elapsed-time percentage. Programs are live broadcasts, not streamed media — so "progress" is wall-clock elapsed, not UserData.PlayedPercentage. Returns 0 (not 100) for finished programs because JRPoster only cleanly removes the VideoProgressBar child when playedPercentage = 0.
| Name | Type | Description |
|---|---|---|
playStart | integer | epoch seconds when the program started (0 = unknown) |
playDuration | integer | program length in seconds (0 = unknown) |
nowSeconds | integer | current epoch seconds (injected for testability) |
- Source
float in (0, 100) while airing, or 0 when not airing / invalid
- Type:
- float
(static) decimalOrZero(value) → {float}
A number from a field that may be a float, an integer, a string, or absent.
The FLOAT-PRESERVING counterpart to mediaNumber, and the distinction matters in both directions. mediaNumber truncates to a 32-bit Integer, which is right for Height / Channels / SampleRate and wrong for two kinds of field:
- FRACTIONAL ones — frame rates, completion percentages — where truncating destroys the figure outright.
- LARGE ones — a byte count passes 2^31 at ~2.1 GB, and RunTimeTicks for a three-hour film is ~1.08e11 — where truncating overflows.
Every caller today passes a media field, so this covers what actually reaches it in practice — an unhandled type (roSGNode, roDateTime, a function) would still fault on return value as float rather than fall back to 0.0.
| Name | Type | Description |
|---|---|---|
value | dynamic | a numeric field off a media object, a settings node, or a literal |
- Source
- the numeric value, or 0.0 for invalid / AA / array / boolean input.
- Type:
- float
(static) displayToast(message, toastTypeopt) → {void}
displayToast: Display a transient toast notification via the scene-level Toast component.
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
message | string | The message to display | ||
toastType | string | <optional> | "error" | "error", "success", or "info" |
- Source
- Type:
- void
(static) divCeiling(a, b) → {integer}
| Name | Type | Description |
|---|---|---|
a | integer | |
b | integer |
- Source
- Type:
- integer
(static) filterNodeArray(nodeArray, nodeKey, excludeArray) → {object}
Filters an array of nodes, excluding any whose nodeKey value appears in excludeArray
| Name | Type | Description |
|---|---|---|
nodeArray | object | Array of nodes to filter |
nodeKey | string | Field name to check on each node |
excludeArray | object | Array of values to exclude |
- Source
Filtered array with excluded nodes removed
- Type:
- object
(static) findNodeBySubtype(node, subtype) → {dynamic}
| Name | Type | Description |
|---|---|---|
node | dynamic | |
subtype | dynamic |
- Source
- Type:
- dynamic
(static) findParentOfType(node, typeName) → {object}
Traverses up the node hierarchy to find a parent node of a specific type This is useful for finding the main component when handling button events
| Name | Type | Description |
|---|---|---|
node | object | |
typeName | string |
- Source
- Type:
- object
(static) findParentWithField(node, fieldName) → {object}
Traverses up the node hierarchy to find a parent node that has a specific field This is useful for finding the main component when handling button events
| Name | Type | Description |
|---|---|---|
node | object | |
fieldName | string |
- Source
- Type:
- object
(static) formatDisambiguatedTime(time) → {string}
The one way to render a time the reader will read off against an on-screen clock — the "Ends at" value in ItemDetails and in the playback OSD. Bare when the half of the day is inferable, suffixed when it isn't, so the same end time reads identically on both screens.
| Name | Type | Description |
|---|---|---|
time | object | a LOCAL roDateTime (caller must have applied toLocalTime()) |
- Source
- Type:
- string
(static) formatIsoDateVideo(isoDateString) → {string}
convert iso date string to a human readable date string eg. "March 13th, 2014"
| Name | Type | Description |
|---|---|---|
isoDateString | string |
- Source
- Type:
- string
(static) formatTime(time) → {string}
Format time as 12 or 24 hour format based on system clock setting NOTE: This is NOT used by app's clock, only for displaying time in dialogs
| Name | Type | Description |
|---|---|---|
time | dynamic |
- Source
- Type:
- string
(static) getActiveView() → {object}
Returns the full-screen view currently on top — the router-mounted view tracked on m.global.activeRoutedView. The whole app is routed: pre-login, content, and playback are all router views in JRScene's outlet. Used by main.bs's playback/dialog/device code and by JRScene's options key to resolve "what's on screen".
- Source
- Type:
- object
(static) getApiVersionFromGlobal() → {integer}
Returns the current API version from global state with a safe default of 2. Works in both class and non-class contexts (components, namespaces, functions). This is the single source of truth for reading m.global.server.apiVersion.
- Source
API version (default: 2 if not set, unset (0), or invalid)
- Type:
- integer
(static) getButton(msg, subnodeopt) → {object}
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
msg | dynamic | |||
subnode | string | <optional> | "buttons" |
- Source
- Type:
- object
(static) getDisambiguatingTimePeriod(time) → {string}
Returns "am"/"pm" ONLY when the suffix disambiguates the time against the current clock, empty string otherwise (and always empty on a 24h clock).
Why this exists: the clock the reader compares against renders bare in 12h mode — "10:32", never "10:32 pm" (see Clock.bs, used by both the overhang and the playback OSD). A time measured against that clock therefore doesn't need a meridian either, UNLESS it crosses noon or midnight, or sits beyond the horizon above — the cases the reader can't infer at a glance. Only for now-anchored times; see getTimePeriod() for absolute ones.
| Name | Type | Description |
|---|---|---|
time | object | a LOCAL roDateTime (caller must have applied toLocalTime()) |
- Source
- Type:
- string
(static) getFirstAudioStreamIndex(streams) → {integer}
Returns the Jellyfin index of the first audio stream in the streams array
| Name | Type | Description |
|---|---|---|
streams | dynamic | Array of media streams from Jellyfin |
- Source
- Jellyfin index of first audio stream, or 0 if not found
- Type:
- integer
(static) getFirstVideoStream(mediaStreams) → {dynamic}
getFirstVideoStream: Finds the first video stream in MediaStreams array MediaStreams[0] is not always a video stream - it could be subtitle or audio
| Name | Type | Description |
|---|---|---|
mediaStreams | roArray | Array of media streams from playback info |
- Source
First video stream object, or invalid if not found
- Type:
- dynamic
(static) getGlobalSplashScreenSetting() → {string}
getGlobalSplashScreenSetting: Gets the global splash screen setting from registry or default
Reads from global registry first. If not found (user never changed it), reads default from settings.json (single source of truth). Works before user login since global settings are device-wide.
- Source
- Global splash screen setting value ("enabled" or "disabled")
- Type:
- string
(static) getMinutes(ticks) → {integer}
Converts ticks to minutes
| Name | Type | Description |
|---|---|---|
ticks | dynamic |
- Source
- Type:
- integer
(static) getMsgPicker(msg, subnodeopt) → {object}
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
msg | dynamic | |||
subnode | string | <optional> | "" |
- Source
- Type:
- object
(static) getRokuAudioTrackPosition(jellyfinAudioIndex, audioStreamsArray, availableAudioTracksopt) → {string}
Converts a Jellyfin audio stream index to a Roku audioTrack Track identifier. For MKV direct play, Roku exposes tracks using 1-based MKV track numbers that include ALL track types (video, audio, subtitle). Jellyfin's stream Index is 0-based and also counts all track types, so Roku Track = Jellyfin Index + 1.
When availableAudioTracks is provided (populated by Roku during active playback), matches by language with ordinal disambiguation: among same-language Jellyfin streams (in original order), find the position N of the chosen index, then return the Nth same-language Roku track. This avoids returning the wrong track when multiple streams share a language (e.g. main + commentary, both English — issue #500). Falls back to Index + 1 mapping when availableAudioTracks is not available or no ordinal match can be found (e.g., before playback starts or for non-MKV containers).
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
jellyfinAudioIndex | integer | The Jellyfin audio stream index | ||
audioStreamsArray | dynamic | Jellyfin fullAudioData array (must have .index and .language fields) | ||
availableAudioTracks | dynamic | <optional> | invalid | Roku's availableAudioTracks array (has .track and .language) |
- Source
- Roku track identifier string for audioTrack field
- Type:
- string
(static) getTimePeriod(time) → {string}
Returns "am" or "pm" for 12h clock, empty string for 24h. Use for times the reader has no on-screen reference for — a broadcast range that may be hours or days away. For a time anchored to "now", prefer formatDisambiguatedTime().
| Name | Type | Description |
|---|---|---|
time | object |
- Source
- Type:
- string
(static) inArray(haystack, needle) → {boolean}
Search string array for search value. Return if it's found
| Name | Type | Description |
|---|---|---|
haystack | dynamic | |
needle | dynamic |
- Source
- Type:
- boolean
(static) inferServerUrl(url) → {string}
take an incomplete url string and use it to make educated guesses about the complete url. then tests these guesses to see if it can find a jf server returns the url of the server it found, or an empty string
| Name | Type | Description |
|---|---|---|
url | string |
- Source
- Type:
- string
(static) injectApiParams(params, apiVersion, imageDefaults) → {object}
injectApiParams: Pure function to inject default image parameters and version-specific fields Separated from ApiClient for testability without network I/O
| Name | Type | Description |
|---|---|---|
params | object | Original query parameters from caller |
apiVersion | integer | API version (1 for pre-10.9, 2+ for 10.9+) |
imageDefaults | object | Default image parameters (e.g., { EnableImageTypes: "Primary,Backdrop,Logo,Thumb", ImageTypeLimit: 1 }) |
- Source
New params object with defaults merged and version-specific fields added
- Type:
- object
(static) isAllValid(input) → {boolean}
Returns whether or not all items in passed array are valid
| Name | Type | Description |
|---|---|---|
input | object |
- Source
- Type:
- boolean
(static) isBooleanTrue(value) → {boolean}
True only for a real boolean true.
value = true faults &h18 on a non-boolean operand, so the TYPE has to be established before the comparison. A field the server omitted dereferences to Invalid, which is exactly the case a bare if stream.IsAnamorphic would fault on. ParseJson yields an unboxed "Boolean" and an AA literal a boxed "roBoolean" — both are genuine booleans, so both have to pass.
Lives here, beside isValid / mediaNumber / textOrEmpty, because it is the same kind of thing: a coercion for a field that may not be there. It used to live in fallbackFont.bs, which meant reaching it from anywhere else imported a module about FONTS to get a boolean guard — enough friction that the playback report grew a byte-identical private copy rather than pay it.
| Name | Type | Description |
|---|---|---|
value | dynamic | any value, including Invalid |
- Source
- true when value is boolean true; false for every other type or value
- Type:
- boolean
(static) isChainValid(root, propertyPath) → {boolean}
isChainValid: Returns whether or not all the properties in the passed property chain are valid. Stops evaluating at first found false value
| Name | Type | Description |
|---|---|---|
root | dynamic | high-level object to test property chain against |
propertyPath | string | chain of properties under root object to test |
- Source
indicating if all properties in chain are valid
- Type:
- boolean
(static) isJellyfinServer(systemInfo) → {boolean}
accepts the raw json string of /system/info/public and returns a boolean indicating if ProductName is "Jellyfin Server"
| Name | Type | Description |
|---|---|---|
systemInfo | object |
- Source
- Type:
- boolean
(static) isLocalhost(url) → {boolean}
Returns true if the string is a loopback, such as 'localhost' or '127.0.0.1'
| Name | Type | Description |
|---|---|---|
url | string |
- Source
- Type:
- boolean
(static) isNodeEvent(msg, field) → {boolean}
| Name | Type | Description |
|---|---|---|
msg | dynamic | |
field | string |
- Source
- Type:
- boolean
(static) isValid(input) → {boolean}
Returns whether or not passed value is valid
| Name | Type | Description |
|---|---|---|
input | dynamic |
- Source
- Type:
- boolean
(static) isValidAndNotEmpty(input) → {boolean}
Returns whether or not passed value is valid and not empty Accepts a string, or any countable type (arrays and lists)
| Name | Type | Description |
|---|---|---|
input | dynamic |
- Source
- Type:
- boolean
(static) lastFocusedChild(obj) → {object}
Walk to the deepest focused descendant. Dialog helpers capture this before presenting, so closing can restore focus exactly where the user left it (source/utils/dialogs.bs presentOverlayDialog -> returnFocusTo).
| Name | Type | Description |
|---|---|---|
obj | object |
- Source
- Type:
- object
(static) leftPad(base, fill, length) → {string}
| Name | Type | Description |
|---|---|---|
base | string | |
fill | string | |
length | integer |
- Source
- Type:
- string
(static) mediaNumber(value) → {integer}
A numeric field from a Jellyfin media object, as an integer.
Jellyfin's numeric fields (Height, Width, Channels, BitRate, Level) arrive as integers from ParseJson, but the app has always handled them arriving as strings too — isWithinH264HardwareCeiling, ItemPostPlaybackInfo's channel count and getDeviceProfile's MaxAudioChannels each carry their own inline copy of this coercion. This is that coercion, once.
Returns 0 for anything missing or non-numeric, so callers can compare without a validity check first — a stream that does not report a height has no height, and 0 is the honest reading of that for every comparison the report makes.
THE TYPE NAMES BELOW WERE MEASURED, NOT REASONED, and the same integer reports three different names depending on where it came from. On a Stick 4K (Roku OS 15.3.4):
type(2) -> "Integer" an unboxed literal type(aa.someInt) -> "roInteger" boxed, read out of an AA type(node.someIntField) -> "roInt" read off a SceneGraph node field
All three are listed because this function is handed all three: MediaStream fields arrive in AAs from ParseJson, user settings arrive from a JellyfinUserSettings node, and callers pass literals. Enumerating only the plausible-looking names is what produced the bug this function was extracted to fix — type(x) = "Integer" against a node field is ALWAYS false, so GetBitRateLimit discarded every user-set bitrate limit and silently substituted Roku's hardware ceiling.
NOT FOR BYTE COUNTS. The return type is a 32-bit Integer, so anything above 2,147,483,647 overflows — and MediaSourceInfo.Size is a byte count, which passes that at ~2.1 GB. Every other numeric media field (Height, Width, Channels, BitRate, Level, SampleRate, BitDepth) is comfortably inside the range. Format sizes from the raw value in floating point instead.
| Name | Type | Description |
|---|---|---|
value | dynamic | a numeric field off a media object, a settings node, or a literal |
- Source
- the numeric value, or 0 for anything missing or non-numeric
- Type:
- integer
(static) meridianDiffers(timeHours, nowHours) → {boolean}
True when two times fall in opposite halves of the day, i.e. one of them is on the far side of noon or midnight from the other. This is exactly when a 12h time needs an am/pm suffix to be readable against the other: same half means the suffix carries no information. Hours are 0-23 local. Injected (rather than read from roDateTime) so this stays unit-testable.
| Name | Type | Description |
|---|---|---|
timeHours | integer | |
nowHours | integer |
- Source
- Type:
- boolean
(static) parseUrl(url) → {object}
Returns an array from a url = [ url, proto, host, port, subdir+params ] If port or subdir are not found, an empty string will be added to the array Proto must be declared or array will be empty
| Name | Type | Description |
|---|---|---|
url | string |
- Source
- Type:
- object
(static) primaryMediaSource(cachedPlaybackInfo) → {dynamic}
The MediaSource this playback is actually using, off the cached /PlaybackInfo response.
ONE definition because two surfaces describe the same streams — the playback-info report (PlayerHostView) and the playback-error Details readout (VideoPlayerView) — and two copies of "which source are we describing" is exactly how they would come to disagree about one stream.
MediaSources[0] is correct rather than merely convenient: the app asks the server for ONE source (the selected mediaSourceId), so the response carries the one that was negotiated for this play session, not a menu to pick from.
| Name | Type | Description |
|---|---|---|
cachedPlaybackInfo | dynamic | the cached ItemPostPlaybackInfo response |
- Source
the MediaSource AA, or invalid when nothing is cached
- Type:
- dynamic
(static) resolveApiVersion(serverVersion) → {integer}
Maps a Jellyfin server version string to an API version integer used by the SDK dispatcher. V1: Covers pre-10.9 servers (10.7.x, 10.8.x) — uses /Users/{userId}/ path prefix V2: Covers 10.9+ servers — endpoints moved to top-level, userId passed as query param Returns 1 if serverVersion is empty/invalid (safe fallback to legacy paths)
| Name | Type | Description |
|---|---|---|
serverVersion | string |
- Source
- Type:
- integer
(static) resolveSplashScreen(globalSplashSetting, serverSplashEnabled) → {boolean}
resolveSplashScreen: Resolves whether to show the splash screen on User Select screen
Checks JellyRock global setting. If "disabled", never shows splash. Otherwise follows server branding configuration. Ensures a valid boolean is always returned.
| Name | Type | Description |
|---|---|---|
globalSplashSetting | dynamic | JellyRock global splash screen setting ("enabled", "disabled", or invalid) |
serverSplashEnabled | dynamic | Server's splashscreenEnabled setting (boolean or invalid) |
- Source
- Resolved splash screen enabled state (guaranteed boolean)
- Type:
- boolean
(static) restoreFocus(view) → {void}
Restore focus to a view's saved descendant (lastFocus) if it's still valid, else focus the view root. The canonical focus-restore rule shared by the JRScreen lifecycle bridge (onScreenShown / handleFocus) and main.bs's side-panel handlers. No-op on an invalid view.
| Name | Type | Description |
|---|---|---|
view | object |
- Source
- Type:
- void
(static) roundNumber(f) → {integer}
Rounds number to nearest integer
| Name | Type | Description |
|---|---|---|
f | float |
- Source
- Type:
- integer
(static) routeForItem(item) → {object}
Maps a selected content item to its route — a named-route arg { name, params } for sgRouter.navigateTo. Pure (no router call) so it can live here in commonly-imported misc.bs; the routed view that owns the selection calls navigateTo itself (passing the rich node as context to avoid a re-fetch). Returns invalid for items that are NOT view navigation (Chapter → playback); the caller handles those via the playback path (quickPlayNode).
| Name | Type | Description |
|---|---|---|
item | object |
- Source
- Type:
- object
(static) secondsToTimestamp(totalSeconds, addLeadingMinuteZero) → {string}
Converts seconds to a human readable timestamp. Used for progress bar during playback eg. 3661 seconds = "01:01:01"
| Name | Type | Description |
|---|---|---|
totalSeconds | integer | |
addLeadingMinuteZero | boolean |
- Source
- Type:
- string
(static) setFieldTextValue(field, value) → {void}
| Name | Type | Description |
|---|---|---|
field | dynamic | |
value | dynamic |
- Source
- Type:
- void
(static) shuffleArray(array) → {object}
Takes an array of data, shuffles the order, then returns the array uses the Fisher-Yates shuffling algorithm
| Name | Type | Description |
|---|---|---|
array | object |
- Source
- Type:
- object
(static) startLoadingSpinner(disableRemoteopt, loadingTextopt) → {void}
startLoadingSpinner: Start a loading spinner and attach it to the main JRScene. Displays an invisible ProgressDialog node by default to disable keypresses while loading.
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
disableRemote | boolean | <optional> | true | |
loadingText | string | <optional> | "" |
- Source
- Type:
- void
(static) stopLoadingSpinner() → {void}
- Source
- Type:
- void
(static) textOrEmpty(input) → {string}
Text from a field that may simply not exist.
toString() faults with a Type Mismatch on Invalid, because it falls through to Str(), so it can only be used where the value is known to be present. Optional fields — most of a Jellyfin MediaStream, every field a newer server added — need this instead: absent reads as "", which is what a caller building display text out of them wants anyway.
| Name | Type | Description |
|---|---|---|
input | dynamic | any value, including Invalid |
- Source
- the text, or "" when there is nothing there
- Type:
- string
(static) ticksToHuman(ticks) → {string}
| Name | Type | Description |
|---|---|---|
ticks | longinteger |
- Source
- Type:
- string
(static) ticksToMinutes(ticks) → {longinteger}
Converts ticks to number of minutes eg. 3661 totalSeconds = 61
| Name | Type | Description |
|---|---|---|
ticks | longinteger |
- Source
- Type:
- longinteger
(static) toBoolean(value) → {dynamic}
convert value to boolean and return value
| Name | Type | Description |
|---|---|---|
value | dynamic |
- Source
- Type:
- dynamic
(static) toString(input) → {string}
| Name | Type | Description |
|---|---|---|
input | dynamic |
- Source
- Type:
- string
(static) urlCandidates(input) → {dynamic}
this is the "educated guess" logic for inferServerUrl that generates a list of complete url's as candidates for the tests in inferServerUrl. takes an incomplete url as an arg and returns a list of extrapolated full urls.
| Name | Type | Description |
|---|---|---|
input | string |
- Source
- Type:
- dynamic
(static) versionChecker(versionToCheck, minVersionAccepted) → {dynamic}
Returns whether or not a version number (e.g. 10.7.7) is greater or equal to some minimum version allowed (e.g. 10.8.0)
| Name | Type | Description |
|---|---|---|
versionToCheck | string | |
minVersionAccepted | string |
- Source
- Type:
- dynamic