1use std::cell::RefCell;
39use std::rc::Rc;
40
41use gpui::SharedString;
42
43use crate::content::transport::{BufferedRange, TrackStep, TransportDuration, TransportState};
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum MediaOrigin {
48 Platform,
51 Fixture,
53}
54
55impl MediaOrigin {
56 pub fn name(self) -> &'static str {
59 match self {
60 Self::Platform => "platform",
61 Self::Fixture => "fixture",
62 }
63 }
64
65 pub fn is_fixture(self) -> bool {
66 matches!(self, Self::Fixture)
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
77pub enum MediaAvailability {
78 #[default]
80 Idle,
81 Loading,
83 NoBackend(SharedString),
86 Failed(SharedString),
88 Ready,
90}
91
92impl MediaAvailability {
93 pub fn name(&self) -> &'static str {
95 match self {
96 Self::Idle => "idle",
97 Self::Loading => "loading",
98 Self::NoBackend(_) => "no-backend",
99 Self::Failed(_) => "failed",
100 Self::Ready => "ready",
101 }
102 }
103
104 pub fn is_ready(&self) -> bool {
105 matches!(self, Self::Ready)
106 }
107
108 pub fn reason(&self) -> Option<SharedString> {
110 match self {
111 Self::NoBackend(reason) | Self::Failed(reason) => Some(reason.clone()),
112 _ => None,
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq)]
119pub struct MediaSnapshot {
120 pub availability: MediaAvailability,
121 pub state: TransportState,
122 pub position: f32,
124 pub duration: TransportDuration,
125 pub volume: f32,
126 pub muted: bool,
127 pub speed: f32,
128 pub buffered: Vec<BufferedRange>,
131}
132
133impl Default for MediaSnapshot {
134 fn default() -> Self {
135 Self {
136 availability: MediaAvailability::default(),
137 state: TransportState::Paused,
138 position: 0.0,
139 duration: TransportDuration::Unknown,
140 volume: 1.0,
141 muted: false,
142 speed: 1.0,
143 buffered: Vec::new(),
144 }
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq)]
150pub enum MediaCommand {
151 Play,
152 Pause,
153 Seek(f32),
155 SetVolume(f32),
156 ToggleMute,
157 SetSpeed(f32),
158 Step(TrackStep),
159}
160
161impl MediaCommand {
162 pub fn name(self) -> &'static str {
164 match self {
165 Self::Play => "play",
166 Self::Pause => "pause",
167 Self::Seek(_) => "seek",
168 Self::SetVolume(_) => "volume",
169 Self::ToggleMute => "mute",
170 Self::SetSpeed(_) => "speed",
171 Self::Step(_) => "step",
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq)]
178pub enum MediaOutcome {
179 Applied,
182 Refused(SharedString),
184 Unsupported,
188}
189
190impl MediaOutcome {
191 pub fn name(&self) -> &'static str {
192 match self {
193 Self::Applied => "applied",
194 Self::Refused(_) => "refused",
195 Self::Unsupported => "unsupported",
196 }
197 }
198}
199
200pub trait MediaTransport: std::fmt::Debug {
206 fn origin(&self) -> MediaOrigin;
208
209 fn snapshot(&self) -> MediaSnapshot;
211
212 fn apply(&self, command: MediaCommand) -> MediaOutcome;
214}
215
216#[derive(Debug, Clone, PartialEq)]
219pub enum MediaEvent {
220 Applied(MediaCommand),
222 Refused(MediaCommand, SharedString),
224 Unsupported(MediaCommand),
226}
227
228impl MediaEvent {
229 pub fn of(command: MediaCommand, outcome: MediaOutcome) -> Self {
231 match outcome {
232 MediaOutcome::Applied => Self::Applied(command),
233 MediaOutcome::Refused(reason) => Self::Refused(command, reason),
234 MediaOutcome::Unsupported => Self::Unsupported(command),
235 }
236 }
237
238 pub fn command(&self) -> MediaCommand {
239 match self {
240 Self::Applied(command) | Self::Refused(command, _) | Self::Unsupported(command) => {
241 *command
242 }
243 }
244 }
245}
246
247pub struct FixtureTransport {
258 snapshot: RefCell<MediaSnapshot>,
259 commands: RefCell<Vec<MediaCommand>>,
260 refusal: Option<SharedString>,
262 unsupported: Vec<&'static str>,
263}
264
265impl std::fmt::Debug for FixtureTransport {
266 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 formatter
268 .debug_struct("FixtureTransport")
269 .field("snapshot", &self.snapshot.borrow())
270 .field("commands", &self.commands.borrow().len())
271 .field("refuses", &self.refusal.is_some())
272 .finish()
273 }
274}
275
276impl Default for FixtureTransport {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282impl FixtureTransport {
283 pub fn new() -> Self {
285 Self {
286 snapshot: RefCell::new(MediaSnapshot::default()),
287 commands: RefCell::new(Vec::new()),
288 refusal: None,
289 unsupported: Vec::new(),
290 }
291 }
292
293 pub fn ready(duration: f32) -> Self {
295 let mut fixture = Self::new();
296 {
297 let snapshot = fixture.snapshot.get_mut();
298 snapshot.availability = MediaAvailability::Ready;
299 snapshot.duration = TransportDuration::Known(duration.max(0.0));
300 }
301 fixture
302 }
303
304 pub fn live() -> Self {
306 let mut fixture = Self::new();
307 {
308 let snapshot = fixture.snapshot.get_mut();
309 snapshot.availability = MediaAvailability::Ready;
310 snapshot.duration = TransportDuration::Unknown;
311 }
312 fixture
313 }
314
315 pub fn state(mut self, state: TransportState) -> Self {
316 self.snapshot.get_mut().state = state;
317 self
318 }
319
320 pub fn position(mut self, seconds: f32) -> Self {
322 self.snapshot.get_mut().position = seconds.max(0.0);
323 self
324 }
325
326 pub fn volume(mut self, volume: f32) -> Self {
327 self.snapshot.get_mut().volume = volume.clamp(0.0, 1.0);
328 self
329 }
330
331 pub fn muted(mut self, muted: bool) -> Self {
332 self.snapshot.get_mut().muted = muted;
333 self
334 }
335
336 pub fn speed(mut self, speed: f32) -> Self {
337 self.snapshot.get_mut().speed = speed.max(f32::EPSILON);
338 self
339 }
340
341 pub fn buffered(mut self, ranges: impl IntoIterator<Item = BufferedRange>) -> Self {
342 self.snapshot.get_mut().buffered = ranges.into_iter().collect();
343 self
344 }
345
346 pub fn loading(mut self) -> Self {
347 self.snapshot.get_mut().availability = MediaAvailability::Loading;
348 self
349 }
350
351 pub fn no_backend(mut self, reason: impl Into<SharedString>) -> Self {
353 self.snapshot.get_mut().availability = MediaAvailability::NoBackend(reason.into());
354 self
355 }
356
357 pub fn failed(mut self, reason: impl Into<SharedString>) -> Self {
359 self.snapshot.get_mut().availability = MediaAvailability::Failed(reason.into());
360 self
361 }
362
363 pub fn refusing(mut self, reason: impl Into<SharedString>) -> Self {
366 self.refusal = Some(reason.into());
367 self
368 }
369
370 pub fn unsupported(mut self, commands: impl IntoIterator<Item = &'static str>) -> Self {
373 self.unsupported = commands.into_iter().collect();
374 self
375 }
376
377 pub fn commands(&self) -> Vec<MediaCommand> {
379 self.commands.borrow().clone()
380 }
381
382 pub fn shared(self) -> Rc<dyn MediaTransport> {
384 Rc::new(self)
385 }
386}
387
388impl MediaTransport for FixtureTransport {
389 fn origin(&self) -> MediaOrigin {
390 MediaOrigin::Fixture
391 }
392
393 fn snapshot(&self) -> MediaSnapshot {
394 self.snapshot.borrow().clone()
395 }
396
397 fn apply(&self, command: MediaCommand) -> MediaOutcome {
398 self.commands.borrow_mut().push(command);
399 if self.unsupported.contains(&command.name()) {
400 return MediaOutcome::Unsupported;
401 }
402 if let Some(reason) = &self.refusal {
403 return MediaOutcome::Refused(reason.clone());
404 }
405 let mut snapshot = self.snapshot.borrow_mut();
406 if !snapshot.availability.is_ready() {
409 return MediaOutcome::Unsupported;
410 }
411 match command {
412 MediaCommand::Play => snapshot.state = TransportState::Playing,
413 MediaCommand::Pause => snapshot.state = TransportState::Paused,
414 MediaCommand::Seek(seconds) => {
415 let clamped = match snapshot.duration.seconds() {
416 Some(total) => seconds.clamp(0.0, total),
417 None => seconds.max(0.0),
418 };
419 snapshot.position = clamped;
420 }
421 MediaCommand::SetVolume(volume) => snapshot.volume = volume.clamp(0.0, 1.0),
422 MediaCommand::ToggleMute => snapshot.muted = !snapshot.muted,
423 MediaCommand::SetSpeed(speed) => snapshot.speed = speed.max(f32::EPSILON),
424 MediaCommand::Step(_) => return MediaOutcome::Unsupported,
427 }
428 MediaOutcome::Applied
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn a_fixture_never_advances_its_own_head() {
438 let fixture = FixtureTransport::ready(120.0).position(30.0);
439 assert_eq!(fixture.apply(MediaCommand::Play), MediaOutcome::Applied);
440 assert_eq!(fixture.snapshot().state, TransportState::Playing);
441 assert_eq!(
442 fixture.snapshot().position,
443 30.0,
444 "playing a fixture decodes nothing, so nothing moves"
445 );
446 }
447
448 #[test]
449 fn a_seek_stops_at_a_known_end_and_only_at_zero_without_one() {
450 let bounded = FixtureTransport::ready(120.0);
451 bounded.apply(MediaCommand::Seek(500.0));
452 assert_eq!(bounded.snapshot().position, 120.0);
453
454 let live = FixtureTransport::live();
455 live.apply(MediaCommand::Seek(-5.0));
456 assert_eq!(live.snapshot().position, 0.0);
457 live.apply(MediaCommand::Seek(500.0));
458 assert_eq!(live.snapshot().position, 500.0);
459 }
460
461 #[test]
462 fn a_refusing_transport_changes_nothing_and_says_why() {
463 let fixture = FixtureTransport::ready(120.0).refusing("The device is in use.");
464 let outcome = fixture.apply(MediaCommand::Play);
465 assert_eq!(
466 outcome,
467 MediaOutcome::Refused(SharedString::from("The device is in use."))
468 );
469 assert_eq!(
470 fixture.snapshot().state,
471 TransportState::Paused,
472 "a refused command must leave the state that still holds"
473 );
474 assert_eq!(fixture.commands(), vec![MediaCommand::Play]);
475 }
476
477 #[test]
478 fn a_transport_holding_nothing_takes_no_command() {
479 let idle = FixtureTransport::new();
480 assert_eq!(idle.apply(MediaCommand::Play), MediaOutcome::Unsupported);
481 assert_eq!(idle.snapshot().state, TransportState::Paused);
482
483 let absent = FixtureTransport::new().no_backend("No decoder for AV1 on this machine.");
484 assert_eq!(absent.apply(MediaCommand::Play), MediaOutcome::Unsupported);
485 assert_eq!(
486 absent.snapshot().availability.name(),
487 "no-backend",
488 "a machine that cannot play it is not a machine that is loading it"
489 );
490 }
491
492 #[test]
493 fn an_event_carries_the_command_and_what_became_of_it() {
494 assert_eq!(
495 MediaEvent::of(MediaCommand::Pause, MediaOutcome::Applied),
496 MediaEvent::Applied(MediaCommand::Pause)
497 );
498 let refused = MediaEvent::of(
499 MediaCommand::Seek(4.0),
500 MediaOutcome::Refused(SharedString::from("Seeking a live stream is refused.")),
501 );
502 assert_eq!(refused.command(), MediaCommand::Seek(4.0));
503 assert!(matches!(refused, MediaEvent::Refused(_, _)));
504 }
505
506 #[test]
507 fn a_fixture_says_it_is_a_fixture() {
508 assert!(FixtureTransport::new().origin().is_fixture());
509 assert_eq!(MediaOrigin::Platform.name(), "platform");
510 }
511}