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:
@@ -187,7 +187,7 @@ pub struct PlayItemRequest {
|
||||
/// 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
|
||||
#[serde(default)]
|
||||
pub transport: Option<crate::repository::Transport>,
|
||||
|
||||
@@ -331,7 +331,7 @@ pub enum VideoSeekResponse {
|
||||
ReloadStream {
|
||||
/// What to open, and how — transport included, so the frontend picks
|
||||
/// its loader from a tagged enum rather than by searching the URL for
|
||||
/// `.m3u8`. TRACES: UR-079 | DR-224
|
||||
/// `.m3u8`. TRACES: UR-079 | DR-225
|
||||
selection: StreamSelection,
|
||||
/// `seek_offset` carries the position to RESUME AT, not a base to add to
|
||||
/// the element's clock. The reloaded stream starts at the item's zero —
|
||||
@@ -353,7 +353,7 @@ pub enum AudioTrackSwitchResponse {
|
||||
},
|
||||
/// HTML5 needs to reload stream with new audio track
|
||||
ReloadStream {
|
||||
/// What to open, and how. TRACES: UR-079 | DR-224
|
||||
/// What to open, and how. TRACES: UR-079 | DR-225
|
||||
selection: StreamSelection,
|
||||
/// Current position to resume from
|
||||
position: f64,
|
||||
@@ -379,7 +379,7 @@ pub enum 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
|
||||
Native {
|
||||
/// What the backend actually opened, so the UI reflects it rather than
|
||||
/// assuming the request was honoured verbatim.
|
||||
@@ -392,7 +392,7 @@ pub enum StreamQualityResponse {
|
||||
/// What to open, and how — already negotiated against the requested
|
||||
/// ceiling. Carries `available` too, so a picker opened after a quality
|
||||
/// change still describes the source correctly.
|
||||
/// TRACES: UR-070, UR-079 | DR-224, DR-226
|
||||
/// TRACES: UR-070, UR-079 | DR-225, DR-227
|
||||
selection: StreamSelection,
|
||||
/// Position to resume from.
|
||||
position: f64,
|
||||
@@ -449,7 +449,7 @@ pub(super) async fn create_media_item(
|
||||
source,
|
||||
video_codec: Some(req.video_codec),
|
||||
needs_transcoding: req.needs_transcoding,
|
||||
// The caller's negotiated transport, when it had one. TRACES: UR-079 | DR-229
|
||||
// The caller's negotiated transport, when it had one. TRACES: UR-079 | DR-230
|
||||
transport: req.transport,
|
||||
video_width: None, // Not available from video-only request
|
||||
video_height: None, // Not available from video-only request
|
||||
@@ -702,9 +702,9 @@ pub async fn player_play_item(
|
||||
// A ceiling chosen from the in-player picker belongs to the playback it was
|
||||
// chosen for. Starting a different item returns to the device default —
|
||||
// otherwise "2 Mbps, just for this one film" quietly governs the rest of the
|
||||
// session, which is the defect DR-225 exists to close.
|
||||
// session, which is the defect DR-226 exists to close.
|
||||
//
|
||||
// TRACES: UR-074, UR-079 | DR-225
|
||||
// TRACES: UR-074, UR-079 | DR-226
|
||||
crate::repository::online::clear_playback_quality_override();
|
||||
|
||||
// Create media item, checking for local download first
|
||||
@@ -898,7 +898,7 @@ pub async fn player_enter_background_audio(
|
||||
/// 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
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn player_background_action(
|
||||
@@ -984,9 +984,9 @@ pub async fn player_play_queue(
|
||||
// A ceiling chosen from the in-player picker belongs to the playback it was
|
||||
// chosen for. Starting a different item returns to the device default —
|
||||
// otherwise "2 Mbps, just for this one film" quietly governs the rest of the
|
||||
// session, which is the defect DR-225 exists to close.
|
||||
// session, which is the defect DR-226 exists to close.
|
||||
//
|
||||
// TRACES: UR-074, UR-079 | DR-225
|
||||
// TRACES: UR-074, UR-079 | DR-226
|
||||
crate::repository::online::clear_playback_quality_override();
|
||||
|
||||
// Handle shuffle first
|
||||
@@ -1461,7 +1461,7 @@ pub async fn player_seek_video(
|
||||
// queued without one fall back to `needs_transcoding`, which is exact:
|
||||
// every transcode this app requests is HLS (DR-140).
|
||||
//
|
||||
// TRACES: UR-004, UR-079 | DR-224, DR-229
|
||||
// TRACES: UR-004, UR-079 | DR-225, DR-230
|
||||
let is_hls = match transport {
|
||||
Some(crate::repository::Transport::Hls) => true,
|
||||
Some(crate::repository::Transport::Progressive)
|
||||
@@ -1676,7 +1676,7 @@ pub async fn player_switch_audio_track(
|
||||
/// 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
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
// Two of the eight arguments are Tauri `State<'_, _>` injections, not caller
|
||||
@@ -1728,7 +1728,7 @@ pub async fn player_set_stream_quality(
|
||||
// builder then never gets to constrain).
|
||||
//
|
||||
// Deliberately the *override*, not the device default: see the doc above.
|
||||
// TRACES: UR-074, UR-079 | DR-225
|
||||
// TRACES: UR-074, UR-079 | DR-226
|
||||
crate::repository::online::set_playback_quality_override(quality);
|
||||
|
||||
// Where to resume. `current_position` is the *element's* clock, which only
|
||||
@@ -1740,7 +1740,7 @@ pub async fn player_set_stream_quality(
|
||||
// playback state); asking the DOM for it and falling back to 0 inverted
|
||||
// that. Fall back to what the controller reports instead.
|
||||
//
|
||||
// TRACES: UR-005, UR-074 | DR-225
|
||||
// TRACES: UR-005, UR-074 | DR-226
|
||||
// The guard is bound inside the arm's block so it is dropped before the
|
||||
// reload below takes the same lock. This codebase has been bitten by a
|
||||
// MutexGuard living longer than the expression that produced it.
|
||||
@@ -2451,9 +2451,9 @@ pub async fn player_play_tracks(
|
||||
// A ceiling chosen from the in-player picker belongs to the playback it was
|
||||
// chosen for. Starting a different item returns to the device default —
|
||||
// otherwise "2 Mbps, just for this one film" quietly governs the rest of the
|
||||
// session, which is the defect DR-225 exists to close.
|
||||
// session, which is the defect DR-226 exists to close.
|
||||
//
|
||||
// TRACES: UR-074, UR-079 | DR-225
|
||||
// TRACES: UR-074, UR-079 | DR-226
|
||||
crate::repository::online::clear_playback_quality_override();
|
||||
|
||||
info!(
|
||||
|
||||
@@ -618,7 +618,7 @@ pub async fn repository_get_video_stream_url(
|
||||
/// 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
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn repository_get_stream_selection(
|
||||
|
||||
@@ -114,7 +114,7 @@ pub fn media_local_url(
|
||||
/// 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
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn media_local_selection(
|
||||
|
||||
@@ -1211,7 +1211,7 @@ pub fn run() {
|
||||
builder.mount_events(app);
|
||||
|
||||
// Native video surface: put a GL area under Tauri's webview so mpv
|
||||
// can draw beneath the controls (UR-080 / DR-230).
|
||||
// can draw beneath the controls (UR-080 / DR-231).
|
||||
//
|
||||
// 🔴 OFF BY DEFAULT — the naive reparent crashes the app on the
|
||||
// first click. `tauri-runtime-wry`'s undecorated-resizing handler
|
||||
@@ -1236,14 +1236,14 @@ pub fn run() {
|
||||
// Kept behind an env var rather than deleted so the next attempt has
|
||||
// something to iterate on: JELLYTAU_NATIVE_VIDEO=1 bun run tauri dev
|
||||
//
|
||||
// TRACES: UR-080 | DR-230
|
||||
// TRACES: UR-080 | DR-231
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::var("JELLYTAU_NATIVE_VIDEO").as_deref() == Ok("1") {
|
||||
use tauri::Manager;
|
||||
log::warn!(
|
||||
"[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \
|
||||
video surface; the app will abort on the first click until \
|
||||
the widget-tree shape is solved (DR-230)"
|
||||
the widget-tree shape is solved (DR-231)"
|
||||
);
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
match window.default_vbox() {
|
||||
|
||||
@@ -123,7 +123,7 @@ pub struct MediaItem {
|
||||
/// 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
|
||||
#[serde(default)]
|
||||
pub transport: Option<crate::repository::Transport>,
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ pub mod android;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod mpv_backend;
|
||||
|
||||
/// The native video surface mpv renders into (UR-080 / DR-230).
|
||||
/// The native video surface mpv renders into (UR-080 / DR-231).
|
||||
///
|
||||
/// Linux-gated for now because the surface is GTK. Everything *around* it — the
|
||||
/// render context, its lifetime, frame pacing, the device profile — is
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
//! real webview into it. Same widgets, one extra move, and the only place
|
||||
//! Tauri-specific behaviour can still bite — which is why it is gate one.
|
||||
//!
|
||||
//! TRACES: UR-080 | DR-230, IR-033
|
||||
//! TRACES: UR-080 | DR-231, IR-033
|
||||
|
||||
use gtk::prelude::*;
|
||||
use log::{info, warn};
|
||||
|
||||
/// The widgets that make up the video surface, kept together because their
|
||||
/// lifetimes are bound: the render context (added next) is created when the GL
|
||||
/// area realizes and must be freed before it unrealizes — DR-231.
|
||||
/// area realizes and must be freed before it unrealizes — DR-232.
|
||||
pub struct VideoSurface {
|
||||
/// The GL area mpv renders into. Main child of the overlay, so it sits
|
||||
/// *under* everything else.
|
||||
@@ -30,7 +30,7 @@ pub struct VideoSurface {
|
||||
|
||||
impl VideoSurface {
|
||||
// Consumed by the render context, which binds to the GL area on `realize`
|
||||
// and is freed on `unrealize` (DR-231). Held here from the moment the
|
||||
// and is freed on `unrealize` (DR-232). Held here from the moment the
|
||||
// surface exists so that binding has something to attach to.
|
||||
#[allow(dead_code)]
|
||||
/// The GL area, for the render context to bind to.
|
||||
@@ -84,7 +84,7 @@ impl std::error::Error for SurfaceError {}
|
||||
/// bottom of the stack. Adding them the other way round yields a webview with
|
||||
/// video painted over it — an easy mistake with an obvious symptom.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-230
|
||||
/// TRACES: UR-080 | DR-231
|
||||
pub fn attach(vbox: >k::Box) -> Result<VideoSurface, SurfaceError> {
|
||||
// Tauri packs exactly one child (the webview) into the default vbox. Take it
|
||||
// rather than assume its type: wry's widget is an implementation detail, and
|
||||
@@ -135,8 +135,8 @@ pub fn attach(vbox: >k::Box) -> Result<VideoSurface, SurfaceError> {
|
||||
/// surface is torn down without returning the webview to the vbox the UI
|
||||
/// disappears while the app keeps running. Mirrors [`attach`] exactly.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-230, DR-231
|
||||
// Called by the render-context teardown, which lands with DR-231. Written now,
|
||||
/// TRACES: UR-080 | DR-231, DR-232
|
||||
// Called by the render-context teardown, which lands with DR-232. Written now,
|
||||
// beside `attach`, because a reparent whose inverse is written later is a
|
||||
// reparent whose inverse is written wrong.
|
||||
#[allow(dead_code)]
|
||||
@@ -166,7 +166,7 @@ mod tests {
|
||||
/// The stacking order is the whole point, and getting it backwards produces
|
||||
/// video painted over the controls rather than under them.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-230
|
||||
/// TRACES: UR-080 | DR-231
|
||||
#[test]
|
||||
#[ignore = "requires a display"]
|
||||
fn test_gl_area_is_below_the_reparented_webview() {
|
||||
@@ -195,7 +195,7 @@ mod tests {
|
||||
/// A surface that tears down without returning the webview leaves a running
|
||||
/// app with no UI.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-230, DR-231
|
||||
/// TRACES: UR-080 | DR-231, DR-232
|
||||
#[test]
|
||||
#[ignore = "requires a display"]
|
||||
fn test_detach_returns_the_webview_to_the_vbox() {
|
||||
@@ -219,7 +219,7 @@ mod tests {
|
||||
|
||||
/// A vbox Tauri has not populated is a changed assumption, not a panic.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-230
|
||||
/// TRACES: UR-080 | DR-231
|
||||
#[test]
|
||||
#[ignore = "requires a display"]
|
||||
fn test_an_empty_vbox_is_an_error_not_a_panic() {
|
||||
|
||||
@@ -215,7 +215,7 @@ pub fn video_audio_codecs(detected: &str) -> String {
|
||||
// Where the video renderer decodes the audio itself (ExoPlayer), the
|
||||
// platform list *is* the answer and narrowing it to the webview's throws
|
||||
// away codecs the device genuinely plays — dts, on the tablet this was
|
||||
// found on. TRACES: UR-004, UR-080 | DR-233
|
||||
// found on. TRACES: UR-004, UR-080 | DR-234
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
return detected.to_string();
|
||||
@@ -258,7 +258,7 @@ pub fn video_audio_codecs(detected: &str) -> String {
|
||||
///
|
||||
/// One source, so the copies cannot disagree again.
|
||||
///
|
||||
/// TRACES: UR-004, UR-080 | DR-233
|
||||
/// TRACES: UR-004, UR-080 | DR-234
|
||||
pub fn renderer_codecs() -> (String, String) {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
@@ -304,7 +304,7 @@ pub fn renderer_codecs() -> (String, String) {
|
||||
/// the audio, so judging it against the webview's capabilities transcodes files
|
||||
/// that would have played.
|
||||
///
|
||||
/// TRACES: UR-004, UR-080 | DR-233
|
||||
/// TRACES: UR-004, UR-080 | DR-234
|
||||
pub fn renderer_can_decode_audio(codec: &str) -> bool {
|
||||
let codec = codec.trim();
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -348,7 +348,7 @@ pub fn webview_can_decode_audio(codec: &str) -> bool {
|
||||
pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> bool {
|
||||
match served_audio_codec(streams) {
|
||||
// The renderer that will decode it, not always the webview — see
|
||||
// `renderer_can_decode_audio`. TRACES: UR-004, UR-080 | DR-233
|
||||
// `renderer_can_decode_audio`. TRACES: UR-004, UR-080 | DR-234
|
||||
Some(codec) => !renderer_can_decode_audio(codec),
|
||||
// No audio at all, or a codec the server did not name: leave it alone.
|
||||
None => false,
|
||||
|
||||
@@ -97,13 +97,13 @@ impl HybridRepository {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Decide what stream to play and describe it fully — the DR-224 contract.
|
||||
/// Decide what stream to play and describe it fully — the DR-225 contract.
|
||||
///
|
||||
/// Online-only for the same reason as `get_video_stream_url`: an offline
|
||||
/// item is a file on disk, and the caller builds
|
||||
/// [`StreamSelection::local_file`] for it rather than negotiating anything.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227
|
||||
/// TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228
|
||||
pub async fn get_stream_selection(
|
||||
&self,
|
||||
item_id: &str,
|
||||
|
||||
@@ -5,7 +5,7 @@ pub mod hybrid;
|
||||
pub mod offline;
|
||||
pub mod online;
|
||||
pub mod series_progress;
|
||||
/// Backend-owned stream selection (UR-079 / DR-224).
|
||||
/// Backend-owned stream selection (UR-079 / DR-225).
|
||||
pub mod stream_selection;
|
||||
pub mod types;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ static STREAMING_QUALITY: RwLock<StreamingQuality> = RwLock::new(StreamingQualit
|
||||
/// Cleared when a new item starts playing, so the override cannot outlive the
|
||||
/// playback it was chosen for. The device default is never touched by it.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225
|
||||
/// TRACES: UR-074, UR-079 | DR-226
|
||||
static PLAYBACK_QUALITY_OVERRIDE: RwLock<Option<StreamingQuality>> = RwLock::new(None);
|
||||
|
||||
/// Set the durable device default. Applies to every stream opened afterwards
|
||||
@@ -71,7 +71,7 @@ pub fn streaming_quality() -> StreamingQuality {
|
||||
|
||||
/// Move *this playback* to a different ceiling without disturbing the default.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225
|
||||
/// TRACES: UR-074, UR-079 | DR-226
|
||||
pub fn set_playback_quality_override(quality: StreamingQuality) {
|
||||
*PLAYBACK_QUALITY_OVERRIDE.write_safe() = Some(quality);
|
||||
}
|
||||
@@ -82,14 +82,14 @@ pub fn set_playback_quality_override(quality: StreamingQuality) {
|
||||
/// not silently govern the next one. Autoplaying the next episode is the case
|
||||
/// that matters — nobody re-opens the picker between episodes.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225
|
||||
/// TRACES: UR-074, UR-079 | DR-226
|
||||
pub fn clear_playback_quality_override() {
|
||||
*PLAYBACK_QUALITY_OVERRIDE.write_safe() = None;
|
||||
}
|
||||
|
||||
/// The per-playback override, if one is in force.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225
|
||||
/// TRACES: UR-074, UR-079 | DR-226
|
||||
pub fn playback_quality_override() -> Option<StreamingQuality> {
|
||||
*PLAYBACK_QUALITY_OVERRIDE.read_safe()
|
||||
}
|
||||
@@ -102,7 +102,7 @@ pub fn playback_quality_override() -> Option<StreamingQuality> {
|
||||
/// the old static: a negotiation that authorises a direct play the URL builder
|
||||
/// then constrains (or vice versa) leaks the cap.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225
|
||||
/// TRACES: UR-074, UR-079 | DR-226
|
||||
pub fn effective_streaming_quality() -> StreamingQuality {
|
||||
playback_quality_override().unwrap_or_else(streaming_quality)
|
||||
}
|
||||
@@ -627,7 +627,7 @@ impl OnlineRepository {
|
||||
// On the webview path this still resolves to "h264" alone, so nothing
|
||||
// changes there.
|
||||
//
|
||||
// TRACES: UR-004, UR-080 | DR-233
|
||||
// TRACES: UR-004, UR-080 | DR-234
|
||||
let (renderer_video_codecs, _) = super::device_profile::renderer_codecs();
|
||||
let mut params = vec (the legacy shape) and
|
||||
/// `get_stream_selection` (the DR-224 contract) go through it, so the
|
||||
/// `get_stream_selection` (the DR-225 contract) go through it, so the
|
||||
/// profile they negotiate under cannot drift apart.
|
||||
///
|
||||
/// TRACES: UR-004, UR-074, UR-079 | DR-224, DR-227
|
||||
/// TRACES: UR-004, UR-074, UR-079 | DR-225, DR-228
|
||||
async fn negotiate_playback(
|
||||
&self,
|
||||
item_id: &str,
|
||||
@@ -788,7 +788,7 @@ impl OnlineRepository {
|
||||
// What the renderer that will decode this can play. One source, shared
|
||||
// with the transcode URL builder and the client-side audio override, so
|
||||
// the profile we advertise and the stream we then ask for cannot
|
||||
// disagree. TRACES: UR-004, UR-080 | DR-233
|
||||
// disagree. TRACES: UR-004, UR-080 | DR-234
|
||||
let (video_codecs, audio_codecs) = super::device_profile::renderer_codecs();
|
||||
|
||||
// Video plays in a webview <video> element on every platform, which
|
||||
@@ -870,7 +870,7 @@ impl OnlineRepository {
|
||||
//
|
||||
// Capped at the two codecs a Jellyfin server actually
|
||||
// encodes, so a wider decode list never asks it for an av1
|
||||
// encode. TRACES: UR-004, UR-080 | DR-233
|
||||
// encode. TRACES: UR-004, UR-080 | DR-234
|
||||
video_codec: Some(
|
||||
if video_codecs.contains("hevc") {
|
||||
"h264,hevc"
|
||||
@@ -948,7 +948,7 @@ impl OnlineRepository {
|
||||
/// frontend stops testing the URL for `.m3u8`, and the quality ladder for
|
||||
/// *this* source so the picker stops offering rungs that mean nothing for it.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-224, DR-225, DR-226, DR-227 | UT-212
|
||||
/// TRACES: UR-070, UR-079 | DR-225, DR-226, DR-227, DR-228 | UT-213
|
||||
pub async fn get_stream_selection(
|
||||
&self,
|
||||
item_id: &str,
|
||||
@@ -1512,7 +1512,7 @@ impl JellyfinItem {
|
||||
// exactly how the cap used to leak (a negotiation authorising a direct play the
|
||||
// URL builder then never got to constrain).
|
||||
//
|
||||
// TRACES: UR-079 | DR-224, DR-227
|
||||
// TRACES: UR-079 | DR-225, DR-228
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -1605,7 +1605,7 @@ pub struct NegotiatedSource {
|
||||
/// The source's own bitrate, when the server reports one.
|
||||
///
|
||||
/// Fills the quality picker's "this rung is the same as Original" judgement
|
||||
/// (DR-226). Absent for some containers — the sampled library has `avi`
|
||||
/// (DR-227). Absent for some containers — the sampled library has `avi`
|
||||
/// files with no bitrate at all — in which case nothing is judged redundant
|
||||
/// and every rung stays offered.
|
||||
#[serde(default)]
|
||||
@@ -1640,7 +1640,7 @@ pub struct NegotiatedStream {
|
||||
/// separately, or that the viewer has pinned a track the file does not default
|
||||
/// to.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227 | UT-212
|
||||
/// TRACES: UR-079 | DR-228 | UT-213
|
||||
pub fn decide_playback_kind(
|
||||
source: &NegotiatedSource,
|
||||
audio_forces_transcode: bool,
|
||||
@@ -3157,7 +3157,7 @@ mod tests {
|
||||
set_streaming_quality(StreamingQuality::Original);
|
||||
// A leaked per-playback override would cap every later test's
|
||||
// expectations without appearing anywhere in its setup.
|
||||
// TRACES: UR-074, UR-079 | DR-225
|
||||
// TRACES: UR-074, UR-079 | DR-226
|
||||
clear_playback_quality_override();
|
||||
}
|
||||
}
|
||||
@@ -4217,7 +4217,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Direct-play negotiation (DR-227)
|
||||
// Direct-play negotiation (DR-228)
|
||||
//
|
||||
// Fixtures rather than a live server, but the *shapes* are real: every one
|
||||
// below was observed in a `PlaybackInfo` response from the development
|
||||
@@ -4242,11 +4242,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole point of DR-227: a source the server will serve untouched is
|
||||
/// The whole point of DR-228: a source the server will serve untouched is
|
||||
/// served untouched. Before this, every video play built an HLS transcode
|
||||
/// URL regardless.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227 | UT-212
|
||||
/// TRACES: UR-079 | DR-228 | UT-213
|
||||
#[test]
|
||||
fn test_a_supported_source_direct_plays() {
|
||||
let source = source_fixture();
|
||||
@@ -4259,7 +4259,7 @@ mod tests {
|
||||
/// The server can remux without re-encoding. That is not a transcode and
|
||||
/// must not be reported as one — the difference is a whole CPU core.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227 | UT-212
|
||||
/// TRACES: UR-079 | DR-228 | UT-213
|
||||
#[test]
|
||||
fn test_a_remuxable_source_direct_streams() {
|
||||
let source = NegotiatedSource {
|
||||
@@ -4278,7 +4278,7 @@ mod tests {
|
||||
/// An unsupported codec — the hevc that is ~80% of the sampled library,
|
||||
/// under the Linux h264-only profile — transcodes.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227 | UT-212
|
||||
/// TRACES: UR-079 | DR-228 | UT-213
|
||||
#[test]
|
||||
fn test_an_unsupported_source_transcodes() {
|
||||
let source = NegotiatedSource {
|
||||
@@ -4297,7 +4297,7 @@ mod tests {
|
||||
/// track the webview cannot decode, which renders as picture with no sound.
|
||||
/// The client's verdict has to win over the server's.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227, DR-148 | UT-212
|
||||
/// TRACES: UR-079 | DR-228, DR-148 | UT-213
|
||||
#[test]
|
||||
fn test_undecodable_audio_overrides_the_servers_direct_play_offer() {
|
||||
let source = source_fixture();
|
||||
@@ -4313,7 +4313,7 @@ mod tests {
|
||||
/// different one. Honouring the viewer's choice means asking the server to
|
||||
/// build a stream around it.
|
||||
///
|
||||
/// TRACES: UR-021, UR-079 | DR-227 | UT-212
|
||||
/// TRACES: UR-021, UR-079 | DR-228 | UT-213
|
||||
#[test]
|
||||
fn test_pinning_an_audio_track_forces_a_transcode() {
|
||||
let source = source_fixture();
|
||||
@@ -4329,7 +4329,7 @@ mod tests {
|
||||
/// arrives here as `supports_direct_play: false`; this pins the mapping so a
|
||||
/// future refactor cannot quietly direct-play past a cap.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225, DR-227 | UT-212
|
||||
/// TRACES: UR-074, UR-079 | DR-226, DR-228 | UT-213
|
||||
#[test]
|
||||
fn test_a_ceiling_below_the_source_bitrate_transcodes() {
|
||||
// 6.65 Mbps source, 2 Mbps ceiling — the server refuses direct play.
|
||||
@@ -4357,7 +4357,7 @@ mod tests {
|
||||
/// Direct play wins over direct stream when both are on offer: copying the
|
||||
/// file is strictly cheaper than repackaging it.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227 | UT-212
|
||||
/// TRACES: UR-079 | DR-228 | UT-213
|
||||
#[test]
|
||||
fn test_direct_play_is_preferred_over_direct_stream() {
|
||||
let source = source_fixture();
|
||||
@@ -4369,15 +4369,15 @@ mod tests {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Per-playback quality ceiling (DR-225)
|
||||
// Per-playback quality ceiling (DR-226)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// The defect DR-225 exists to fix: the in-player picker documented itself
|
||||
/// The defect DR-226 exists to fix: the in-player picker documented itself
|
||||
/// as a "this film, this connection" control but was implemented by writing
|
||||
/// the device default, so one awkward film silently capped everything played
|
||||
/// afterwards. The override must not touch the default.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225 | UT-212
|
||||
/// TRACES: UR-074, UR-079 | DR-226 | UT-213
|
||||
#[test]
|
||||
fn test_a_playback_override_does_not_disturb_the_device_default() {
|
||||
let _guard = QUALITY_LOCK.lock_safe();
|
||||
@@ -4410,7 +4410,7 @@ mod tests {
|
||||
/// autoplayed next episode is the case that matters, since nobody reopens
|
||||
/// the picker between episodes.
|
||||
///
|
||||
/// TRACES: UR-074, UR-079 | DR-225 | UT-212
|
||||
/// TRACES: UR-074, UR-079 | DR-226 | UT-213
|
||||
#[test]
|
||||
fn test_the_override_is_droppable_so_it_cannot_outlive_its_playback() {
|
||||
let _guard = QUALITY_LOCK.lock_safe();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! decides *how to deliver it*** — is the point. A multi-variant playlist handed
|
||||
//! to ExoPlayer is still ExoPlayer's to adapt over; Rust never paces bytes.
|
||||
//!
|
||||
//! TRACES: UR-079 | DR-224
|
||||
//! TRACES: UR-079 | DR-225
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -30,7 +30,7 @@ use crate::settings::StreamingQuality;
|
||||
/// 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
|
||||
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Transport {
|
||||
@@ -52,7 +52,7 @@ pub enum Transport {
|
||||
/// 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
|
||||
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PlaybackKind {
|
||||
@@ -73,7 +73,7 @@ impl PlaybackKind {
|
||||
/// that several seek/reload paths still branch on; this keeps the two from
|
||||
/// drifting by making one derive from the other.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227
|
||||
/// TRACES: UR-079 | DR-228
|
||||
pub fn needs_transcoding(&self) -> bool {
|
||||
matches!(self, PlaybackKind::Transcode)
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl PlaybackKind {
|
||||
/// 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
|
||||
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Rendition {
|
||||
@@ -108,7 +108,7 @@ pub struct Rendition {
|
||||
/// 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
|
||||
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QualityOption {
|
||||
@@ -133,7 +133,7 @@ pub struct QualityOption {
|
||||
///
|
||||
/// 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
|
||||
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StreamSelection {
|
||||
@@ -145,7 +145,7 @@ pub struct StreamSelection {
|
||||
pub playback_kind: PlaybackKind,
|
||||
/// The negotiated rendition; `None` when direct-playing the source as-is.
|
||||
pub rendition: Option<Rendition>,
|
||||
/// What this media source can offer, for the quality picker (DR-226).
|
||||
/// What this media source can offer, for the quality picker (DR-227).
|
||||
pub available: Vec<QualityOption>,
|
||||
/// The media source this selection is for, so a later re-open (quality
|
||||
/// change, audio-track switch, transcoded seek) targets the same one.
|
||||
@@ -160,7 +160,7 @@ pub struct StreamSelection {
|
||||
/// 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
|
||||
pub needs_transcoding: bool,
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ impl StreamSelection {
|
||||
/// source's — and offering a quality ladder over it would be a lie, since
|
||||
/// nothing about a local file can be re-negotiated.
|
||||
///
|
||||
/// TRACES: UR-071, UR-079 | DR-224
|
||||
/// TRACES: UR-071, UR-079 | DR-225
|
||||
pub fn local_file(url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
url: url.into(),
|
||||
@@ -199,7 +199,7 @@ impl StreamSelection {
|
||||
/// all). In that case nothing can be judged redundant and every rung is offered,
|
||||
/// which is the safe direction: the viewer keeps every choice they had before.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-226, DR-121 | UT-211
|
||||
/// TRACES: UR-070, UR-079 | DR-227, DR-121 | UT-212
|
||||
pub fn quality_options_for_source(source_bitrate: Option<u64>) -> Vec<QualityOption> {
|
||||
StreamingQuality::ALL
|
||||
.iter()
|
||||
@@ -226,7 +226,7 @@ mod tests {
|
||||
/// The tag the frontend matches on has to be exactly what it expects, and
|
||||
/// it is a *string in TypeScript* — nothing but a test keeps the two in step.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-224 | UT-211
|
||||
/// TRACES: UR-079 | DR-225 | UT-212
|
||||
#[test]
|
||||
fn test_transport_serialises_with_the_tag_the_frontend_matches() {
|
||||
let cases = [
|
||||
@@ -242,7 +242,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: UR-079 | DR-227 | UT-211
|
||||
/// TRACES: UR-079 | DR-228 | UT-212
|
||||
#[test]
|
||||
fn test_playback_kind_serialises_with_the_tag_the_frontend_matches() {
|
||||
let cases = [
|
||||
@@ -261,7 +261,7 @@ mod tests {
|
||||
/// Only a transcode costs the server encoder time. A direct *stream* is a
|
||||
/// remux — cheap, and not what `needs_transcoding` has ever meant.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-227 | UT-211
|
||||
/// TRACES: UR-079 | DR-228 | UT-212
|
||||
#[test]
|
||||
fn test_only_transcode_counts_as_transcoding() {
|
||||
assert!(PlaybackKind::Transcode.needs_transcoding());
|
||||
@@ -272,7 +272,7 @@ mod tests {
|
||||
/// A local file is a direct play over a local transport, with no ladder:
|
||||
/// nothing about a file on disk can be re-negotiated.
|
||||
///
|
||||
/// TRACES: UR-071, UR-079 | DR-224 | UT-211
|
||||
/// TRACES: UR-071, UR-079 | DR-225 | UT-212
|
||||
#[test]
|
||||
fn test_local_file_selection_offers_no_ladder() {
|
||||
let selection = StreamSelection::local_file("http://127.0.0.1:9000/media/x.mkv");
|
||||
@@ -286,7 +286,7 @@ mod tests {
|
||||
/// The camelCase rule applies to nested struct fields too, and
|
||||
/// `playbackKind` is the one the frontend branches on.
|
||||
///
|
||||
/// TRACES: UR-079 | DR-224 | UT-211
|
||||
/// TRACES: UR-079 | DR-225 | UT-212
|
||||
#[test]
|
||||
fn test_stream_selection_fields_are_camel_case_on_the_wire() {
|
||||
let selection = StreamSelection {
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
/// The measured library has 1.1 Mbps sources in it. Offering those a choice
|
||||
/// of 20, 10, 8, 4 and 2 Mbps is offering five ways to spell "Original".
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-226, DR-121 | UT-211
|
||||
/// TRACES: UR-070, UR-079 | DR-227, DR-121 | UT-212
|
||||
#[test]
|
||||
fn test_rungs_above_the_source_bitrate_are_marked_redundant() {
|
||||
let options = quality_options_for_source(Some(1_122_137));
|
||||
@@ -361,7 +361,7 @@ mod tests {
|
||||
/// `Original` is the source, so it is never "above" it — not even for a
|
||||
/// source whose bitrate is unknown or zero.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-226 | UT-211
|
||||
/// TRACES: UR-070, UR-079 | DR-227 | UT-212
|
||||
#[test]
|
||||
fn test_original_is_never_marked_as_exceeding_the_source() {
|
||||
for bitrate in [None, Some(0), Some(1), Some(50_000_000)] {
|
||||
@@ -377,7 +377,7 @@ mod tests {
|
||||
/// An `avi` with no reported bitrate must not lose the picker. Judging
|
||||
/// nothing redundant is the safe direction — the viewer keeps every choice.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-226 | UT-211
|
||||
/// TRACES: UR-070, UR-079 | DR-227 | UT-212
|
||||
#[test]
|
||||
fn test_an_unknown_source_bitrate_keeps_every_rung_offered() {
|
||||
let options = quality_options_for_source(None);
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
|
||||
/// A 4K remux constrains at every rung — the ladder is fully meaningful.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-226 | UT-211
|
||||
/// TRACES: UR-070, UR-079 | DR-227 | UT-212
|
||||
#[test]
|
||||
fn test_a_source_above_the_ladder_marks_nothing_redundant() {
|
||||
let options = quality_options_for_source(Some(40_000_000));
|
||||
@@ -401,7 +401,7 @@ mod tests {
|
||||
/// The picker's text comes from Rust, beside the numbers it describes, so a
|
||||
/// relabelled rung cannot drift out of step with what it does.
|
||||
///
|
||||
/// TRACES: UR-070, UR-079 | DR-226 | UT-211
|
||||
/// TRACES: UR-070, UR-079 | DR-227 | UT-212
|
||||
#[test]
|
||||
fn test_options_carry_the_ladder_labels() {
|
||||
let options = quality_options_for_source(Some(6_652_961));
|
||||
|
||||
@@ -474,9 +474,9 @@ pub struct LiveStreamInfo {
|
||||
/// 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
|
||||
pub transport: super::stream_selection::Transport,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user