Members
(static, constant) MAX_PLAUSIBLE_FRAMERATE
Above this, a reported frame rate is a container timebase rather than a frame rate. See sourceFramerate.
- Default Value
- 1000
Methods
(static) audioSection(stream, transcoding, urlParams, isCopy) → {object}
| Name | Type | Description |
|---|---|---|
stream | dynamic | |
transcoding | dynamic | |
urlParams | object | |
isCopy | boolean |
- Type:
- object
(static) bitDepthLabel(depth) → {string}
| Name | Type | Description |
|---|---|---|
depth | dynamic |
- Type:
- string
(static) buildPlaybackReport(input) → {object}
Build the report model.
missing one costs its rows, never the report
| Name | Type | Description |
|---|---|---|
input | object | see the field list below; every one is optional and a |
- { status, sections }
- Type:
- object
(static) codecOption(urlParams, codec, name) → {string}
A per-codec stream option, e.g. h264-rangetype. StreamInfo.GetOption falls back to the unqualified name when the qualified one is absent, so this does too.
| Name | Type | Description |
|---|---|---|
urlParams | object | |
codec | dynamic | |
name | string |
- Type:
- string
(static) dimensionsLabel(width, height) → {string}
| Name | Type | Description |
|---|---|---|
width | dynamic | |
height | dynamic |
- Type:
- string
(static) encoderIsIdle(transcoding, previousCompletion) → {boolean}
True when the encoder is not advancing.
The test is one comparison of the RAW CompletionPercentage against the previous poll, and it is safe at a 5-second interval because the server moves that value far faster: measured against a live transcode, 26 consecutive samples at ~1.2s spacing were all distinct and monotonically increasing, with no repeat. So an unchanged value across 5s cannot happen while ffmpeg is running.
previousCompletion invalid means this is the first sample and nothing can be concluded — the answer is "not idle" rather than a guess, so the row shows the last real speed until a comparison exists (about 5s after the dialog opens).
| Name | Type | Description |
|---|---|---|
transcoding | object | |
previousCompletion | dynamic |
- Type:
- boolean
(static) fileSection(source, transcoding, input) → {object}
The file itself.
PATH IS ADMIN-ONLY, and that is a privacy decision rather than a permissions one. A library path is the server's filesystem laid bare — it routinely carries a person's name in a home directory, a mount point that names a NAS, or a folder structure someone did not choose to publish, and the report is the one screen users are asked to photograph when they file a bug. An administrator is already able to see every path in the Jellyfin dashboard, so showing it to them reveals nothing new; showing it to a shared-server guest can.
Absent policy reads as NOT an administrator. The field is server-authoritative and only populated from a real session, so the safe default is the one that withholds — a report missing one row is a smaller failure than a report that leaks a path because a session had not loaded yet.
It is still the last row of the last section for the admins who do see it: the report scrolls, so a screenshot of the transcode detail at the top cannot include it without deliberately scrolling down first.
| Name | Type | Description |
|---|---|---|
source | object | |
transcoding | dynamic | |
input | object |
- Type:
- object
(static) findStreamByIndex(streams, index, streamType) → {dynamic}
The stream Jellyfin is actually using, found by its own index rather than by position. MediaStreams is a mixed list and the selected audio track is very often not the first one — reading streams[0] is how the old report ended up describing a track nobody was listening to.
| Name | Type | Description |
|---|---|---|
streams | dynamic | |
index | dynamic | |
streamType | string |
- Type:
- dynamic
(static) formatMultiplier(value) → {string}
| Name | Type | Description |
|---|---|---|
value | float |
- Type:
- string
(static) framerateLabel(fps) → {string}
| Name | Type | Description |
|---|---|---|
fps | float |
- Type:
- string
(static) parseTranscodeUrlParams(url) → {object}
Parse the TranscodingUrl's query string into lowercase-keyed values.
JellyRock already reads this URL for TranscodeReasons; the rest of it is the server's own declaration of what it was asked to produce, and it is the only target-side evidence available on every supported server version. Keys are lowercased because per-codec options are written as <codec>-<name> with the codec's own casing, which differs between the profile and the session.
| Name | Type | Description |
|---|---|---|
url | dynamic |
- Type:
- object
(static) percentageLabel(value) → {string}
| Name | Type | Description |
|---|---|---|
value | dynamic |
- Type:
- string
(static) plausibleFramerate(value) → {float}
A frame rate, or 0.0 when the value cannot be one.
| Name | Type | Description |
|---|---|---|
value | dynamic |
- Type:
- float
(static) playbackStatusLabel(session, transcoding, videoIsCopy, audioIsCopy) → {string}
Direct play / direct stream / transcoding — or nothing, when we do not know.
A SESSION WE NEVER GOT IS NOT A SESSION THAT SAYS DIRECT PLAY, and conflating the two is the worst thing this report can do. TranscodingInfo is absent in both cases: on a genuine direct play, and on a dropped /Sessions request or a server that has not registered playback yet. Reading only transcoding made every one of those say "Direct playing" — the report's most consequential claim, asserted from an answer nobody gave. The distinction is the SESSION's presence, so that is what this takes.
Empty is deliberately silent rather than a "status unknown" line: the sections below are all source-only in this state (no target evidence exists without a session), so the report already reads as "here is the file, nothing claimed about what the server is doing with it". The caller keeps polling and the line fills itself in.
The three-way split is Jellyfin's own — its web client swaps its panel heading between "Transcoding Info", "Remuxing Info" and "Direct Streaming Info" on exactly this test. IsVideoDirect and IsAudioDirect are literally IsCopyCodec(outputCodec) in the server, so both being true means the container was rewrapped and not one frame re-encoded.
| Name | Type | Description |
|---|---|---|
session | dynamic | |
transcoding | dynamic | |
videoIsCopy | boolean | |
audioIsCopy | boolean |
- Type:
- string
(static) positiveCount(value) → {string}
A count worth printing, or "" — zero and absent are the same non-answer.
| Name | Type | Description |
|---|---|---|
value | dynamic |
- Type:
- string
(static) profileAndLevel(stream) → {string}
| Name | Type | Description |
|---|---|---|
stream | object |
- Type:
- string
(static) reasonsSection(transcoding, videoStream, selectedAudio, source, input) → {object}
The server's reason codes, verbatim, each with our own explanation beside it — but ONLY where a JellyRock setting provably caused it.
The codes are not translated and not re-worded. Jellyfin already maintains that vocabulary and a parallel copy here would drift; more to the point, a code we cannot explain is the server's to explain, and inventing a gloss would add nothing a user could act on. What IS worth saying is the part no server can know: that the constraint came from a switch in OUR settings screen. See transcodeCause.bs for how narrowly that is decided.
| Name | Type | Description |
|---|---|---|
transcoding | dynamic | |
videoStream | dynamic | |
selectedAudio | dynamic | |
source | object | |
input | object |
- Type:
- object
(static) reportAppend(report, section) → {void}
| Name | Type | Description |
|---|---|---|
report | object | |
section | dynamic |
- Type:
- void
(static) reportRow(rows, id, labelKey, value) → {void}
Append a row only when it has something to say. Every field the report reads is optional on some server or some file, so "the row is absent" is the normal way this report handles missing data — never a placeholder, and never a heading with nothing beneath it.
| Name | Type | Description |
|---|---|---|
rows | object | |
id | string | |
labelKey | string | |
value | string |
- Type:
- void
(static) reportSection(id, heading, rows, wideLabels) → {object}
| Name | Type | Description |
|---|---|---|
id | string | |
heading | string | |
rows | object | |
wideLabels | boolean |
- Type:
- object
(static) sampleRateLabel(rate) → {string}
| Name | Type | Description |
|---|---|---|
rate | dynamic |
- Type:
- string
(static) sourceChannels(stream) → {string}
Channel count in the SAME vocabulary as the target, which is the whole point.
ChannelLayout is the server's own words and reads better on its own ("stereo"), and preferring it here was a real bug: the target side can only come from TranscodingInfo.AudioChannels, which is a number, so an unchanged stereo track rendered "stereo → 2.0" — an arrow claiming a conversion that never happened. Both sides go through formatAudioChannels so the comparison is meaningful, and ChannelLayout is the fallback only when the count itself is missing.
| Name | Type | Description |
|---|---|---|
stream | object |
- Type:
- string
(static) sourceFramerate(stream) → {float}
AverageFrameRate first, RealFrameRate as the fallback — matching how Jellyfin's own web client resolves it, so the speed multiplier is computed against the same denominator both clients use.
BOTH ARE SANITY-CHECKED, because on an HLS live stream the server can report the container's 90 kHz MPEG-TS clock where a frame rate belongs. Seen on device against a Live TV channel: "Frame Rate 90000.0 fps". That figure is not merely ugly — it is also the denominator of the transcode speed multiplier, so it would turn a healthy encoder into a confident "0.0x", which reads as "the server cannot keep up".
A value we cannot believe is treated as no answer at all, so the row is omitted rather than wrong — the same rule the rest of this file follows. The ceiling is deliberately far above any real content (high-frame-rate capture tops out in the hundreds) and far below the 90000 it exists to reject, so it cannot start discarding a rate somebody actually shot.
| Name | Type | Description |
|---|---|---|
stream | dynamic |
- Type:
- float
(static) subtitleDelivery(stream, urlParams) → {string}
| Name | Type | Description |
|---|---|---|
stream | object | |
urlParams | object |
- Type:
- string
(static) subtitleDeliveryLabel(method) → {string}
A delivery method as the user reads it, or the server's own word when it is one we do not have a label for (Hls, Drop). Never invents one.
| Name | Type | Description |
|---|---|---|
method | string |
- Type:
- string
(static) subtitleSection(stream, urlParams) → {object}
What is happening to subtitles, which no other row can explain.
&SubtitleMethod is an EXACT declaration, not a constraint — and "Encode" is the one that matters most, because burning subtitles in forces a full video re-encode. Without this row a viewer sees the video being transcoded with no video-shaped reason for it.
| Name | Type | Description |
|---|---|---|
stream | dynamic | |
urlParams | object |
- Type:
- object
(static) targetBitDepth(stream, urlParams, transcoding, isCopy) → {string}
The target bit depth, but only when the source actually exceeds the ceiling.
<codec>-videobitdepth is a maximum, not an output. A 10-bit source under an 8-bit ceiling will come out at 8; an 8-bit source under the same ceiling is untouched, and rendering "8-bit → 8-bit" there would imply a conversion that never happened.
| Name | Type | Description |
|---|---|---|
stream | object | |
urlParams | object | |
transcoding | dynamic | |
isCopy | boolean |
- Type:
- string
(static) targetVideoRange(urlParams, transcoding, isCopy) → {string}
The target dynamic range, but only when the server named exactly one.
<codec>-rangetype is a CONSTRAINT: StreamBuilder writes a comma-joined list of permitted values there, and in one branch the complement of a list. A single value is the only case where the constraint and the outcome coincide.
| Name | Type | Description |
|---|---|---|
urlParams | object | |
transcoding | dynamic | |
isCopy | boolean |
- Type:
- string
(static) transcodeRunway(transcoding, source, playheadSeconds) → {string}
The encoder's lead over the playhead, as a timestamp in the same format the OSD progress bar uses.
RunTimeTicks is deliberately NOT read through mediaNumber: a three-hour runtime is ~1.08e11 ticks, which overflows a 32-bit integer. The arithmetic stays in floating point throughout for the same reason getReadableSize does.
| Name | Type | Description |
|---|---|---|
transcoding | object | |
source | object | |
playheadSeconds | dynamic |
- Type:
- string
(static) transcodeSection(transcoding, videoStream, source, input) → {object}
The live half of the report — every row here changes while you read it.
Speed is the most actionable figure in the whole report and nothing else in the app surfaces it: TranscodingInfo.Framerate is how many frames per second the server is ENCODING (not the output frame rate), so dividing by the source frame rate gives a real-time multiplier. Below 1.0x the server cannot keep up and playback will stall. Jellyfin's web client shows the same ratio.
| Name | Type | Description |
|---|---|---|
transcoding | dynamic | |
videoStream | dynamic | |
source | object | |
input | object |
- Type:
- object
(static) transcodeSpeed(transcoding, videoStream) → {string}
| Name | Type | Description |
|---|---|---|
transcoding | object | |
videoStream | dynamic |
- Type:
- string
(static) upperOrEmpty(value) → {string}
| Name | Type | Description |
|---|---|---|
value | dynamic |
- Type:
- string
(static) videoLevelLabel(codec, level) → {string}
A codec's level as people write it, or "" when we cannot say.
THERE IS NO SINGLE SCALE, which is the thing that makes this worth a function. The report shipped dividing everything by ten and rendered an HEVC Main 10 stream at level 150 as "L15.0" — a level that does not exist in the spec, whose maximum is 6.2. Read off Jellyfin's own device profile (jellyfin-web src/scripts/browserDeviceProfile.js), the three scales are:
h264 maxH264Level = 42 -> 4.2 level x 10 hevc maxHevcLevel = 153 -> 5.1 level x 30 av1 maxAv1Level = 15 -> 5.3 seq_level_idx, neither
AV1 is an INDEX, not a multiple: idx = (major - 2) * 4 + minor, which their own // level 5.3 comment on 15 confirms and their 16/17/18/19 -> 6.0/6.1/6.2/6.3 run corroborates. deviceCapabilities.bs encodes all three scales in jellyfinVideoLevel() for the device profile — this is the same knowledge read backwards, and the two must not be allowed to disagree. (That function replaced convertHevcLevelToString and a decimal-stripping h264 path in #868, which had av1 on neither scale and mpeg2 on none at all.)
ANYTHING ELSE RENDERS NO LEVEL AT ALL. vp9 gets no level condition in Jellyfin's own profile and mpeg2's scale is not established here, so there is no number to divide by and a guess would be the "convincing wrong thing" this file's header warns about. The profile still renders; only the level is withheld.
| Name | Type | Description |
|---|---|---|
codec | string | |
level | integer |
- Type:
- string
(static) videoSection(stream, transcoding, urlParams, isCopy) → {object}
| Name | Type | Description |
|---|---|---|
stream | dynamic | |
transcoding | dynamic | |
urlParams | object | |
isCopy | boolean |
- Type:
- object