fix(player): a duration of zero is not a duration
DR-251. Scrubbing was dead on Android because the seek bar had no scale: every position tick read `<position> / 0.0`. ExoPlayer reports C.TIME_UNSET until it has resolved a duration, and JellyTauPlayer.getDuration() maps that to 0.0. So the engine answered Some(0.0) rather than None, which satisfied every "unknown duration" fallback in the controller — `observed_duration()` was never consulted, and neither was the runtime the catalog had carried since long before anything started decoding. Zero is now read as "does not know yet" at each step, with a final fallback to the item's own duration. That fixes it for any engine that cannot answer, rather than for ExoPlayer specifically. Red first: the test asserts a controller whose engine reports nothing usable still reports the queued item's 1800s, and failed with None before the change. 790 Rust tests, clippy clean both ways.
This commit is contained in:
@@ -1102,12 +1102,34 @@ impl PlayerController {
|
||||
///
|
||||
/// TRACES: UR-005 | DR-178
|
||||
pub fn duration(&self) -> Option<f64> {
|
||||
// Zero is not a duration, it is an engine saying it does not know yet.
|
||||
//
|
||||
// ExoPlayer reports `C.TIME_UNSET` until it has resolved one, and
|
||||
// `JellyTauPlayer.getDuration()` maps that to `0.0` — so the engine
|
||||
// answers `Some(0.0)`, every "unknown duration" fallback below is
|
||||
// skipped, and the seek bar is left with no scale. That presents as
|
||||
// scrubbing being broken rather than as a duration that never arrived.
|
||||
//
|
||||
// The item usually knows: the catalog carried a runtime long before
|
||||
// anything started decoding.
|
||||
//
|
||||
// TRACES: UR-005, UR-040 | DR-251
|
||||
let usable = |d: f64| (d > 0.0).then_some(d);
|
||||
|
||||
self.backend
|
||||
.lock_safe()
|
||||
.snapshot()
|
||||
.duration
|
||||
.map(|d| d.as_secs_f64())
|
||||
.or_else(|| self.observed_duration())
|
||||
.and_then(usable)
|
||||
.or_else(|| self.observed_duration().and_then(usable))
|
||||
.or_else(|| {
|
||||
self.queue
|
||||
.lock_safe()
|
||||
.current()
|
||||
.and_then(|item| item.duration)
|
||||
.and_then(usable)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get queue reference
|
||||
@@ -2290,6 +2312,35 @@ impl Default for PlayerController {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
/// A duration the engine does not know must fall back to the one the item
|
||||
/// carries, and zero must count as "does not know".
|
||||
///
|
||||
/// ExoPlayer reports `C.TIME_UNSET` for a duration it has not resolved;
|
||||
/// `JellyTauPlayer.getDuration()` maps that to `0.0`, so the engine answers
|
||||
/// `Some(0.0)` rather than `None` and every "unknown duration" fallback is
|
||||
/// skipped. The seek bar then has no scale, which presents as scrubbing
|
||||
/// being dead rather than as a missing duration.
|
||||
///
|
||||
/// TRACES: UR-005, UR-040 | DR-251 | UT-221
|
||||
#[test]
|
||||
fn test_duration_falls_back_to_the_item_when_the_engine_does_not_know() {
|
||||
let controller = PlayerController::default();
|
||||
let mut item = MediaItem::sample("item-1", "https://example.invalid/a.mp4");
|
||||
item.duration = Some(1800.0);
|
||||
|
||||
{
|
||||
let queue_arc = controller.queue();
|
||||
let mut queue = queue_arc.lock_safe();
|
||||
queue.set_queue(vec![item], 0);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
controller.duration(),
|
||||
Some(1800.0),
|
||||
"an engine that cannot report a duration should not erase the one the item carries"
|
||||
);
|
||||
}
|
||||
use super::*;
|
||||
|
||||
/// Test emitter that captures events for asserting the HTML5 report methods
|
||||
|
||||
Reference in New Issue
Block a user