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> {
107 Err(PlayerError::not_implemented())
109 }
110
111 fn set_subtitle_track(&mut self, _stream_index: Option<i32>) -> Result<(), PlayerError> {
119 Err(PlayerError::not_implemented())
121 }
122}
123
124pub struct NullBackend {
128 state: PlayerState,
129 volume: f32,
130 position: f64,
131 duration: Option<f64>,
132 audio_settings: AudioSettings,
133}
134
135impl Default for NullBackend {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl NullBackend {
142 pub fn new() -> Self {
143 Self {
144 state: PlayerState::Idle,
145 volume: 1.0,
146 position: 0.0,
147 duration: None,
148 audio_settings: AudioSettings::default(),
149 }
150 }
151}
152
153impl PlayerBackend for NullBackend {
154 fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError> {
155 self.state = PlayerState::Loading {
156 media: media.clone(),
157 };
158 self.duration = media.duration;
160 self.position = 0.0;
161 self.state = PlayerState::Paused {
162 media: media.clone(),
163 position: 0.0,
164 duration: media.duration.unwrap_or(0.0),
165 };
166 Ok(())
167 }
168
169 fn play(&mut self) -> Result<(), PlayerError> {
170 if let PlayerState::Paused {
171 media,
172 position,
173 duration,
174 } = &self.state
175 {
176 self.state = PlayerState::Playing {
177 media: media.clone(),
178 position: *position,
179 duration: *duration,
180 };
181 }
182 Ok(())
183 }
184
185 fn pause(&mut self) -> Result<(), PlayerError> {
186 if let PlayerState::Playing {
187 media,
188 position,
189 duration,
190 } = &self.state
191 {
192 self.state = PlayerState::Paused {
193 media: media.clone(),
194 position: *position,
195 duration: *duration,
196 };
197 }
198 Ok(())
199 }
200
201 fn stop(&mut self) -> Result<(), PlayerError> {
202 self.state = PlayerState::Idle;
203 self.position = 0.0;
204 self.duration = None;
205 Ok(())
206 }
207
208 fn seek(&mut self, position: f64) -> Result<(), PlayerError> {
209 self.position = position;
210 match &mut self.state {
211 PlayerState::Playing { position: pos, .. } => *pos = position,
212 PlayerState::Paused { position: pos, .. } => *pos = position,
213 _ => {}
214 }
215 Ok(())
216 }
217
218 fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> {
219 self.volume = volume.clamp(0.0, 1.0);
220 Ok(())
221 }
222
223 fn position(&self) -> f64 {
224 self.position
225 }
226
227 fn duration(&self) -> Option<f64> {
228 self.duration
229 }
230
231 fn state(&self) -> PlayerState {
232 self.state.clone()
233 }
234
235 fn volume(&self) -> f32 {
236 self.volume
237 }
238
239 fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> {
240 self.audio_settings = settings.clone().with_crossfade_clamped();
241 Ok(())
242 }
243
244 fn audio_settings(&self) -> AudioSettings {
245 self.audio_settings.clone()
246 }
247}
248
249impl PlayerBackend for Box<dyn PlayerBackend> {
256 fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError> {
257 (**self).load(media)
258 }
259 fn play(&mut self) -> Result<(), PlayerError> {
260 (**self).play()
261 }
262 fn pause(&mut self) -> Result<(), PlayerError> {
263 (**self).pause()
264 }
265 fn stop(&mut self) -> Result<(), PlayerError> {
266 (**self).stop()
267 }
268 fn seek(&mut self, position: f64) -> Result<(), PlayerError> {
269 (**self).seek(position)
270 }
271 fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> {
272 (**self).set_volume(volume)
273 }
274 fn position(&self) -> f64 {
275 (**self).position()
276 }
277 fn duration(&self) -> Option<f64> {
278 (**self).duration()
279 }
280 fn state(&self) -> PlayerState {
281 (**self).state()
282 }
283 fn volume(&self) -> f32 {
284 (**self).volume()
285 }
286 fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> {
287 (**self).set_audio_settings(settings)
288 }
289 fn audio_settings(&self) -> AudioSettings {
290 (**self).audio_settings()
291 }
292 fn set_audio_track(&mut self, stream_index: i32) -> Result<(), PlayerError> {
293 (**self).set_audio_track(stream_index)
294 }
295 fn set_subtitle_track(&mut self, stream_index: Option<i32>) -> Result<(), PlayerError> {
296 (**self).set_subtitle_track(stream_index)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
307 fn test_null_backend_volume_default() {
308 let backend = NullBackend::new();
309 assert_eq!(backend.volume(), 1.0);
310 }
311
312 #[test]
317 fn test_null_backend_set_volume() {
318 let mut backend = NullBackend::new();
319 backend.set_volume(0.5).unwrap();
320 assert_eq!(backend.volume(), 0.5);
321 }
322
323 #[test]
328 fn test_null_backend_volume_clamping_high() {
329 let mut backend = NullBackend::new();
330 backend.set_volume(1.5).unwrap();
331 assert_eq!(backend.volume(), 1.0);
332 }
333
334 #[test]
339 fn test_null_backend_volume_clamping_low() {
340 let mut backend = NullBackend::new();
341 backend.set_volume(-0.5).unwrap();
342 assert_eq!(backend.volume(), 0.0);
343 }
344
345 #[test]
350 fn test_null_backend_volume_boundary() {
351 let mut backend = NullBackend::new();
352
353 backend.set_volume(0.0).unwrap();
354 assert_eq!(backend.volume(), 0.0);
355
356 backend.set_volume(1.0).unwrap();
357 assert_eq!(backend.volume(), 1.0);
358 }
359
360 #[test]
366 fn test_null_backend_audio_settings_default() {
367 let backend = NullBackend::new();
368 let settings = backend.audio_settings();
369 assert_eq!(settings.crossfade_duration, 0.0);
370 assert!(settings.gapless_playback);
371 assert!(!settings.normalize_volume);
372 }
373
374 #[test]
380 fn test_null_backend_set_audio_settings() {
381 use crate::settings::VolumeLevel;
382
383 let mut backend = NullBackend::new();
384 let settings = AudioSettings {
385 crossfade_duration: 5.0,
386 gapless_playback: false,
387 normalize_volume: true,
388 volume_level: VolumeLevel::Loud,
389 ..Default::default()
390 };
391
392 backend.set_audio_settings(&settings).unwrap();
393
394 let result = backend.audio_settings();
395 assert_eq!(result.crossfade_duration, 5.0);
396 assert!(!result.gapless_playback);
397 assert!(result.normalize_volume);
398 assert_eq!(result.volume_level, VolumeLevel::Loud);
399 }
400
401 #[test]
405 fn test_null_backend_audio_settings_crossfade_clamping() {
406 let mut backend = NullBackend::new();
407 let settings = AudioSettings {
408 crossfade_duration: 20.0,
409 ..Default::default()
410 };
411
412 backend.set_audio_settings(&settings).unwrap();
413 assert_eq!(backend.audio_settings().crossfade_duration, 12.0);
414 }
415
416 #[test]
421 fn test_null_backend_seek_updates_position() {
422 use crate::player::media::{MediaItem, MediaSource, MediaType};
423
424 let mut backend = NullBackend::new();
425
426 let media = MediaItem {
428 transport: None,
430 id: "test_media".to_string(),
431 title: "Test Track".to_string(),
432 name: Some("Test Track".to_string()),
433 artist: Some("Test Artist".to_string()),
434 album: Some("Test Album".to_string()),
435 album_name: Some("Test Album".to_string()),
436 album_id: None,
437 artist_items: None,
438 artists: Some(vec!["Test Artist".to_string()]),
439 primary_image_tag: None,
440 image_id: None,
441 item_type: Some("Audio".to_string()),
442 playlist_id: None,
443 duration: Some(180.0),
444 artwork_url: None,
445 media_type: MediaType::Audio,
446 source: MediaSource::DirectUrl {
447 url: "http://example.com/test.mp3".to_string(),
448 },
449 video_codec: None,
450 needs_transcoding: false,
451 video_width: None,
452 video_height: None,
453 subtitles: vec![],
454 series_id: None,
455 server_id: None,
456 };
457
458 backend.load(&media).unwrap();
460 backend.play().unwrap();
461
462 assert_eq!(backend.position(), 0.0);
464
465 backend.seek(30.0).unwrap();
467 assert_eq!(backend.position(), 30.0);
468
469 backend.seek(60.0).unwrap();
471 assert_eq!(backend.position(), 60.0);
472
473 backend.seek(15.0).unwrap();
475 assert_eq!(backend.position(), 15.0);
476 }
477
478 #[test]
483 fn test_null_backend_seek_while_paused() {
484 use crate::player::media::{MediaItem, MediaSource, MediaType};
485
486 let mut backend = NullBackend::new();
487
488 let media = MediaItem {
489 transport: None,
491 id: "test_media".to_string(),
492 title: "Test Track".to_string(),
493 name: Some("Test Track".to_string()),
494 artist: Some("Test Artist".to_string()),
495 album: Some("Test Album".to_string()),
496 album_name: Some("Test Album".to_string()),
497 album_id: None,
498 artist_items: None,
499 artists: Some(vec!["Test Artist".to_string()]),
500 primary_image_tag: None,
501 image_id: None,
502 item_type: Some("Audio".to_string()),
503 playlist_id: None,
504 duration: Some(180.0),
505 artwork_url: None,
506 media_type: MediaType::Audio,
507 source: MediaSource::DirectUrl {
508 url: "http://example.com/test.mp3".to_string(),
509 },
510 video_codec: None,
511 needs_transcoding: false,
512 video_width: None,
513 video_height: None,
514 subtitles: vec![],
515 series_id: None,
516 server_id: None,
517 };
518
519 backend.load(&media).unwrap();
521
522 assert!(matches!(backend.state(), PlayerState::Paused { .. }));
524
525 backend.seek(45.0).unwrap();
527 assert_eq!(backend.position(), 45.0);
528
529 assert!(matches!(backend.state(), PlayerState::Paused { .. }));
531 }
532
533 #[test]
538 fn test_null_backend_position_updates_in_state() {
539 use crate::player::media::{MediaItem, MediaSource, MediaType};
540
541 let mut backend = NullBackend::new();
542
543 let media = MediaItem {
544 transport: None,
546 id: "test_media".to_string(),
547 title: "Test Track".to_string(),
548 name: Some("Test Track".to_string()),
549 artist: Some("Test Artist".to_string()),
550 album: Some("Test Album".to_string()),
551 album_name: Some("Test Album".to_string()),
552 album_id: None,
553 artist_items: None,
554 artists: Some(vec!["Test Artist".to_string()]),
555 primary_image_tag: None,
556 image_id: None,
557 item_type: Some("Audio".to_string()),
558 playlist_id: None,
559 duration: Some(180.0),
560 artwork_url: None,
561 media_type: MediaType::Audio,
562 source: MediaSource::DirectUrl {
563 url: "http://example.com/test.mp3".to_string(),
564 },
565 video_codec: None,
566 needs_transcoding: false,
567 video_width: None,
568 video_height: None,
569 subtitles: vec![],
570 series_id: None,
571 server_id: None,
572 };
573
574 backend.load(&media).unwrap();
575 backend.play().unwrap();
576
577 backend.seek(30.0).unwrap();
579
580 if let PlayerState::Playing { position, .. } = backend.state() {
582 assert_eq!(position, 30.0);
583 } else {
584 panic!("Expected Playing state");
585 }
586 }
587}