//! A [`MediaPlayer`] over the old [`PlayerBackend`] trait. //! //! Two purposes. //! //! **Migration.** Engines not yet ported — ExoPlayer, the webview element, the //! null backend — keep working while `PlayerController` moves onto the new //! contract (DR-245). Without this the port would have to land all four engines //! at once. //! //! **Evidence.** It reproduces exactly what every caller used to do: `load`, //! then `play`, then `seek` for a start position. Running the conformance suite //! against it therefore shows the old path failing the cases the new one passes, //! on the same engine and the same media — which is the difference between //! asserting that a design was wrong and demonstrating it. //! //! It is deliberately a faithful reproduction, not a fixed-up one. Making it //! pass would defeat the point. //! //! TRACES: UR-081 | DR-245 #![allow(dead_code)] // Consumed when PlayerController is ported (DR-245). use std::time::Duration; use super::backend::{PlayerBackend, PlayerError}; use super::media_player::{Capabilities, MediaPlayer, OpenRequest, Phase, PlaybackSnapshot}; use super::state::PlayerState; pub struct LegacyPlayer { inner: B, /// The old trait has no notion of "opening", so this is the best the wrapper /// can do: it knows an item was handed over, not whether the engine is ready /// for one. That gap is the whole problem. has_item: bool, } impl LegacyPlayer { pub fn new(inner: B) -> Self { Self { inner, has_item: false, } } pub fn inner_mut(&mut self) -> &mut B { &mut self.inner } } impl MediaPlayer for LegacyPlayer { /// Load, play, then seek — the sequence every caller used to write. /// /// The seek is issued immediately, because a caller has no way to know when /// the engine becomes ready. On an engine whose load is asynchronous it /// fails and is discarded, and playback begins at zero: DR-241, reproduced. fn open(&mut self, req: OpenRequest) -> Result<(), PlayerError> { self.inner.load(&req.media)?; self.has_item = true; if req.autoplay { self.inner.play()?; } if !req.start.is_zero() { // Faithfully ignoring the failure, exactly as the old callers did. let _ = self.inner.seek(req.start.as_secs_f64()); } Ok(()) } fn play(&mut self) -> Result<(), PlayerError> { self.inner.play() } fn pause(&mut self) -> Result<(), PlayerError> { self.inner.pause() } fn close(&mut self) -> Result<(), PlayerError> { self.has_item = false; self.inner.stop() } fn seek(&mut self, to: Duration) -> Result<(), PlayerError> { self.inner.seek(to.as_secs_f64()) } fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> { self.inner.set_volume(volume) } /// The old trait has no mute. Folding it into volume would lose the user's /// level, so this reports unsupported rather than pretending. fn set_muted(&mut self, _muted: bool) -> Result<(), PlayerError> { Err(PlayerError { message: "mute is not supported by this backend".to_string(), }) } fn set_rate(&mut self, _rate: f64) -> Result<(), PlayerError> { Err(PlayerError { message: "playback rate is not supported by this backend".to_string(), }) } fn select_audio_track(&mut self, index: Option) -> Result<(), PlayerError> { self.inner.set_audio_track(index.unwrap_or(-1)) } fn select_subtitle_track(&mut self, index: Option) -> Result<(), PlayerError> { self.inner.set_subtitle_track(index) } fn snapshot(&self) -> PlaybackSnapshot { let phase = match self.inner.state() { _ if !self.has_item => Phase::Idle, PlayerState::Playing { .. } => Phase::Playing, PlayerState::Paused { .. } => Phase::Paused, PlayerState::Idle => Phase::Idle, PlayerState::Error { error, .. } => Phase::Failed(error), // `Loading` is the closest the old trait comes to an opening state, // but it is set once the engine has accepted the item rather than // while it is still accepting it — which is precisely the window it // cannot describe. PlayerState::Loading { .. } | PlayerState::Seeking { .. } => Phase::Ready, }; PlaybackSnapshot { phase, position: Duration::from_secs_f64(self.inner.position().max(0.0)), duration: self.inner.duration().map(Duration::from_secs_f64), seekable: true, volume: self.inner.volume(), muted: false, rate: 1.0, audio_track: None, subtitle_track: None, } } fn capabilities(&self) -> Capabilities { Capabilities { video: false, audio_settings: true, subtitle_switching: true, audio_track_switching: true, } } }