docs(player): allocate UR-074/DR-162 for the streaming bitrate cap
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 5m10s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m27s
Traceability Validation / Check Requirement Traces (push) Successful in 16s

The feature shipped tagged against DR-160, which a parallel session had
claimed for picture-in-picture in the meantime. Renumbered to DR-162
across the Rust and frontend TRACES comments (the PiP tags in
VideoPlayer.svelte, pictureInPicture.ts and nativeVideo.ts keep DR-160)
and regenerated bindings.ts.

Adds the requirement rows the tags point at: UR-074 for the user need, and
DR-162 covering why the cap has to reach the PlaybackInfo negotiation and
not only the transcode URL, why the ceiling is process-wide, and why the
Settings default persists while the in-player override does not. Notes
that this gives UR-070 its resume-at-the-same-point mechanism while the
server-offered rendition list that requirement also asks for stays
proposed. UT-156/157 record what the tests pin.

docs/specs/streaming-bitrate-cap.md carries the layer assignment — the
step definitions, the video/audio split, the resolution pairing and the
reload decision are all Rust; the frontend holds a serde token and the
labels it was handed.

TRACES: UR-074 | DR-162 | UT-156, UT-157
This commit is contained in:
2026-08-15 16:39:53 +02:00
parent 9c352fdb77
commit d49d027020
13 changed files with 197 additions and 38 deletions
+11 -11
View File
@@ -23,7 +23,7 @@ use crate::utils::lock::RwLockSafe;
/// Set from `player_set_video_settings` / `player_set_stream_quality`, and
/// restored from the database at startup.
///
/// TRACES: UR-074 | DR-160
/// TRACES: UR-074 | DR-162
static STREAMING_QUALITY: RwLock<StreamingQuality> = RwLock::new(StreamingQuality::Original);
/// Apply a bandwidth ceiling to every subsequently-opened video stream.
@@ -32,14 +32,14 @@ static STREAMING_QUALITY: RwLock<StreamingQuality> = RwLock::new(StreamingQualit
/// property of the URL the server is transcoding for, so changing it mid-stream
/// requires re-opening at the new quality (`player_set_stream_quality`).
///
/// TRACES: UR-074 | DR-160
/// TRACES: UR-074 | DR-162
pub fn set_streaming_quality(quality: StreamingQuality) {
*STREAMING_QUALITY.write_safe() = quality;
}
/// The ceiling currently applied to new video streams.
///
/// TRACES: UR-074 | DR-160
/// TRACES: UR-074 | DR-162
pub fn streaming_quality() -> StreamingQuality {
*STREAMING_QUALITY.read_safe()
}
@@ -421,7 +421,7 @@ impl OnlineRepository {
/// that suits the budget. `Original` keeps the historical 20/18 Mbps
/// allowance, which is a transcode ceiling rather than a user-facing limit.
///
/// TRACES: UR-004, UR-074 | DR-140, DR-160 | UT-130, UT-156
/// TRACES: UR-004, UR-074 | DR-140, DR-162 | UT-130, UT-156
pub async fn get_video_stream_url(
&self,
item_id: &str,
@@ -537,7 +537,7 @@ impl OnlineRepository {
// Audio-only is already far under any video cap, but a user on the
// bottom rungs of the ladder asked for *less traffic*, so take the
// lower of the two rather than always 384 kbps.
// TRACES: UR-074 | DR-160
// TRACES: UR-074 | DR-162
(
"MaxStreamingBitrate",
streaming_quality().audio_bitrate().min(384_000).to_string(),
@@ -1473,7 +1473,7 @@ impl MediaRepository for OnlineRepository {
// downstream is moot. `Original` keeps the historical "no ceiling"
// sentinel so the default path negotiates exactly as before.
//
// TRACES: UR-074 | DR-160
// TRACES: UR-074 | DR-162
let quality = streaming_quality();
let negotiated_bitrate = quality.max_bitrate().unwrap_or(999_999_999) as i64;
if let Some(cap) = quality.max_bitrate() {
@@ -1714,7 +1714,7 @@ impl MediaRepository for OnlineRepository {
is_playback: true,
// Live TV is video like any other, so the user's cap applies here
// too — a channel opened at the source bitrate would walk straight
// past a limit set for the connection. TRACES: UR-074 | DR-160
// past a limit set for the connection. TRACES: UR-074 | DR-162
max_streaming_bitrate: streaming_quality().max_bitrate().unwrap_or(20_000_000),
};
@@ -2466,7 +2466,7 @@ mod tests {
/// streaming ceiling, and restores the uncapped default afterwards — without
/// it, a capped test running concurrently changes what an uncapped one sees.
///
/// TRACES: UR-074 | DR-160
/// TRACES: UR-074 | DR-162
static QUALITY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct QualityFixture(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
@@ -2490,7 +2490,7 @@ mod tests {
/// can carry. Capping only `MaxStreamingBitrate` would leave the server
/// encoding 1080p into 2 Mbps.
///
/// TRACES: UR-074 | DR-160 | UT-156
/// TRACES: UR-074 | DR-162 | UT-156
#[tokio::test]
async fn test_video_stream_url_applies_bitrate_cap() {
let _fixture = QualityFixture::set(StreamingQuality::Mbps2);
@@ -2512,7 +2512,7 @@ mod tests {
/// The uncapped default must keep the exact transcode allowance this
/// endpoint has always used, and must not start constraining resolution.
///
/// TRACES: UR-074 | DR-160 | UT-156
/// TRACES: UR-074 | DR-162 | UT-156
#[tokio::test]
async fn test_video_stream_url_uncapped_keeps_legacy_allowance() {
let _fixture = QualityFixture::set(StreamingQuality::Original);
@@ -2535,7 +2535,7 @@ mod tests {
/// The background-audio handoff is already cheap, but someone who capped the
/// connection at 720 kbps asked for less traffic than its fixed 384 kbps.
///
/// TRACES: UR-040, UR-074 | DR-160 | UT-156
/// TRACES: UR-040, UR-074 | DR-162 | UT-156
#[tokio::test]
async fn test_audio_only_stream_url_takes_the_lower_of_cap_and_default() {
{