chore(traceability): shift this branch's ids clear of master's

Master allocated DR-224 and UT-211 while this branch was in flight — the third
collision on this work. Everything here moves up by one: DR-224..236 become
DR-225..237, UT-211..213 become UT-212..214. UR-079, UR-080 and IR-033 were
still free and are unchanged.

Mechanical, and matched on each row's own text rather than on its number, so a
row cannot be shifted twice or the wrong one caught. Master's DR-224 (the
background-audio toggle) and UT-211 are untouched.
This commit is contained in:
2026-08-22 13:45:04 +02:00
parent 156b9e3684
commit fecd6022fe
30 changed files with 5309 additions and 5070 deletions
+16 -16
View File
@@ -39,7 +39,7 @@ async playerPlayItem(item: PlayItemRequest) : Promise<PlayerStatus> {
* playing there is nothing to pause, and an error would make the frontend
* handle a case that is not a failure.
*
* TRACES: UR-040, UR-041 | DR-224 | UT-211
* TRACES: UR-040, UR-041 | DR-225 | UT-212
*/
async playerBackgroundAction(backgroundAudioArmed: boolean, inPictureInPicture: boolean) : Promise<BackgroundAction> {
return await TAURI_INVOKE("player_background_action", { backgroundAudioArmed, inPictureInPicture });
@@ -273,7 +273,7 @@ async playerGetStreamingQualities() : Promise<([StreamingQuality, string, string
* belongs to Settings, and `player_set_video_settings` is the one that writes
* to the database.
*
* TRACES: UR-074, UR-079 | DR-162, DR-225
* TRACES: UR-074, UR-079 | DR-162, DR-226
*/
async playerSetStreamQuality(repositoryHandle: string, quality: StreamingQuality, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamQualityResponse> {
return await TAURI_INVOKE("player_set_stream_quality", { repositoryHandle, quality, useHtml5, currentPosition, mediaSourceId, audioStreamIndex });
@@ -1048,7 +1048,7 @@ async mediaLocalUrl(path: string) : Promise<string> {
* to consume rather than two, and so no caller has to infer a transport from a
* loopback URL.
*
* TRACES: UR-071, UR-079 | DR-224
* TRACES: UR-071, UR-079 | DR-225
*/
async mediaLocalSelection(path: string) : Promise<StreamSelection> {
return await TAURI_INVOKE("media_local_selection", { path });
@@ -1651,7 +1651,7 @@ async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId:
* position on an HLS playlist is copied onto every segment URI and the server
* rejects each with `400` (DR-181). Callers resume by seeking after load.
*
* TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 | UT-212
* TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228 | UT-213
*/
async repositoryGetStreamSelection(handle: string, itemId: string, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamSelection> {
return await TAURI_INVOKE("repository_get_stream_selection", { handle, itemId, mediaSourceId, audioStreamIndex });
@@ -2374,9 +2374,9 @@ export type LiveStreamInfo = { streamUrl: string; playSessionId: string | null;
* A live channel is always an HLS transcode — the server has to repackage a
* broadcast mux into something a browser can play, and there is no static
* file to direct-play. Saying so here means the player page never has to
* work it out from the URL, which is the whole of DR-224.
* work it out from the URL, which is the whole of DR-225.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
transport: Transport }
/**
@@ -2626,7 +2626,7 @@ needsTranscoding: boolean;
* caller falls back to `needs_transcoding` — every transcode this app
* requests is HLS (DR-140), so that fallback is exact rather than a guess.
*
* TRACES: UR-003, UR-004, UR-079 | DR-224, DR-229
* TRACES: UR-003, UR-004, UR-079 | DR-225, DR-230
*/
transport?: Transport | null;
/**
@@ -2750,7 +2750,7 @@ export type PlaybackInfo = { mediaSourceId: string; playSessionId: string; strea
* lets the UI say "this is not costing the server anything" without inferring
* it from a URL shape.
*
* TRACES: UR-079 | DR-227
* TRACES: UR-079 | DR-228
*/
export type PlaybackKind =
/**
@@ -2875,7 +2875,7 @@ needsTranscoding?: boolean;
* caller falls back to `needs_transcoding` — every transcode this app
* requests is HLS (DR-140), so that fallback is exact rather than a guess.
*
* TRACES: UR-003, UR-004, UR-079 | DR-224, DR-229
* TRACES: UR-003, UR-004, UR-079 | DR-225, DR-230
*/
transport?: Transport | null;
/**
@@ -3168,7 +3168,7 @@ skipped: number }
* them indistinguishable from Original. `exceeds_source` is what lets the
* frontend render that honestly without knowing anything about bitrates.
*
* TRACES: UR-070, UR-079 | DR-226, DR-121
* TRACES: UR-070, UR-079 | DR-227, DR-121
*/
export type QualityOption = { quality: StreamingQuality;
/**
@@ -3207,7 +3207,7 @@ export type RemoteSessionStatus = { position: number; duration: number | null; i
* there is no *chosen* rendition in that case, only the file itself, and
* reporting the ceiling that happened to be set would misdescribe it.
*
* TRACES: UR-079 | DR-224, DR-225
* TRACES: UR-079 | DR-225, DR-226
*/
export type Rendition = {
/**
@@ -3353,7 +3353,7 @@ export type StreamQualityResponse =
* that would have used the requested value was never reached. The stream
* changed and the menu did not.
*
* TRACES: UR-074, UR-079 | DR-225, DR-226
* TRACES: UR-074, UR-079 | DR-226, DR-227
*/
{ strategy: "native"; selection: StreamSelection; position: number } |
/**
@@ -3366,7 +3366,7 @@ export type StreamQualityResponse =
*
* Replaces the bare `String` URL that `get_video_stream_url` used to return.
*
* TRACES: UR-079 | DR-224, DR-226, DR-227
* TRACES: UR-079 | DR-225, DR-227, DR-228
*/
export type StreamSelection = {
/**
@@ -3386,7 +3386,7 @@ playbackKind: PlaybackKind;
*/
rendition: Rendition | null;
/**
* What this media source can offer, for the quality picker (DR-226).
* What this media source can offer, for the quality picker (DR-227).
*/
available: QualityOption[];
/**
@@ -3407,7 +3407,7 @@ playSessionId: string | null;
* queue's long-standing `needs_transcoding` flag and the seek strategy both
* read this, so there is one answer rather than three.
*
* TRACES: UR-079 | DR-224, DR-227
* TRACES: UR-079 | DR-225, DR-228
*/
needsTranscoding: boolean }
/**
@@ -3503,7 +3503,7 @@ export type ThumbnailCacheStats = { totalSizeBytes: number; itemCount: number; l
* Tagged (`{"type":"hls"}`) rather than a bare string so the frontend matches a
* discriminant instead of comparing text.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
export type Transport =
/**
+1 -1
View File
@@ -260,7 +260,7 @@ export class RepositoryClient {
* start position on an HLS playlist makes Jellyfin reject every segment behind
* it with `400` (DR-181). Resume by seeking once loaded.
*
* TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 | UT-212
* TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228 | UT-213
*/
async getStreamSelection(
itemId: string,
+17 -17
View File
@@ -94,7 +94,7 @@
* which forced this component to re-derive the transport by searching for
* `.m3u8`.
*
* TRACES: UR-079 | DR-224, DR-226
* TRACES: UR-079 | DR-225, DR-227
*/
selection: StreamSelection;
mediaSourceId?: string; // Media source ID for subtitle URLs
@@ -196,7 +196,7 @@
// The selection currently loaded. Starts from the prop and is replaced
// wholesale by a reload (quality change, audio-track switch, transcoded seek)
// so transport and URL can never disagree.
// TRACES: UR-079 | DR-224
// TRACES: UR-079 | DR-225
let currentSelection = $state<StreamSelection>(untrack(() => selection));
const currentStreamUrl = $derived(currentSelection.url);
/**
@@ -281,11 +281,11 @@
* Used by the paths that swap the stream without re-negotiating — the
* background-audio handoff and its return. Each states the transport it is
* moving to rather than letting it be inferred, which is the whole point of
* DR-224: the audio handoff really is a progressive mp3, and the rebuilt
* DR-225: the audio handoff really is a progressive mp3, and the rebuilt
* video stream really is an HLS transcode, and neither is knowable from the
* URL text.
*
* TRACES: UR-040, UR-079 | DR-224
* TRACES: UR-040, UR-079 | DR-225
*/
function selectionAt(url: string, transport: StreamSelection["transport"]): StreamSelection {
// A re-opened stream is a new transcode job; the old session id is stale.
@@ -326,14 +326,14 @@
/**
* The rungs to offer for the stream that is playing, straight from the
* backend (DR-226). Rungs whose ceiling is at or above the source bitrate are
* backend (DR-227). Rungs whose ceiling is at or above the source bitrate are
* dropped: they produce the same bytes as Original, so listing five of them is
* five ways to spell one choice. Rust decides which those are — this only
* decides not to draw them.
*
* `Original` is always kept; it is the source, never redundant with it.
*
* TRACES: UR-070, UR-079 | DR-226, DR-121
* TRACES: UR-070, UR-079 | DR-227, DR-121
*/
const qualityOptions = $derived(
currentSelection.available.filter((o) => !o.exceedsSource || o.quality === "original"),
@@ -654,7 +654,7 @@
// teardown for an unchanged stream and left the element showing nothing
// until a seek forced another cycle.
//
// TRACES: UR-079 | DR-224 | UT-213
// TRACES: UR-079 | DR-225 | UT-214
const loader = loaderForTransport(transportKind, {
hlsJsSupported: Hls.isSupported(),
nativeHlsSupported: !!videoElement.canPlayType("application/vnd.apple.mpegurl"),
@@ -892,7 +892,7 @@
});
});
// The quality *ladder* now arrives with the stream selection (DR-226), so all
// The quality *ladder* now arrives with the stream selection (DR-227), so all
// this still needs is the device default, for the case where the stream is a
// direct play and has no rendition of its own.
//
@@ -901,7 +901,7 @@
// HTML5 mode and breaks native seeking, and nothing about playback waits on
// this value.
//
// TRACES: UR-074, UR-079 | DR-162, DR-226
// TRACES: UR-074, UR-079 | DR-162, DR-227
onMount(() => {
commands
.playerGetVideoSettings()
@@ -974,7 +974,7 @@
videoCodec: needsTranscoding ? "hevc" : "h264",
needsTranscoding: needsTranscoding,
// Carry the negotiated transport onto the queue item so a later seek
// reads it instead of falling back. TRACES: UR-079 | DR-229
// reads it instead of falling back. TRACES: UR-079 | DR-230
transport: currentSelection.transport,
// Order matters: player_set_subtitle_track(n) is a position in this
// array. Previously this array was built and then dropped, so
@@ -1846,7 +1846,7 @@
// whether the item has a picture to lose, which is Rust's to know. This used
// to be decided implicitly by Kotlin gating the event on the toggle, which
// is why the native path -- whose media service keeps playing regardless --
// ignored the toggle entirely (DR-224).
// ignored the toggle entirely (DR-225).
let action: BackgroundAction;
try {
action = await commands.playerBackgroundAction(
@@ -2034,7 +2034,7 @@
id: media.id,
videoCodec: needsTranscoding ? "hevc" : "h264",
needsTranscoding,
// TRACES: UR-079 | DR-229
// TRACES: UR-079 | DR-230
transport: targetSelection.transport,
subtitles: nativeSubtitleTracks(sentSubtitleTracks),
});
@@ -2395,12 +2395,12 @@
* only supplies the position to resume at.
*
* The change applies to this playback alone; the durable Settings default is
* untouched (DR-225). Nothing is optimistically assigned here: what the picker
* untouched (DR-226). Nothing is optimistically assigned here: what the picker
* shows comes from the selection the backend hands back, because what you get
* is not always what you asked for — a ceiling above the source bitrate is the
* source, and claiming otherwise is the kind of lie the old picker told.
*
* TRACES: UR-074, UR-079 | DR-162, DR-225, DR-226
* TRACES: UR-074, UR-079 | DR-162, DR-226, DR-227
*/
async function selectQuality(quality: StreamingQuality) {
showQualityMenu = false;
@@ -2424,7 +2424,7 @@
// this the menu stayed on the first stream's rung while the stream itself
// changed underneath.
//
// TRACES: UR-074, UR-079 | DR-225, DR-226
// TRACES: UR-074, UR-079 | DR-226, DR-227
if (negotiated) {
currentSelection = negotiated;
}
@@ -2883,7 +2883,7 @@
<!--
Streaming quality (bandwidth ceiling), populated from what this media
source can actually offer. TRACES: UR-070, UR-074 | DR-162, DR-226
source can actually offer. TRACES: UR-070, UR-074 | DR-162, DR-227
-->
{#if qualityOptions.length > 1}
<div class="relative">
@@ -2910,7 +2910,7 @@
<div class="text-white text-sm font-semibold">Quality</div>
<!--
What the server is actually doing. Only knowable now that
the backend reports it. TRACES: UR-079 | DR-227
the backend reports it. TRACES: UR-079 | DR-228
-->
<div class="text-xs text-gray-400 mt-0.5">{playbackKindLabel}</div>
</div>
+3 -3
View File
@@ -37,7 +37,7 @@ const log = createLogger("Html5PlayerAdapter");
* This is the one place a fallback is tolerable, and it is explicitly a
* fallback: the negotiated path never reaches it.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
function selectionForLoad(streamUrl: string, options: PlayerLoadOptions): StreamSelection {
if (options.selection) return options.selection;
@@ -75,9 +75,9 @@ export interface Html5ElementBridge {
*
* Carries the whole [`StreamSelection`], not just the URL: the component's
* effect has to know the transport to choose a loader, and deriving that from
* the URL is the substring check DR-224 removes.
* the URL is the substring check DR-225 removes.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
setStreamSelection(selection: StreamSelection): void;
/** Tear down the component-owned hls.js instance (dual-audio prevention). */
+2 -2
View File
@@ -51,7 +51,7 @@ export interface PlayerLoadOptions {
* plugin's direct URL where the adapter falls back to what the other
* options already say rather than to reading the URL.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
selection?: StreamSelection | null;
/** The source is a file on disk (or the loopback server in front of one). */
@@ -127,7 +127,7 @@ export interface PlayerAdapter {
* already decided to reload, and `selection.transport` says how to open it, so
* no adapter has to infer that from the URL.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
reloadSource(selection: StreamSelection, offset: number): Promise<void>;
+1 -1
View File
@@ -199,7 +199,7 @@ async function switchAudioTrack(
* actually got, which is not always what was asked for: a ceiling above the
* source bitrate is the source.
*
* TRACES: UR-074, UR-079 | DR-162, DR-225
* TRACES: UR-074, UR-079 | DR-162, DR-226
*/
async function setStreamQuality(
quality: StreamingQuality,
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* The loader is chosen from the backend's `transport` tag, never from the URL.
*
* TRACES: UR-079 | DR-224 | UT-213
* TRACES: UR-079 | DR-225 | UT-214
*/
import { describe, expect, it } from "vitest";
+3 -3
View File
@@ -4,7 +4,7 @@
* Extracted from `VideoPlayer.svelte` so the decision can be unit-tested the
* same pattern as `episodeStrip.ts` and `TrackList.logic.test.ts`.
*
* TRACES: UR-079 | DR-224 | UT-213
* TRACES: UR-079 | DR-225 | UT-214
*/
import type { StreamSelection, Transport } from "$lib/api/bindings";
@@ -54,11 +54,11 @@ export function videoLoaderFor(
* whenever the selection **object** is replaced even with an identical URL and
* transport and the HLS effect's teardown/rebuild is not idempotent: it
* destroys the hls.js instance and reattaches, which leaves the element with no
* video until something forces another cycle. The pre-DR-224 code read a plain
* video until something forces another cycle. The pre-DR-225 code read a plain
* URL *string*, so re-assigning the same value was a no-op and the effect stayed
* put. Passing primitives restores that.
*
* TRACES: UR-079 | DR-224 | UT-213
* TRACES: UR-079 | DR-225 | UT-214
*/
export function loaderForTransport(
transport: Transport["type"],
+4 -4
View File
@@ -82,7 +82,7 @@
* Replaces a bare URL string: the transport travels with it, so neither this
* page nor VideoPlayer has to work out whether the URL is a playlist.
*
* TRACES: UR-079 | DR-224
* TRACES: UR-079 | DR-225
*/
let selection = $state<StreamSelection | null>(null);
let mediaSourceId = $state<string | null>(null);
@@ -325,7 +325,7 @@
//
// A downloaded file is a direct play over a local transport, and Rust
// says so rather than this page assuming it.
// TRACES: UR-071 | DR-137, DR-224
// TRACES: UR-071 | DR-137, DR-225
selection = await commands.mediaLocalSelection(fullPath);
videoNeedsTranscoding = false;
// Use explicit startPosition, or fall back to retrieved progress from database
@@ -397,7 +397,7 @@
// to stop it a moment later. Observed in the log as a pair of
// `[StreamSelection]` lines for one play.
//
// TRACES: UR-071 | DR-123, DR-137, DR-224
// TRACES: UR-071 | DR-123, DR-137, DR-225
const localPath = await commands.playerLocalMediaPath(id);
if (localPath) {
// A downloaded file is a direct play over a local transport, served
@@ -413,7 +413,7 @@
// the device profile and the ceiling in force, and returns the
// transport and the media-source id with it. This page no longer
// decides — or separately asks for — any of that.
// TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227
// TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228
selection = await repo.getStreamSelection(id, null, null);
mediaSourceId = selection.mediaSourceId;
// Rust's own verdict — "which kinds count as transcoding" is a