use std::cell::RefCell;
use std::rc::Rc;
use gpui::SharedString;
use crate::content::transport::{BufferedRange, TrackStep, TransportDuration, TransportState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaOrigin {
Platform,
Fixture,
}
impl MediaOrigin {
pub fn name(self) -> &'static str {
match self {
Self::Platform => "platform",
Self::Fixture => "fixture",
}
}
pub fn is_fixture(self) -> bool {
matches!(self, Self::Fixture)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum MediaAvailability {
#[default]
Idle,
Loading,
NoBackend(SharedString),
Failed(SharedString),
Ready,
}
impl MediaAvailability {
pub fn name(&self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Loading => "loading",
Self::NoBackend(_) => "no-backend",
Self::Failed(_) => "failed",
Self::Ready => "ready",
}
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready)
}
pub fn reason(&self) -> Option<SharedString> {
match self {
Self::NoBackend(reason) | Self::Failed(reason) => Some(reason.clone()),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MediaSnapshot {
pub availability: MediaAvailability,
pub state: TransportState,
pub position: f32,
pub duration: TransportDuration,
pub volume: f32,
pub muted: bool,
pub speed: f32,
pub buffered: Vec<BufferedRange>,
}
impl Default for MediaSnapshot {
fn default() -> Self {
Self {
availability: MediaAvailability::default(),
state: TransportState::Paused,
position: 0.0,
duration: TransportDuration::Unknown,
volume: 1.0,
muted: false,
speed: 1.0,
buffered: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MediaCommand {
Play,
Pause,
Seek(f32),
SetVolume(f32),
ToggleMute,
SetSpeed(f32),
Step(TrackStep),
}
impl MediaCommand {
pub fn name(self) -> &'static str {
match self {
Self::Play => "play",
Self::Pause => "pause",
Self::Seek(_) => "seek",
Self::SetVolume(_) => "volume",
Self::ToggleMute => "mute",
Self::SetSpeed(_) => "speed",
Self::Step(_) => "step",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MediaOutcome {
Applied,
Refused(SharedString),
Unsupported,
}
impl MediaOutcome {
pub fn name(&self) -> &'static str {
match self {
Self::Applied => "applied",
Self::Refused(_) => "refused",
Self::Unsupported => "unsupported",
}
}
}
pub trait MediaTransport: std::fmt::Debug {
fn origin(&self) -> MediaOrigin;
fn snapshot(&self) -> MediaSnapshot;
fn apply(&self, command: MediaCommand) -> MediaOutcome;
}
#[derive(Debug, Clone, PartialEq)]
pub enum MediaEvent {
Applied(MediaCommand),
Refused(MediaCommand, SharedString),
Unsupported(MediaCommand),
}
impl MediaEvent {
pub fn of(command: MediaCommand, outcome: MediaOutcome) -> Self {
match outcome {
MediaOutcome::Applied => Self::Applied(command),
MediaOutcome::Refused(reason) => Self::Refused(command, reason),
MediaOutcome::Unsupported => Self::Unsupported(command),
}
}
pub fn command(&self) -> MediaCommand {
match self {
Self::Applied(command) | Self::Refused(command, _) | Self::Unsupported(command) => {
*command
}
}
}
}
pub struct FixtureTransport {
snapshot: RefCell<MediaSnapshot>,
commands: RefCell<Vec<MediaCommand>>,
refusal: Option<SharedString>,
unsupported: Vec<&'static str>,
}
impl std::fmt::Debug for FixtureTransport {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FixtureTransport")
.field("snapshot", &self.snapshot.borrow())
.field("commands", &self.commands.borrow().len())
.field("refuses", &self.refusal.is_some())
.finish()
}
}
impl Default for FixtureTransport {
fn default() -> Self {
Self::new()
}
}
impl FixtureTransport {
pub fn new() -> Self {
Self {
snapshot: RefCell::new(MediaSnapshot::default()),
commands: RefCell::new(Vec::new()),
refusal: None,
unsupported: Vec::new(),
}
}
pub fn ready(duration: f32) -> Self {
let mut fixture = Self::new();
{
let snapshot = fixture.snapshot.get_mut();
snapshot.availability = MediaAvailability::Ready;
snapshot.duration = TransportDuration::Known(duration.max(0.0));
}
fixture
}
pub fn live() -> Self {
let mut fixture = Self::new();
{
let snapshot = fixture.snapshot.get_mut();
snapshot.availability = MediaAvailability::Ready;
snapshot.duration = TransportDuration::Unknown;
}
fixture
}
pub fn state(mut self, state: TransportState) -> Self {
self.snapshot.get_mut().state = state;
self
}
pub fn position(mut self, seconds: f32) -> Self {
self.snapshot.get_mut().position = seconds.max(0.0);
self
}
pub fn volume(mut self, volume: f32) -> Self {
self.snapshot.get_mut().volume = volume.clamp(0.0, 1.0);
self
}
pub fn muted(mut self, muted: bool) -> Self {
self.snapshot.get_mut().muted = muted;
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.snapshot.get_mut().speed = speed.max(f32::EPSILON);
self
}
pub fn buffered(mut self, ranges: impl IntoIterator<Item = BufferedRange>) -> Self {
self.snapshot.get_mut().buffered = ranges.into_iter().collect();
self
}
pub fn loading(mut self) -> Self {
self.snapshot.get_mut().availability = MediaAvailability::Loading;
self
}
pub fn no_backend(mut self, reason: impl Into<SharedString>) -> Self {
self.snapshot.get_mut().availability = MediaAvailability::NoBackend(reason.into());
self
}
pub fn failed(mut self, reason: impl Into<SharedString>) -> Self {
self.snapshot.get_mut().availability = MediaAvailability::Failed(reason.into());
self
}
pub fn refusing(mut self, reason: impl Into<SharedString>) -> Self {
self.refusal = Some(reason.into());
self
}
pub fn unsupported(mut self, commands: impl IntoIterator<Item = &'static str>) -> Self {
self.unsupported = commands.into_iter().collect();
self
}
pub fn commands(&self) -> Vec<MediaCommand> {
self.commands.borrow().clone()
}
pub fn shared(self) -> Rc<dyn MediaTransport> {
Rc::new(self)
}
}
impl MediaTransport for FixtureTransport {
fn origin(&self) -> MediaOrigin {
MediaOrigin::Fixture
}
fn snapshot(&self) -> MediaSnapshot {
self.snapshot.borrow().clone()
}
fn apply(&self, command: MediaCommand) -> MediaOutcome {
self.commands.borrow_mut().push(command);
if self.unsupported.contains(&command.name()) {
return MediaOutcome::Unsupported;
}
if let Some(reason) = &self.refusal {
return MediaOutcome::Refused(reason.clone());
}
let mut snapshot = self.snapshot.borrow_mut();
if !snapshot.availability.is_ready() {
return MediaOutcome::Unsupported;
}
match command {
MediaCommand::Play => snapshot.state = TransportState::Playing,
MediaCommand::Pause => snapshot.state = TransportState::Paused,
MediaCommand::Seek(seconds) => {
let clamped = match snapshot.duration.seconds() {
Some(total) => seconds.clamp(0.0, total),
None => seconds.max(0.0),
};
snapshot.position = clamped;
}
MediaCommand::SetVolume(volume) => snapshot.volume = volume.clamp(0.0, 1.0),
MediaCommand::ToggleMute => snapshot.muted = !snapshot.muted,
MediaCommand::SetSpeed(speed) => snapshot.speed = speed.max(f32::EPSILON),
MediaCommand::Step(_) => return MediaOutcome::Unsupported,
}
MediaOutcome::Applied
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fixture_never_advances_its_own_head() {
let fixture = FixtureTransport::ready(120.0).position(30.0);
assert_eq!(fixture.apply(MediaCommand::Play), MediaOutcome::Applied);
assert_eq!(fixture.snapshot().state, TransportState::Playing);
assert_eq!(
fixture.snapshot().position,
30.0,
"playing a fixture decodes nothing, so nothing moves"
);
}
#[test]
fn a_seek_stops_at_a_known_end_and_only_at_zero_without_one() {
let bounded = FixtureTransport::ready(120.0);
bounded.apply(MediaCommand::Seek(500.0));
assert_eq!(bounded.snapshot().position, 120.0);
let live = FixtureTransport::live();
live.apply(MediaCommand::Seek(-5.0));
assert_eq!(live.snapshot().position, 0.0);
live.apply(MediaCommand::Seek(500.0));
assert_eq!(live.snapshot().position, 500.0);
}
#[test]
fn a_refusing_transport_changes_nothing_and_says_why() {
let fixture = FixtureTransport::ready(120.0).refusing("The device is in use.");
let outcome = fixture.apply(MediaCommand::Play);
assert_eq!(
outcome,
MediaOutcome::Refused(SharedString::from("The device is in use."))
);
assert_eq!(
fixture.snapshot().state,
TransportState::Paused,
"a refused command must leave the state that still holds"
);
assert_eq!(fixture.commands(), vec![MediaCommand::Play]);
}
#[test]
fn a_transport_holding_nothing_takes_no_command() {
let idle = FixtureTransport::new();
assert_eq!(idle.apply(MediaCommand::Play), MediaOutcome::Unsupported);
assert_eq!(idle.snapshot().state, TransportState::Paused);
let absent = FixtureTransport::new().no_backend("No decoder for AV1 on this machine.");
assert_eq!(absent.apply(MediaCommand::Play), MediaOutcome::Unsupported);
assert_eq!(
absent.snapshot().availability.name(),
"no-backend",
"a machine that cannot play it is not a machine that is loading it"
);
}
#[test]
fn an_event_carries_the_command_and_what_became_of_it() {
assert_eq!(
MediaEvent::of(MediaCommand::Pause, MediaOutcome::Applied),
MediaEvent::Applied(MediaCommand::Pause)
);
let refused = MediaEvent::of(
MediaCommand::Seek(4.0),
MediaOutcome::Refused(SharedString::from("Seeking a live stream is refused.")),
);
assert_eq!(refused.command(), MediaCommand::Seek(4.0));
assert!(matches!(refused, MediaEvent::Refused(_, _)));
}
#[test]
fn a_fixture_says_it_is_a_fixture() {
assert!(FixtureTransport::new().origin().is_fixture());
assert_eq!(MediaOrigin::Platform.name(), "platform");
}
}