aboutsummaryrefslogtreecommitdiffstats
path: root/src/audio.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-05-27 20:46:33 +0200
committersalmonik <raisersoft@proton.me>2026-05-27 20:46:33 +0200
commit0ae9d89f18bf36407229798a3b4943604fb708e4 (patch)
tree47fbc6ad1d22ddd575f9773d5e1d36dae7bd0815 /src/audio.rs
parentplay [track] [-a/--album] option (diff)
downloadrsplayer-0ae9d89f18bf36407229798a3b4943604fb708e4.tar.gz
rsplayer-0ae9d89f18bf36407229798a3b4943604fb708e4.zip
Add internet streams support
Diffstat (limited to 'src/audio.rs')
-rw-r--r--src/audio.rs131
1 files changed, 127 insertions, 4 deletions
diff --git a/src/audio.rs b/src/audio.rs
index e3d88bc..6da47c3 100644
--- a/src/audio.rs
+++ b/src/audio.rs
@@ -6,12 +6,107 @@
6*/ 6*/
7 7
8use crate::notification; 8use crate::notification;
9use std::path::PathBuf; 9use std::io::{Read, Seek, SeekFrom};
10use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicBool, Ordering}; 11use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::mpsc::{self, Receiver, Sender}; 12use std::sync::mpsc::{self, Receiver, Sender};
12use std::sync::{Arc, Mutex}; 13use std::sync::{Arc, Mutex};
13use std::thread; 14use std::thread;
14 15
16struct HttpMediaSource {
17 reader: Mutex<Box<dyn Read + Send>>,
18 buffer: Vec<u8>,
19 position: usize,
20 eof: bool,
21}
22
23impl HttpMediaSource {
24 fn new(reader: Box<dyn Read + Send>) -> Self {
25 HttpMediaSource {
26 reader: Mutex::new(reader),
27 buffer: Vec::new(),
28 position: 0,
29 eof: false,
30 }
31 }
32}
33
34impl Read for HttpMediaSource {
35 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
36 if self.position < self.buffer.len() {
37 let available = self.buffer.len() - self.position;
38 let to_copy = buf.len().min(available);
39 buf[..to_copy].copy_from_slice(&self.buffer[self.position..self.position + to_copy]);
40 self.position += to_copy;
41 return Ok(to_copy);
42 }
43 if self.eof {
44 return Ok(0);
45 }
46 let n = self.reader.lock().unwrap().read(buf)?;
47 if n > 0 {
48 self.buffer.extend_from_slice(&buf[..n]);
49 self.position += n;
50 } else {
51 self.eof = true;
52 }
53 Ok(n)
54 }
55}
56
57impl Seek for HttpMediaSource {
58 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
59 match pos {
60 SeekFrom::Start(offset) => {
61 let offset = offset as usize;
62 while offset > self.buffer.len() && !self.eof {
63 let mut temp = [0u8; 8192];
64 let n = self.reader.lock().unwrap().read(&mut temp)?;
65 if n == 0 {
66 self.eof = true;
67 } else {
68 self.buffer.extend_from_slice(&temp[..n]);
69 }
70 }
71 self.position = offset.min(self.buffer.len());
72 Ok(self.position as u64)
73 }
74 SeekFrom::End(_) => Err(std::io::Error::new(
75 std::io::ErrorKind::Unsupported,
76 "cannot seek from end on a stream",
77 )),
78 SeekFrom::Current(offset) => {
79 let new_pos = if offset >= 0 {
80 self.position.saturating_add(offset as usize)
81 } else {
82 self.position.saturating_sub((-offset) as usize)
83 };
84 while new_pos > self.buffer.len() && !self.eof {
85 let mut temp = [0u8; 8192];
86 let n = self.reader.lock().unwrap().read(&mut temp)?;
87 if n == 0 {
88 self.eof = true;
89 } else {
90 self.buffer.extend_from_slice(&temp[..n]);
91 }
92 }
93 self.position = new_pos.min(self.buffer.len());
94 Ok(self.position as u64)
95 }
96 }
97 }
98}
99
100impl symphonia::core::io::MediaSource for HttpMediaSource {
101 fn is_seekable(&self) -> bool {
102 true
103 }
104
105 fn byte_len(&self) -> Option<u64> {
106 None
107 }
108}
109
15#[allow(dead_code)] 110#[allow(dead_code)]
16pub enum AudioCommand { 111pub enum AudioCommand {
17 Play(PathBuf), 112 Play(PathBuf),
@@ -426,10 +521,38 @@ fn open_audio_file(path: &PathBuf) -> Result<AudioData, Box<dyn std::error::Erro
426 use symphonia::core::formats::probe::Hint; 521 use symphonia::core::formats::probe::Hint;
427 use symphonia::core::io::MediaSourceStream; 522 use symphonia::core::io::MediaSourceStream;
428 523
429 let file = std::fs::File::open(path)?; 524 let path_str = path.to_string_lossy();
430 let mss = MediaSourceStream::new(Box::new(file), Default::default()); 525 let is_url = path_str.starts_with("http://") || path_str.starts_with("https://");
526
527 let mss: MediaSourceStream = if is_url {
528 let client = reqwest::blocking::Client::builder()
529 .user_agent("RSPlayer/0.8.2")
530 .connect_timeout(std::time::Duration::from_secs(10))
531 .build()
532 .map_err(|e| format!("Failed to create HTTP client: {}", e))?;
533 let response = client
534 .get(path_str.as_ref())
535 .send()
536 .map_err(|e| format!("HTTP request failed: {}", e))?;
537 let status = response.status();
538 if !status.is_success() {
539 return Err(format!("HTTP error {} for URL", status).into());
540 }
541 let reader = HttpMediaSource::new(Box::new(response));
542 MediaSourceStream::new(Box::new(reader), Default::default())
543 } else {
544 let file = std::fs::File::open(path)?;
545 MediaSourceStream::new(Box::new(file), Default::default())
546 };
431 547
432 let hint = Hint::new(); 548 let mut hint = Hint::new();
549 if is_url
550 && let Some(ext) = Path::new(path_str.as_ref())
551 .extension()
552 .and_then(|e| e.to_str())
553 {
554 hint.with_extension(ext);
555 }
433 let format = symphonia::default::get_probe().probe( 556 let format = symphonia::default::get_probe().probe(
434 &hint, 557 &hint,
435 mss, 558 mss,