remoteProtocol

Transport-agnostic protocol constants + pure helpers for the remote-control receiver: the http-gate, the session-socket URL builder, reconnect backoff, and the client KeepAlive frame. No socket, no node, no m.global — fully unit-testable.

Members

(static, constant) CONTRACT_VERSION

The long-poll wire-contract version this client implements. The plugin advertises its own contractVersion in the /info probe body; JellyRock refuses a mismatch and stays dark rather than risk acting on a command shape it might misread. See remote-control-longpoll-contract.md (Versioning).

Default Value
  • 1

(static, constant) PLUGIN_ROUTE

--- HTTPS long-poll transport (#667) --- The plugin route prefix. JellyRock consumes the long-poll over TLS (roUrlTransfer) because Roku can't wss://. Unlike buildSocketUrl, these carry NO token in the URL — the caller attaches the standard Authorization header (baseRequest.buildAuthHeader), which already binds the session by DeviceId. See docs/architecture/remote-control-longpoll-contract.md.

Default Value
  • /JellyRock/RemoteControl

Methods

(static) buildLongPollUrl(serverUrl, waitMs, ackIdopt) → {string}

Build the plugin's long-poll command-channel URL for a server base URL. Returns "" for a blank server URL. waitMs is JellyRock's requested hold ceiling; the client-side transfer timeout is set longer so a 204 (empty hold) always arrives before the transfer itself times out.

ack=1 is the at-least-once opt-in flag (contract v1, additive): it tells the plugin this client will acknowledge receipt, so the plugin retains delivered commands and redelivers any we didn't confirm. It is present on EVERY poll (including the first) — a plugin that doesn't understand it just ignores it and stays at-most-once. ackId is our cumulative ack: the last MessageId we durably received ("" until we've received one, and omitted then). GUIDs are URL-safe, so no encoding is needed. https://host -> https://host/JellyRock/RemoteControl/poll?waitMs=&ack=1[&ackId=]

Parameters:
NameTypeAttributesDefaultDescription
serverUrldynamic
waitMsinteger
ackIddynamic<optional>
""
Returns:
Type: 
string

(static) buildPairPayload(rokuIps, appId, isDev) → {string}

Build the pairing-report JSON body from a device's LAN addresses + app identity. rokuIps is an array of IP strings (the values of roDeviceInfo.GetIPAddrs()); appId is roAppInfo.GetID() ("dev" when sideloaded, else the published channel id); isDev is roAppInfo.IsDev() (lets the plugin name a dev target distinctly). Returns "" when there are no usable IPs (nothing the plugin could wake) OR appId is blank, so the caller can skip a pointless report. The IPs are sanitized (see sanitizeIps).

Parameters:
NameTypeDescription
rokuIpsdynamic
appIddynamic
isDevdynamic
Returns:
Type: 
string

(static) buildPairRequest(serverUrl, rokuIps, appId, isDev) → {dynamic}

Compose the pairing report as a fire-and-forget SideEffect request AA (for SubmitSideEffect). Returns invalid when there's nothing worth reporting — no server url, or no usable wake address / appId (see buildPairPayload) — so the caller can skip a pointless POST. NO auth in the AA: the SideEffectTask attaches the Authorization header (which binds device/user identity) and Content-Type. rokuIps comes from the caller's roDeviceInfo.GetIPAddrs() (a device call — kept out of this pure module).

Parameters:
NameTypeDescription
serverUrldynamic
rokuIpsdynamic
appIddynamic
isDevdynamic
Returns:
Type: 
dynamic

(static) buildPairUrl(serverUrl) → {string}

Build the pairing-report URL for a server base URL. Returns "" for a blank server url. https://host -> https://host/JellyRock/RemoteControl/pair

Parameters:
NameTypeDescription
serverUrldynamic
Returns:
Type: 
string

(static) buildProbeUrl(serverUrl) → {string}

Build the plugin presence/version probe URL (200 = plugin present, 404 = absent). Returns "" for a blank server URL. No token in the URL (auth is header-based). https://host -> https://host/JellyRock/RemoteControl/info

Parameters:
NameTypeDescription
serverUrldynamic
Returns:
Type: 
string

(static) buildSocketUrl(serverUrl, authToken, deviceId) → {string}

Build the Jellyfin session-socket URL from the server URL + session credentials. Returns "" when the server isn't http:// (never downgrades an https:// token onto ws://) or when a credential is missing. http://host:port -> ws://host:port/socket?api_key=&deviceId=

The deviceId query param does NOT bind the session — Jellyfin parses DeviceId from the Authorization header ONLY and ignores the query string entirely (verified in AuthorizationContext across 10.7 -> 10.11). It is kept because the endpoint accepts it and it is harmless, but the binding is done by the Authorization header RemoteControlTask sets on the upgrade handshake. Assuming otherwise is what split the session in #743. api_key IS load-bearing: it authenticates the upgrade, and keeping it means a proxy that strips Authorization degrades rather than fails.

Parameters:
NameTypeDescription
serverUrldynamic
authTokendynamic
deviceIddynamic
Returns:
Type: 
string

(static) isHttpServer(serverUrl) → {boolean}

The http-gate. JellyRock can only open a ws:// socket to an http:// server — Roku can't do wss:// (no socket TLS), so an https:// session stays uncontrolled (the future plugin long-poll path). Both the advertised capability (deviceCapabilities) and the receiver gate on this, so they never disagree.

Parameters:
NameTypeDescription
serverUrldynamic
Returns:
Type: 
boolean

(static) keepAliveFrame() → {string}

The client->server KeepAlive frame. Jellyfin's ForceKeepAlive asks the client to send these on the requested interval so the session isn't reaped by the inactivity timeout.

Returns:
Type: 
string

(static) nextBackoffMs(attempt) → {integer}

Exponential reconnect backoff, 0-based attempt: 1s, 2s, 4s, 8s, 16s, then capped at 30s.

Parameters:
NameTypeDescription
attemptinteger
Returns:
Type: 
integer

(static) parseContractVersion(body) → {integer}

Extract the contractVersion from a /info probe body. Returns 0 for a blank/unparseable body, a non-object body, a missing key, or a non-numeric value — callers treat anything <> CONTRACT_VERSION as "no usable plugin". NEVER throws on a hostile/garbled body (it's network-controlled input): a string/object/bool contractVersion is rejected before any coercion that could raise at runtime.

Parameters:
NameTypeDescription
bodydynamic
Returns:
Type: 
integer

(static) pluginBaseUrl(serverUrl) → {string}

Shared plugin route-prefix builder. Trims a single trailing slash on the server URL so we never emit "//". Returns "" for a missing/blank server URL.

Parameters:
NameTypeDescription
serverUrldynamic
Returns:
Type: 
string

(static) sanitizeIps(rokuIps) → {object}

Sanitize a raw IP list into the addresses worth reporting: trim, drop blanks, drop loopback (127.x) and the unspecified address (0.0.0.0 — GetIPAddrs can surface a not-yet-configured interface as that), and dedupe (a Roku with both wired + wifi up can report the same address on more than one interface). Order-preserving. Always returns an array (never invalid), possibly empty.

Parameters:
NameTypeDescription
rokuIpsdynamic
Returns:
Type: 
object