1use super::media::MediaItem;
2use super::state::PlayerState;
3use crate::settings::AudioSettings;
4
5#[derive(Debug, Clone)]
7pub struct PlayerError {
8 pub message: String,
9}
10
11impl std::fmt::Display for PlayerError {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 write!(f, "{}", self.message)
14 }
15}
16
17impl std::error::Error for PlayerError {}
18
19impl PlayerError {
20 pub fn not_implemented() -> Self {
21 Self {
22 message: "Not implemented".to_string(),
23 }
24 }
25
26 #[cfg(target_os = "android")]
30 pub fn playback_failed<S: Into<String>>(message: S) -> Self {
31 Self {
32 message: message.into(),
33 }
34 }
35}
36
37pub trait PlayerBackend: Send + Sync {
41 fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError>;
44
45 fn play(&mut self) -> Result<(), PlayerError>;
48
49 fn pause(&mut self) -> Result<(), PlayerError>;
52
53 fn stop(&mut self) -> Result<(), PlayerError>;
56
57 fn seek(&mut self, position: f64) -> Result<(), PlayerError>;
60
61 fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError>;
64
65 fn position(&self) -> f64;
67
68 fn duration(&self) -> Option<f64>;
70
71 fn state(&self) -> PlayerState;
73
74 fn volume(&self) -> f32;
76
77 fn set_audio_settings(&mut self, _settings: &AudioSettings) -> Result<(), PlayerError> {
86 Ok(())
88 }
89
90 fn audio_settings(&self) -> AudioSettings {
96 AudioSettings::default()
97 }
98
99 fn set_audio_track(&mut self, _stream_index: i32) -> Result<(), PlayerError> {
108 Err(PlayerError::not_implemented())
110 }
111
112 fn set_subtitle_track(&mut self, _stream_index: Option<i32>) -> Result<(), PlayerError> {
121 Err(PlayerError::not_implemented())
123 }
124}
125
126pub struct NullBackend {
130 state: PlayerState,
131 volume: f32,
132 position: f64,
133 duration: Option<f64>,
134 audio_settings: AudioSettings,
135}
136
137impl Default for NullBackend {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl NullBackend {
144 pub fn new() -> Self {
145 Self {
146 state: PlayerState::Idle,
147 volume: 1.0,
148 position: 0.0,
149 duration: None,
150 audio_settings: AudioSettings::default(),
151 }
152 }
153}
154
155impl PlayerBackend for NullBackend {
156 fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError> {
157 self.state = PlayerState::Loading {
158 media: media.clone(),
159 };
160 self.duration = media.duration;
162 self.position = 0.0;
163 self.state = PlayerState::Paused {
164 media: media.clone(),
165 position: 0.0,
166 duration: media.duration.unwrap_or(0.0),
167 };
168 Ok(())
169 }
170
171 fn play(&mut self) -> Result<(), PlayerError> {
172 if let PlayerState::Paused {
173 media,
174 position,
175 duration,
176 } = &self.state
177 {
178 self.state = PlayerState::Playing {
179 media: media.clone(),
180 position: *position,
181 duration: *duration,
182 };
183 }
184 Ok(())
185 }
186
187 fn pause(&mut self) -> Result<(), PlayerError> {
188 if let PlayerState::Playing {
189 media,
190 position,
191 duration,
192 } = &self.state
193 {
194 self.state = PlayerState::Paused {
195 media: media.clone(),
196 position: *position,
197 duration: *duration,
198 };
199 }
200 Ok(())
201 }
202
203 fn stop(&mut self) -> Result<(), PlayerError> {
204 self.state = PlayerState::Idle;
205 self.position = 0.0;
206 self.duration = None;
207 Ok(())
208 }
209
210 fn seek(&mut self, position: f64) -> Result<(), PlayerError> {
211 self.position = position;
212 match &mut self.state {
213 PlayerState::Playing { position: pos, .. } => *pos = position,
214 PlayerState::Paused { position: pos, .. } => *pos = position,
215 _ => {}
216 }
217 Ok(())
218 }
219
220 fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> {
221 self.volume = volume.clamp(0.0, 1.0);
222 Ok(())
223 }
224
225 fn position(&self) -> f64 {
226 self.position
227 }
228
229 fn duration(&self) -> Option<f64> {
230 self.duration
231 }
232
233 fn state(&self) -> PlayerState {
234 self.state.clone()
235 }
236
237 fn volume(&self) -> f32 {
238 self.volume
239 }
240
241 fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> {
242 self.audio_settings = settings.clone().with_crossfade_clamped();
243 Ok(())
244 }
245
246 fn audio_settings(&self) -> AudioSettings {
247 self.audio_settings.clone()
248 }
249}
250
251impl PlayerBackend for Box<dyn PlayerBackend> {
258 fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError> {
259 (**self).load(media)
260 }
261 fn play(&mut self) -> Result<(), PlayerError> {
262 (**self).play()
263 }
264 fn pause(&mut self) -> Result<(), PlayerError> {
265 (**self).pause()
266 }
267 fn stop(&mut self) -> Result<(), PlayerError> {
268 (**self).stop()
269 }
270 fn seek(&mut self, position: f64) -> Result<(), PlayerError> {
271 (**self).seek(position)
272 }
273 fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> {
274 (**self).set_volume(volume)
275 }
276 fn position(&self) -> f64 {
277 (**self).position()
278 }
279 fn duration(&self) -> Option<f64> {
280 (**self).duration()
281 }
282 fn state(&self) -> PlayerState {
283 (**self).state()
284 }
285 fn volume(&self) -> f32 {
286 (**self).volume()
287 }
288 fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> {
289 (**self).set_audio_settings(settings)
290 }
291 fn audio_settings(&self) -> AudioSettings {
292 (**self).audio_settings()
293 }
294 fn set_audio_track(&mut self, stream_index: i32) -> Result<(), PlayerError> {
295 (**self).set_audio_track(stream_index)
296 }
297 fn set_subtitle_track(&mut self, stream_index: Option<i32>) -> Result<(), PlayerError> {
298 (**self).set_subtitle_track(stream_index)
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
309 fn test_null_backend_volume_default() {
310 let backend = NullBackend::new();
311 assert_eq!(backend.volume(), 1.0);
312 }
313
314 #[test]
319 fn test_null_backend_set_volume() {
320 let mut backend = NullBackend::new();
321 backend.set_volume(0.5).unwrap();
322 assert_eq!(backend.volume(), 0.5);
323 }
324
325 #[test]
330 fn test_null_backend_volume_clamping_high() {
331 let mut backend = NullBackend::new();
332 backend.set_volume(1.5).unwrap();
333 assert_eq!(backend.volume(), 1.0);
334 }
335
336 #[test]
341 fn test_null_backend_volume_clamping_low() {
342 let mut backend = NullBackend::new();
343 backend.set_volume(-0.5).unwrap();
344 assert_eq!(backend.volume(), 0.0);
345 }
346
347 #[test]
352 fn test_null_backend_volume_boundary() {
353 let mut backend = NullBackend::new();
354
355 backend.set_volume(0.0).unwrap();
356 assert_eq!(backend.volume(), 0.0);
357
358 backend.set_volume(1.0).unwrap();
359 assert_eq!(backend.volume(), 1.0);
360 }
361
362 #[test]
368 fn test_null_backend_audio_settings_default() {
369 let backend = NullBackend::new();
370 let settings = backend.audio_settings();
371 assert_eq!(settings.crossfade_duration, 0.0);
372 assert!(settings.gapless_playback);
373 assert!(!settings.normalize_volume);
374 }
375
376 #[test]
382 fn test_null_backend_set_audio_settings() {
383 use crate::settings::VolumeLevel;
384
385 let mut backend = NullBackend::new();
386 let settings = AudioSettings {
387 crossfade_duration: 5.0,
388 gapless_playback: false,
389 normalize_volume: true,
390 volume_level: VolumeLevel::Loud,
391 ..Default::default()
392 };
393
394 backend.set_audio_settings(&settings).unwrap();
395
396 let result = backend.audio_settings();
397 assert_eq!(result.crossfade_duration, 5.0);
398 assert!(!result.gapless_playback);
399 assert!(result.normalize_volume);
400 assert_eq!(result.volume_level, VolumeLevel::Loud);
401 }
402
403 #[test]
407 fn test_null_backend_audio_settings_crossfade_clamping() {
408 let mut backend = NullBackend::new();
409 let settings = AudioSettings {
410 crossfade_duration: 20.0,
411 ..Default::default()
412 };
413
414 backend.set_audio_settings(&settings).unwrap();
415 assert_eq!(backend.audio_settings().crossfade_duration, 12.0);
416 }
417
418 #[test]
423 fn test_null_backend_seek_updates_position() {
424 use crate::player::media::{MediaItem, MediaSource, MediaType};
425
426 let mut backend = NullBackend::new();
427
428 let media = MediaItem {
430 transport: None,
432 id: "test_media".to_string(),
433 title: "Test Track".to_string(),
434 name: Some("Test Track".to_string()),
435 artist: Some("Test Artist".to_string()),
436 album: Some("Test Album".to_string()),
437 album_name: Some("Test Album".to_string()),
438 album_id: None,
439 artist_items: None,
440 artists: Some(vec!["Test Artist".to_string()]),
441 primary_image_tag: None,
442 image_id: None,
443 item_type: Some("Audio".to_string()),
444 playlist_id: None,
445 duration: Some(180.0),
446 artwork_url: None,
447 media_type: MediaType::Audio,
448 source: MediaSource::DirectUrl {
449 url: "http://example.com/test.mp3".to_string(),
450 },
451 video_codec: None,
452 needs_transcoding: false,
453 video_width: None,
454 video_height: None,
455 subtitles: vec![],
456 series_id: None,
457 server_id: None,
458 };
459
460 backend.load(&media).unwrap();
462 backend.play().unwrap();
463
464 assert_eq!(backend.position(), 0.0);
466
467 backend.seek(30.0).unwrap();
469 assert_eq!(backend.position(), 30.0);
470
471 backend.seek(60.0).unwrap();
473 assert_eq!(backend.position(), 60.0);
474
475 backend.seek(15.0).unwrap();
477 assert_eq!(backend.position(), 15.0);
478 }
479
480 #[test]
485 fn test_null_backend_seek_while_paused() {
486 use crate::player::media::{MediaItem, MediaSource, MediaType};
487
488 let mut backend = NullBackend::new();
489
490 let media = MediaItem {
491 transport: None,
493 id: "test_media".to_string(),
494 title: "Test Track".to_string(),
495 name: Some("Test Track".to_string()),
496 artist: Some("Test Artist".to_string()),
497 album: Some("Test Album".to_string()),
498 album_name: Some("Test Album".to_string()),
499 album_id: None,
500 artist_items: None,
501 artists: Some(vec!["Test Artist".to_string()]),
502 primary_image_tag: None,
503 image_id: None,
504 item_type: Some("Audio".to_string()),
505 playlist_id: None,
506 duration: Some(180.0),
507 artwork_url: None,
508 media_type: MediaType::Audio,
509 source: MediaSource::DirectUrl {
510 url: "http://example.com/test.mp3".to_string(),
511 },
512 video_codec: None,
513 needs_transcoding: false,
514 video_width: None,
515 video_height: None,
516 subtitles: vec![],
517 series_id: None,
518 server_id: None,
519 };
520
521 backend.load(&media).unwrap();
523
524 assert!(matches!(backend.state(), PlayerState::Paused { .. }));
526
527 backend.seek(45.0).unwrap();
529 assert_eq!(backend.position(), 45.0);
530
531 assert!(matches!(backend.state(), PlayerState::Paused { .. }));
533 }
534
535 #[test]
540 fn test_null_backend_position_updates_in_state() {
541 use crate::player::media::{MediaItem, MediaSource, MediaType};
542
543 let mut backend = NullBackend::new();
544
545 let media = MediaItem {
546 transport: None,
548 id: "test_media".to_string(),
549 title: "Test Track".to_string(),
550 name: Some("Test Track".to_string()),
551 artist: Some("Test Artist".to_string()),
552 album: Some("Test Album".to_string()),
553 album_name: Some("Test Album".to_string()),
554 album_id: None,
555 artist_items: None,
556 artists: Some(vec!["Test Artist".to_string()]),
557 primary_image_tag: None,
558 image_id: None,
559 item_type: Some("Audio".to_string()),
560 playlist_id: None,
561 duration: Some(180.0),
562 artwork_url: None,
563 media_type: MediaType::Audio,
564 source: MediaSource::DirectUrl {
565 url: "http://example.com/test.mp3".to_string(),
566 },
567 video_codec: None,
568 needs_transcoding: false,
569 video_width: None,
570 video_height: None,
571 subtitles: vec![],
572 series_id: None,
573 server_id: None,
574 };
575
576 backend.load(&media).unwrap();
577 backend.play().unwrap();
578
579 backend.seek(30.0).unwrap();
581
582 if let PlayerState::Playing { position, .. } = backend.state() {
584 assert_eq!(position, 30.0);
585 } else {
586 panic!("Expected Playing state");
587 }
588 }
589}