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
251#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
259 fn test_null_backend_volume_default() {
260 let backend = NullBackend::new();
261 assert_eq!(backend.volume(), 1.0);
262 }
263
264 #[test]
269 fn test_null_backend_set_volume() {
270 let mut backend = NullBackend::new();
271 backend.set_volume(0.5).unwrap();
272 assert_eq!(backend.volume(), 0.5);
273 }
274
275 #[test]
280 fn test_null_backend_volume_clamping_high() {
281 let mut backend = NullBackend::new();
282 backend.set_volume(1.5).unwrap();
283 assert_eq!(backend.volume(), 1.0);
284 }
285
286 #[test]
291 fn test_null_backend_volume_clamping_low() {
292 let mut backend = NullBackend::new();
293 backend.set_volume(-0.5).unwrap();
294 assert_eq!(backend.volume(), 0.0);
295 }
296
297 #[test]
302 fn test_null_backend_volume_boundary() {
303 let mut backend = NullBackend::new();
304
305 backend.set_volume(0.0).unwrap();
306 assert_eq!(backend.volume(), 0.0);
307
308 backend.set_volume(1.0).unwrap();
309 assert_eq!(backend.volume(), 1.0);
310 }
311
312 #[test]
318 fn test_null_backend_audio_settings_default() {
319 let backend = NullBackend::new();
320 let settings = backend.audio_settings();
321 assert_eq!(settings.crossfade_duration, 0.0);
322 assert!(settings.gapless_playback);
323 assert!(!settings.normalize_volume);
324 }
325
326 #[test]
332 fn test_null_backend_set_audio_settings() {
333 use crate::settings::VolumeLevel;
334
335 let mut backend = NullBackend::new();
336 let settings = AudioSettings {
337 crossfade_duration: 5.0,
338 gapless_playback: false,
339 normalize_volume: true,
340 volume_level: VolumeLevel::Loud,
341 ..Default::default()
342 };
343
344 backend.set_audio_settings(&settings).unwrap();
345
346 let result = backend.audio_settings();
347 assert_eq!(result.crossfade_duration, 5.0);
348 assert!(!result.gapless_playback);
349 assert!(result.normalize_volume);
350 assert_eq!(result.volume_level, VolumeLevel::Loud);
351 }
352
353 #[test]
357 fn test_null_backend_audio_settings_crossfade_clamping() {
358 let mut backend = NullBackend::new();
359 let settings = AudioSettings {
360 crossfade_duration: 20.0,
361 ..Default::default()
362 };
363
364 backend.set_audio_settings(&settings).unwrap();
365 assert_eq!(backend.audio_settings().crossfade_duration, 12.0);
366 }
367
368 #[test]
373 fn test_null_backend_seek_updates_position() {
374 use crate::player::media::{MediaItem, MediaSource, MediaType};
375
376 let mut backend = NullBackend::new();
377
378 let media = MediaItem {
380 id: "test_media".to_string(),
381 title: "Test Track".to_string(),
382 name: Some("Test Track".to_string()),
383 artist: Some("Test Artist".to_string()),
384 album: Some("Test Album".to_string()),
385 album_name: Some("Test Album".to_string()),
386 album_id: None,
387 artist_items: None,
388 artists: Some(vec!["Test Artist".to_string()]),
389 primary_image_tag: None,
390 image_id: None,
391 item_type: Some("Audio".to_string()),
392 playlist_id: None,
393 duration: Some(180.0),
394 artwork_url: None,
395 media_type: MediaType::Audio,
396 source: MediaSource::DirectUrl {
397 url: "http://example.com/test.mp3".to_string(),
398 },
399 video_codec: None,
400 needs_transcoding: false,
401 video_width: None,
402 video_height: None,
403 subtitles: vec![],
404 series_id: None,
405 server_id: None,
406 };
407
408 backend.load(&media).unwrap();
410 backend.play().unwrap();
411
412 assert_eq!(backend.position(), 0.0);
414
415 backend.seek(30.0).unwrap();
417 assert_eq!(backend.position(), 30.0);
418
419 backend.seek(60.0).unwrap();
421 assert_eq!(backend.position(), 60.0);
422
423 backend.seek(15.0).unwrap();
425 assert_eq!(backend.position(), 15.0);
426 }
427
428 #[test]
433 fn test_null_backend_seek_while_paused() {
434 use crate::player::media::{MediaItem, MediaSource, MediaType};
435
436 let mut backend = NullBackend::new();
437
438 let media = MediaItem {
439 id: "test_media".to_string(),
440 title: "Test Track".to_string(),
441 name: Some("Test Track".to_string()),
442 artist: Some("Test Artist".to_string()),
443 album: Some("Test Album".to_string()),
444 album_name: Some("Test Album".to_string()),
445 album_id: None,
446 artist_items: None,
447 artists: Some(vec!["Test Artist".to_string()]),
448 primary_image_tag: None,
449 image_id: None,
450 item_type: Some("Audio".to_string()),
451 playlist_id: None,
452 duration: Some(180.0),
453 artwork_url: None,
454 media_type: MediaType::Audio,
455 source: MediaSource::DirectUrl {
456 url: "http://example.com/test.mp3".to_string(),
457 },
458 video_codec: None,
459 needs_transcoding: false,
460 video_width: None,
461 video_height: None,
462 subtitles: vec![],
463 series_id: None,
464 server_id: None,
465 };
466
467 backend.load(&media).unwrap();
469
470 assert!(matches!(backend.state(), PlayerState::Paused { .. }));
472
473 backend.seek(45.0).unwrap();
475 assert_eq!(backend.position(), 45.0);
476
477 assert!(matches!(backend.state(), PlayerState::Paused { .. }));
479 }
480
481 #[test]
486 fn test_null_backend_position_updates_in_state() {
487 use crate::player::media::{MediaItem, MediaSource, MediaType};
488
489 let mut backend = NullBackend::new();
490
491 let media = MediaItem {
492 id: "test_media".to_string(),
493 title: "Test Track".to_string(),
494 name: Some("Test Track".to_string()),
495 artist: Some("Test Artist".to_string()),
496 album: Some("Test Album".to_string()),
497 album_name: Some("Test Album".to_string()),
498 album_id: None,
499 artist_items: None,
500 artists: Some(vec!["Test Artist".to_string()]),
501 primary_image_tag: None,
502 image_id: None,
503 item_type: Some("Audio".to_string()),
504 playlist_id: None,
505 duration: Some(180.0),
506 artwork_url: None,
507 media_type: MediaType::Audio,
508 source: MediaSource::DirectUrl {
509 url: "http://example.com/test.mp3".to_string(),
510 },
511 video_codec: None,
512 needs_transcoding: false,
513 video_width: None,
514 video_height: None,
515 subtitles: vec![],
516 series_id: None,
517 server_id: None,
518 };
519
520 backend.load(&media).unwrap();
521 backend.play().unwrap();
522
523 backend.seek(30.0).unwrap();
525
526 if let PlayerState::Playing { position, .. } = backend.state() {
528 assert_eq!(position, 30.0);
529 } else {
530 panic!("Expected Playing state");
531 }
532 }
533}