1use std::collections::{BTreeMap, VecDeque};
13use std::io::Read;
14use std::net::{IpAddr, ToSocketAddrs};
15use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
16use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, sync_channel};
17use std::sync::{Arc, Mutex, OnceLock};
18use std::time::{Duration, Instant};
19
20use crate::audio::Resampler;
21use crate::net::url_host;
22
23pub const CONNECTING: u32 = 0;
24pub const PLAYING: u32 = 1;
25pub const PAUSED: u32 = 2;
26pub const FAILED: u32 = 3;
27pub const ENDED: u32 = 4;
28pub const CAN_VOLUME: u32 = 1;
29pub const CAN_LOWPASS: u32 = 2;
30pub const CAN_METADATA: u32 = 4;
31
32pub const MAX_STREAMS: usize = 4;
34const MAX_URL: usize = 2048;
36const MAX_TITLE: usize = 512;
38const BUFFER_S: f64 = 2.0;
40const START_S: f64 = 0.25;
42const STALL: Duration = Duration::from_secs(15);
44const PAUSE_DROP: Duration = Duration::from_secs(10);
46const RECONNECTS: u32 = 3;
48const MAX_REDIRECTS: u32 = 5;
49
50#[derive(Clone, Debug, Default)]
56pub struct MediaPolicy {
57 pub allowed: Option<Vec<String>>,
59 pub declared: bool,
61 pub consent: Option<crate::consent::Consent>,
62}
63
64pub const CONSENT_SUBJECT: &str = "media";
65
66impl MediaPolicy {
67 pub fn new(allowed: Option<Vec<String>>) -> MediaPolicy {
68 MediaPolicy { allowed: allowed.map(|h| h.into_iter().map(|h| h.to_ascii_lowercase()).collect()), declared: false, consent: None }
69 }
70
71 fn listed(&self, host: &str) -> bool {
72 self.allowed.as_ref().is_some_and(|l| l.iter().any(|h| h == host))
73 }
74
75 fn may_open(&self) -> Result<(), String> {
77 if self.allowed.is_some() {
78 return Ok(());
79 }
80 if !self.declared {
81 return Err("this game's manifest doesn't ask to play media (\"media\": true), and --allow-media isn't given".into());
82 }
83 if self.consent.is_none() {
84 return Err("media streams need --allow-media here".into());
85 }
86 Ok(())
87 }
88
89 fn answered(&self) -> Result<bool, String> {
91 if self.allowed.is_some() {
92 return Ok(true);
93 }
94 match self.consent.as_ref().map(|c| c.lock().unwrap().check(CONSENT_SUBJECT)) {
95 Some(Some(true)) => Ok(true),
96 Some(Some(false)) => Err("the player said no to internet audio".into()),
97 Some(None) => Ok(false),
98 None => Err("media streams need --allow-media here".into()),
99 }
100 }
101
102 pub fn check_url(&self, url: &str) -> Result<(), String> {
105 if !url.starts_with("https://") {
106 return Err(format!("{url}: only https streams"));
107 }
108 let host = url_host(url).ok_or_else(|| format!("{url}: no host"))?;
109 let bare = host.trim_start_matches('[').trim_end_matches(']');
110 if let Some(list) = &self.allowed {
111 if !list.is_empty() && !self.listed(&host) && !self.listed(bare) {
112 return Err(format!("{host} isn't in --allow-media's list"));
113 }
114 }
115 if self.listed(&host) || self.listed(bare) {
116 return Ok(());
117 }
118 let port = url_port(url);
119 let addrs: Vec<IpAddr> = match bare.parse::<IpAddr>() {
120 Ok(ip) => vec![ip],
121 Err(_) => (bare, port).to_socket_addrs().map_err(|e| format!("{host}: {e}"))?.map(|a| a.ip()).collect(),
122 };
123 match addrs.iter().find(|ip| !is_public(**ip)) {
124 Some(ip) => Err(format!("{host} is a private or local address ({ip}): list it in --allow-media to reach it")),
125 None => Ok(()),
126 }
127 }
128}
129
130fn url_port(url: &str) -> u16 {
131 let rest = url.split_once("://").map_or(url, |(_, r)| r);
132 let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
133 let after = authority.rsplit_once(']').map_or(authority, |(_, a)| a);
134 after.rsplit_once(':').and_then(|(_, p)| p.parse().ok()).unwrap_or(443)
135}
136
137pub fn is_public(ip: IpAddr) -> bool {
140 match ip {
141 IpAddr::V4(v) => {
142 let o = v.octets();
143 !(v.is_loopback()
144 || v.is_private()
145 || v.is_link_local()
146 || v.is_unspecified()
147 || v.is_broadcast()
148 || v.is_multicast()
149 || v.is_documentation()
150 || o[0] == 0
151 || (o[0] == 100 && (o[1] & 0xc0) == 64)
152 || o[0] >= 240)
153 }
154 IpAddr::V6(v) => {
155 if let Some(v4) = v.to_ipv4_mapped() {
156 return is_public(IpAddr::V4(v4));
157 }
158 let s = v.segments();
159 !(v.is_loopback() || v.is_unspecified() || v.is_multicast() || (s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || s[0] == 0x2001 && s[1] == 0xdb8)
160 }
161 }
162}
163
164pub fn parse_desc(json: &str) -> Result<String, String> {
166 let v: serde_json::Value = serde_json::from_str(json).map_err(|e| format!("not valid JSON: {e}"))?;
167 let o = v.as_object().ok_or("not a JSON object")?;
168 let url = o.get("url").and_then(|u| u.as_str()).ok_or("no \"url\"")?;
169 if url.len() > MAX_URL {
170 return Err(format!("the URL is longer than {MAX_URL} bytes"));
171 }
172 if !url.starts_with("https://") {
173 return Err("only https:// streams".into());
174 }
175 if url.bytes().any(|b| b <= b' ' || b == 0x7f) || url_host(url).is_none() {
176 return Err(format!("not a URL: {url:?}"));
177 }
178 Ok(url.to_owned())
179}
180
181pub struct Voice {
186 buf: Mutex<VecDeque<f32>>,
187 gain: AtomicU32,
189 lowpass: AtomicU32,
191 playing: AtomicBool,
192 played: AtomicU64,
194 dsp: Mutex<Dsp>,
196}
197
198#[derive(Default)]
199struct Dsp {
200 gain: f32,
201 hz: f32,
202 k: [f32; 5],
204 s: [[f32; 4]; 2],
205}
206
207impl Voice {
208 fn new() -> Voice {
209 Voice {
210 buf: Mutex::new(VecDeque::new()),
211 gain: AtomicU32::new(1.0f32.to_bits()),
212 lowpass: AtomicU32::new(0),
213 playing: AtomicBool::new(false),
214 played: AtomicU64::new(0),
215 dsp: Mutex::new(Dsp { gain: 1.0, ..Default::default() }),
216 }
217 }
218
219 fn buffered(&self) -> usize {
220 self.buf.lock().unwrap().len() / 2
221 }
222}
223
224pub struct MediaBus {
226 pub rate: u32,
227 voices: Mutex<Vec<Arc<Voice>>>,
228}
229
230impl MediaBus {
231 pub fn new(rate: u32) -> Arc<MediaBus> {
232 Arc::new(MediaBus { rate, voices: Mutex::new(Vec::new()) })
233 }
234
235 pub fn silent() -> Arc<MediaBus> {
238 let bus = MediaBus::new(48_000);
239 let b = Arc::downgrade(&bus);
240 std::thread::Builder::new()
241 .name("gasm-media-clock".into())
242 .spawn(move || {
243 let mut last = Instant::now();
244 let mut owed = 0.0f64;
245 let mut scratch = Vec::new();
246 while let Some(bus) = b.upgrade() {
247 std::thread::sleep(Duration::from_millis(20));
248 owed += last.elapsed().as_secs_f64() * bus.rate as f64;
249 last = Instant::now();
250 let n = owed as usize;
251 owed -= n as f64;
252 scratch.clear();
253 scratch.resize(n * 2, 0.0);
254 bus.mix(&mut scratch);
255 }
256 })
257 .expect("media clock thread");
258 bus
259 }
260
261 fn add(&self, v: Arc<Voice>) {
262 self.voices.lock().unwrap().push(v);
263 }
264
265 fn remove(&self, v: &Arc<Voice>) {
266 self.voices.lock().unwrap().retain(|x| !Arc::ptr_eq(x, v));
267 }
268
269 pub fn mix(&self, out: &mut [f32]) {
273 let Ok(voices) = self.voices.try_lock() else { return };
274 for v in voices.iter() {
275 if !v.playing.load(Ordering::Acquire) {
276 continue;
277 }
278 let Ok(mut dsp) = v.dsp.try_lock() else { continue };
279 let target = f32::from_bits(v.gain.load(Ordering::Relaxed));
280 let hz = f32::from_bits(v.lowpass.load(Ordering::Relaxed));
281 if hz != dsp.hz {
282 dsp.hz = hz;
283 dsp.k = lowpass(hz, self.rate as f32);
284 }
285 let step = 1.0 / (0.02 * self.rate as f32);
287 let mut buf = v.buf.lock().unwrap();
288 let frames = (out.len() / 2).min(buf.len() / 2);
289 for f in 0..frames {
290 dsp.gain += (target - dsp.gain).clamp(-step, step);
291 for c in 0..2 {
292 let x = buf.pop_front().unwrap_or(0.0);
293 let y = if dsp.hz > 0.0 {
294 let [b0, b1, b2, a1, a2] = dsp.k;
295 let s = &mut dsp.s[c];
296 let y = b0 * x + b1 * s[0] + b2 * s[1] - a1 * s[2] - a2 * s[3];
297 *s = [x, s[0], y, s[2]];
298 y
299 } else {
300 x
301 };
302 out[f * 2 + c] += y * dsp.gain;
303 }
304 }
305 drop(buf);
306 v.played.fetch_add(frames as u64, Ordering::Relaxed);
307 }
308 }
309}
310
311fn lowpass(hz: f32, rate: f32) -> [f32; 5] {
313 if hz <= 0.0 {
314 return [1.0, 0.0, 0.0, 0.0, 0.0];
315 }
316 let w = 2.0 * std::f32::consts::PI * hz.min(rate * 0.45) / rate;
317 let alpha = w.sin() / (2.0 * std::f32::consts::FRAC_1_SQRT_2);
318 let cos = w.cos();
319 let a0 = 1.0 + alpha;
320 let b1 = (1.0 - cos) / a0;
321 [b1 / 2.0, b1, b1 / 2.0, -2.0 * cos / a0, (1.0 - alpha) / a0]
322}
323
324#[derive(Default)]
328struct Info {
329 state: u32,
330 error: String,
331 caps: u32,
332 duration: f64,
333 name: Option<String>,
334 title: Option<String>,
335 codec: Option<String>,
336 bitrate: Option<u32>,
337 version: u32,
338}
339
340impl Info {
341 fn metadata(&self) -> String {
342 serde_json::json!({ "name": self.name, "title": self.title, "codec": self.codec, "bitrate": self.bitrate }).to_string()
343 }
344}
345
346struct Stream {
347 voice: Arc<Voice>,
348 info: Arc<Mutex<Info>>,
349 want: Arc<AtomicBool>,
351 started: bool,
352 closed: Arc<AtomicBool>,
353 url: String,
354 paused_at: Arc<Mutex<Option<Instant>>>,
356}
357
358pub struct Media {
360 policy: MediaPolicy,
361 bus: Option<Arc<MediaBus>>,
362 streams: BTreeMap<i32, Option<Stream>>,
364 next: i32,
365 last_refusal: String,
366 output: Option<Arc<MediaBus>>,
368}
369
370impl Default for Media {
371 fn default() -> Media {
372 Media::new(MediaPolicy::default())
373 }
374}
375
376impl Media {
377 pub fn new(policy: MediaPolicy) -> Media {
378 Media { policy, bus: None, streams: BTreeMap::new(), next: 1, last_refusal: String::new(), output: None }
379 }
380
381 pub fn set_output(&mut self, bus: Option<Arc<MediaBus>>) {
383 self.output = bus;
384 }
385
386 fn bus(&mut self) -> Arc<MediaBus> {
387 if self.bus.is_none() {
388 self.bus = Some(self.output.clone().unwrap_or_else(MediaBus::silent));
389 }
390 self.bus.clone().expect("bus")
391 }
392
393 pub fn open(&mut self, desc: &str) -> i32 {
394 let refuse = |m: &mut Media, why: String| {
395 eprintln!("[gasm] media: refused: {why}");
396 m.last_refusal = why;
397 -1
398 };
399 let url = match parse_desc(desc) {
400 Ok(u) => u,
401 Err(e) => return refuse(self, e),
402 };
403 if let Err(why) = self.policy.may_open() {
404 return refuse(self, why);
405 }
406 if self.streams.values().filter(|s| s.is_some()).count() >= MAX_STREAMS {
407 return refuse(self, format!("{MAX_STREAMS} streams are open already"));
408 }
409 let h = self.next;
410 self.next += 1;
411 let voice = Arc::new(Voice::new());
412 self.bus().add(voice.clone());
413 let info = Arc::new(Mutex::new(Info { duration: -1.0, ..Default::default() }));
414 self.streams.insert(
415 h,
416 Some(Stream { voice, info, want: Arc::new(AtomicBool::new(false)), started: false, closed: Arc::new(AtomicBool::new(false)), url, paused_at: Arc::new(Mutex::new(None)) }),
417 );
418 h
419 }
420
421 fn get(&mut self, h: i32) -> Result<Option<&mut Stream>, String> {
422 match self.streams.get_mut(&h) {
423 Some(s) => Ok(s.as_mut()),
424 None => Err(format!("gasm:media: {h} is not a stream handle")),
425 }
426 }
427
428 pub fn play(&mut self, h: i32) -> Result<(), String> {
429 let bus = self.bus();
430 let policy = self.policy.clone();
431 let Some(s) = self.get(h)? else { return Ok(()) };
432 s.want.store(true, Ordering::Release);
433 *s.paused_at.lock().unwrap() = None;
434 if !s.started {
435 s.started = true;
436 let (url, voice, info, want, closed, paused_at) = (s.url.clone(), s.voice.clone(), s.info.clone(), s.want.clone(), s.closed.clone(), s.paused_at.clone());
437 let ctl = Control { voice, info, want, closed, paused_at, rate: bus.rate };
438 std::thread::Builder::new()
439 .name("gasm-media".into())
440 .spawn(move || run(url, policy, ctl))
441 .map_err(|e| format!("gasm:media: no thread: {e}"))?;
442 }
443 Ok(())
444 }
445
446 pub fn pause(&mut self, h: i32) -> Result<(), String> {
447 let Some(s) = self.get(h)? else { return Ok(()) };
448 if s.want.swap(false, Ordering::AcqRel) {
449 s.voice.playing.store(false, Ordering::Release);
450 *s.paused_at.lock().unwrap() = Some(Instant::now());
451 let mut i = s.info.lock().unwrap();
452 if matches!(i.state, PLAYING | CONNECTING) && s.started {
453 i.state = PAUSED;
454 }
455 }
456 Ok(())
457 }
458
459 pub fn close(&mut self, h: i32) -> Result<(), String> {
460 let bus = self.bus.clone();
461 match self.streams.get_mut(&h) {
462 None => Err(format!("gasm:media: {h} is not a stream handle")),
463 Some(slot) => {
464 if let Some(s) = slot.take() {
465 s.closed.store(true, Ordering::Release);
466 if let Some(b) = bus {
467 b.remove(&s.voice);
468 }
469 }
470 Ok(())
471 }
472 }
473 }
474
475 pub fn set_volume(&mut self, h: i32, gain: f32) -> Result<(), String> {
476 let g = if gain.is_nan() { 0.0 } else { gain.clamp(0.0, 1.0) };
477 if let Some(s) = self.get(h)? {
478 s.voice.gain.store(g.to_bits(), Ordering::Relaxed);
479 }
480 Ok(())
481 }
482
483 pub fn set_lowpass(&mut self, h: i32, hz: f32) -> Result<(), String> {
484 let hz = if hz.is_nan() || hz <= 0.0 { 0.0 } else { hz.clamp(20.0, 20_000.0) };
485 if let Some(s) = self.get(h)? {
486 s.voice.lowpass.store(hz.to_bits(), Ordering::Relaxed);
487 }
488 Ok(())
489 }
490
491 fn info<R>(&mut self, h: i32, f: impl FnOnce(&Info) -> R, closed: R) -> Result<R, String> {
492 Ok(match self.get(h)? {
493 Some(s) => f(&s.info.lock().unwrap()),
494 None => closed,
495 })
496 }
497
498 pub fn caps(&mut self, h: i32) -> Result<u32, String> {
499 self.info(h, |i| i.caps, 0)
500 }
501
502 pub fn state(&mut self, h: i32) -> Result<u32, String> {
503 self.info(h, |i| i.state, FAILED)
504 }
505
506 pub fn error(&mut self, h: i32) -> Result<String, String> {
507 if h == -1 {
508 return Ok(self.last_refusal.clone());
509 }
510 self.info(h, |i| i.error.clone(), "closed".into())
511 }
512
513 pub fn position(&mut self, h: i32) -> Result<f64, String> {
514 let rate = self.bus.as_ref().map_or(48_000, |b| b.rate) as f64;
515 Ok(match self.get(h)? {
516 Some(s) => s.voice.played.load(Ordering::Relaxed) as f64 / rate,
517 None => 0.0,
518 })
519 }
520
521 pub fn duration(&mut self, h: i32) -> Result<f64, String> {
522 self.info(h, |i| i.duration, -1.0)
523 }
524
525 pub fn metadata(&mut self, h: i32) -> Result<String, String> {
526 self.info(h, Info::metadata, Info::default().metadata())
527 }
528
529 pub fn metadata_version(&mut self, h: i32) -> Result<u32, String> {
530 self.info(h, |i| i.version, 0)
531 }
532}
533
534impl Drop for Media {
535 fn drop(&mut self) {
536 for s in self.streams.values().flatten() {
537 s.closed.store(true, Ordering::Release);
538 if let Some(b) = &self.bus {
539 b.remove(&s.voice);
540 }
541 }
542 }
543}
544
545struct Control {
548 voice: Arc<Voice>,
549 info: Arc<Mutex<Info>>,
550 want: Arc<AtomicBool>,
551 closed: Arc<AtomicBool>,
552 paused_at: Arc<Mutex<Option<Instant>>>,
553 rate: u32,
554}
555
556impl Control {
557 fn closed(&self) -> bool {
558 self.closed.load(Ordering::Acquire)
559 }
560
561 fn set(&self, f: impl FnOnce(&mut Info)) {
562 f(&mut self.info.lock().unwrap());
563 }
564}
565
566fn agent() -> &'static ureq::Agent {
567 static AGENT: OnceLock<ureq::Agent> = OnceLock::new();
568 AGENT.get_or_init(|| {
569 let roots = rustls_native_certs::load_native_certs();
570 let certs = roots.certs.iter().map(|c| ureq::tls::Certificate::from_der(c.as_ref()).to_owned()).collect();
571 let tls = ureq::tls::TlsConfig::builder()
572 .provider(ureq::tls::TlsProvider::Rustls)
573 .root_certs(ureq::tls::RootCerts::Specific(Arc::new(certs)))
574 .unversioned_rustls_crypto_provider(Arc::new(rustls::crypto::ring::default_provider()))
575 .build();
576 ureq::Agent::config_builder()
578 .http_status_as_error(false)
579 .max_redirects(0)
580 .max_redirects_will_error(false)
581 .timeout_connect(Some(Duration::from_secs(30)))
582 .timeout_recv_response(Some(Duration::from_secs(30)))
583 .tls_config(tls)
584 .build()
585 .into()
586 })
587}
588
589#[derive(Default)]
591struct Head {
592 metaint: Option<usize>,
593 name: Option<String>,
594 bitrate: Option<u32>,
595 content_type: Option<String>,
596 live: bool,
598}
599
600fn connect(url: &str, policy: &MediaPolicy, ctl: &Control) -> Result<(Head, ureq::Body), Refused> {
603 let mut url = url.to_owned();
604 for _ in 0..=MAX_REDIRECTS {
605 policy.check_url(&url)?;
606 let req = ureq::http::Request::builder()
607 .method("GET")
608 .uri(url.as_str())
609 .header("user-agent", crate::fetch::user_agent(None))
610 .header("icy-metadata", "1")
611 .body(())
612 .map_err(|e| format!("{url}: {e}"))?;
613 let resp = agent().run(req).map_err(|e| Refused::Retry(format!("{url}: {e}")))?;
614 let status = resp.status().as_u16();
615 let header = |n: &str| resp.headers().get(n).and_then(|v| v.to_str().ok()).map(|s| s.trim().to_owned());
616 if matches!(status, 301 | 302 | 303 | 307 | 308) {
617 let Some(loc) = header("location") else { return Err(format!("{url}: a redirect without a location").into()) };
618 url = crate::fetch::resolve_url(&url, &loc);
619 if ctl.closed() {
620 return Err(Refused::Fatal(String::new()));
621 }
622 continue;
623 }
624 if status >= 500 {
625 return Err(Refused::Retry(format!("{url}: HTTP {status}")));
626 }
627 if status != 200 {
628 return Err(Refused::Fatal(format!("{url}: HTTP {status}")));
629 }
630 let metaint = header("icy-metaint").and_then(|v| v.parse().ok()).filter(|&n: &usize| n > 0 && n <= 1 << 20);
631 let length = header("content-length").is_some();
632 let head = Head {
633 metaint,
634 name: header("icy-name").filter(|n| !n.is_empty()).map(|n| n.chars().take(MAX_TITLE).collect()),
635 bitrate: header("icy-br").and_then(|v| v.split(',').next().and_then(|b| b.trim().parse().ok())),
636 content_type: header("content-type").map(|c| c.to_ascii_lowercase()),
637 live: metaint.is_some() || header("icy-name").is_some() || !length,
638 };
639 return Ok((head, resp.into_body()));
640 }
641 Err(Refused::Fatal(format!("{url}: too many redirects")))
642}
643
644fn pump(body: ureq::Body, head_metaint: Option<usize>, info: Arc<Mutex<Info>>, tx: SyncSender<Vec<u8>>, closed: Arc<AtomicBool>) {
647 let mut reader = body.into_reader();
648 let mut icy = head_metaint.map(Icy::new);
649 let mut buf = vec![0u8; 16 * 1024];
650 loop {
651 if closed.load(Ordering::Acquire) {
652 return;
653 }
654 let n = match reader.read(&mut buf) {
655 Ok(0) | Err(_) => return,
656 Ok(n) => n,
657 };
658 let audio = match &mut icy {
659 Some(icy) => {
660 let (audio, titles) = icy.feed(&buf[..n]);
661 for t in titles {
662 let mut i = info.lock().unwrap();
663 if i.title.as_deref() != Some(&t) {
664 i.title = Some(t);
665 i.version = i.version.wrapping_add(1);
666 }
667 }
668 audio
669 }
670 None => buf[..n].to_vec(),
671 };
672 if !audio.is_empty() && tx.send(audio).is_err() {
673 return;
674 }
675 }
676}
677
678struct Icy {
681 metaint: usize,
682 left: usize,
684 meta_left: Option<usize>,
686 meta: Vec<u8>,
687}
688
689impl Icy {
690 fn new(metaint: usize) -> Icy {
691 Icy { metaint, left: metaint, meta_left: None, meta: Vec::new() }
692 }
693
694 fn feed(&mut self, mut data: &[u8]) -> (Vec<u8>, Vec<String>) {
696 let (mut audio, mut titles) = (Vec::with_capacity(data.len()), Vec::new());
697 while !data.is_empty() {
698 match self.meta_left {
699 None if self.left > 0 => {
700 let n = self.left.min(data.len());
701 audio.extend_from_slice(&data[..n]);
702 self.left -= n;
703 data = &data[n..];
704 }
705 None => {
706 self.meta_left = Some(data[0] as usize * 16);
707 self.meta.clear();
708 data = &data[1..];
709 }
710 Some(m) => {
711 let n = m.min(data.len());
712 self.meta.extend_from_slice(&data[..n]);
713 data = &data[n..];
714 if m == n {
715 if let Some(t) = stream_title(&self.meta) {
716 titles.push(t);
717 }
718 self.meta_left = None;
719 self.left = self.metaint;
720 } else {
721 self.meta_left = Some(m - n);
722 }
723 }
724 }
725 if self.meta_left == Some(0) {
726 self.meta_left = None;
727 self.left = self.metaint;
728 }
729 }
730 (audio, titles)
731 }
732}
733
734pub fn stream_title(meta: &[u8]) -> Option<String> {
736 let end = meta.iter().position(|&b| b == 0).unwrap_or(meta.len());
737 let meta = &meta[..end];
738 let key = b"StreamTitle='";
739 let start = meta.windows(key.len()).position(|w| w == key)? + key.len();
740 let rest = &meta[start..];
741 let stop = rest.windows(2).position(|w| w == b"';").unwrap_or(rest.len());
742 let raw = &rest[..stop];
743 let text = match std::str::from_utf8(raw) {
744 Ok(s) => s.to_owned(),
745 Err(_) => raw.iter().map(|&b| b as char).collect(),
746 };
747 let text: String = text.chars().filter(|c| !c.is_control()).collect();
748 let text = text.trim();
749 if text.is_empty() {
750 return None;
751 }
752 let mut t = text.to_owned();
753 while t.len() > MAX_TITLE {
754 t.pop();
755 }
756 Some(t)
757}
758
759struct Pipe {
762 rx: Mutex<Receiver<Vec<u8>>>,
763 chunk: Vec<u8>,
764 at: usize,
765 closed: Arc<AtomicBool>,
766}
767
768impl Read for Pipe {
769 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
770 while self.at >= self.chunk.len() {
771 let deadline = Instant::now() + STALL;
772 loop {
773 if self.closed.load(Ordering::Acquire) {
774 return Ok(0);
775 }
776 match self.rx.lock().unwrap().recv_timeout(Duration::from_millis(250)) {
777 Ok(c) => {
778 self.chunk = c;
779 self.at = 0;
780 break;
781 }
782 Err(RecvTimeoutError::Disconnected) => return Ok(0),
783 Err(RecvTimeoutError::Timeout) if Instant::now() >= deadline => {
784 return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "the stream stalled"));
785 }
786 Err(RecvTimeoutError::Timeout) => {}
787 }
788 }
789 }
790 let n = out.len().min(self.chunk.len() - self.at);
791 out[..n].copy_from_slice(&self.chunk[self.at..self.at + n]);
792 self.at += n;
793 Ok(n)
794 }
795}
796
797enum Refused {
800 Retry(String),
801 Fatal(String),
802}
803
804impl From<String> for Refused {
805 fn from(why: String) -> Refused {
806 Refused::Fatal(why)
807 }
808}
809
810enum End {
812 Eof,
814 Dropped(String),
816 Released,
818 Closed,
820}
821
822fn run(url: String, policy: MediaPolicy, ctl: Control) {
825 loop {
827 if ctl.closed() {
828 return;
829 }
830 match policy.answered() {
831 Ok(true) => break,
832 Ok(false) => std::thread::sleep(Duration::from_millis(50)),
833 Err(why) => return fail(&ctl, why),
834 }
835 }
836 let mut tries = 0;
837 loop {
838 if ctl.closed() {
839 return;
840 }
841 while !ctl.want.load(Ordering::Acquire) {
843 if ctl.closed() {
844 return;
845 }
846 std::thread::sleep(Duration::from_millis(50));
847 }
848 ctl.set(|i| {
849 if i.state != PAUSED {
850 i.state = CONNECTING;
851 }
852 });
853 let began = Instant::now();
854 let outcome = connection(&url, &policy, &ctl);
855 if began.elapsed() >= Duration::from_secs(10) {
857 tries = 0;
858 }
859 let live = match outcome {
860 Ok((End::Closed, _)) => return,
861 Ok((End::Eof, false)) => return finish(&ctl),
862 Ok((End::Released, _)) => {
863 tries = 0;
864 continue;
865 }
866 Ok((End::Eof | End::Dropped(_), true)) => true,
867 Ok((End::Dropped(why), false)) => return fail(&ctl, why),
868 Err(Refused::Fatal(why)) if why.is_empty() => return,
869 Err(Refused::Fatal(why)) => return fail(&ctl, why),
870 Err(Refused::Retry(why)) => {
871 if tries >= RECONNECTS {
872 return fail(&ctl, why);
873 }
874 eprintln!("[gasm] media: {why}");
875 false
876 }
877 };
878 if live {
879 eprintln!("[gasm] media: {url}: the stream dropped, reconnecting");
880 ctl.set(|i| {
882 if i.state != PAUSED {
883 i.state = CONNECTING;
884 }
885 });
886 }
887 tries += 1;
888 if tries > RECONNECTS {
889 return fail(&ctl, format!("{url}: the stream dropped {RECONNECTS} times"));
890 }
891 std::thread::sleep(Duration::from_secs(1 << (tries - 1)));
892 }
893}
894
895fn fail(ctl: &Control, why: String) {
896 eprintln!("[gasm] media: {why}");
897 ctl.voice.playing.store(false, Ordering::Release);
898 ctl.set(|i| {
899 i.state = FAILED;
900 i.error = why;
901 });
902}
903
904fn finish(ctl: &Control) {
906 while ctl.voice.buffered() > 0 && !ctl.closed() {
907 std::thread::sleep(Duration::from_millis(20));
908 }
909 ctl.voice.playing.store(false, Ordering::Release);
910 ctl.set(|i| i.state = ENDED);
911}
912
913fn connection(url: &str, policy: &MediaPolicy, ctl: &Control) -> Result<(End, bool), Refused> {
916 use symphonia::core::codecs::CodecParameters;
917 use symphonia::core::codecs::audio::AudioDecoderOptions;
918 use symphonia::core::formats::probe::Hint;
919 use symphonia::core::formats::{FormatOptions, TrackType};
920 use symphonia::core::io::{MediaSourceStream, ReadOnlySource};
921 use symphonia::core::meta::MetadataOptions;
922
923 let (head, body) = connect(url, policy, ctl)?;
924 let live = head.live;
925 ctl.set(|i| {
926 let (name, bitrate) = (head.name.clone().or(i.name.clone()), head.bitrate.or(i.bitrate));
927 if (&name, bitrate) != (&i.name, i.bitrate) {
928 (i.name, i.bitrate) = (name, bitrate);
929 i.version = i.version.wrapping_add(1);
930 }
931 });
932 let (tx, rx) = sync_channel::<Vec<u8>>(64);
933 {
934 let (info, closed) = (ctl.info.clone(), ctl.closed.clone());
935 let metaint = head.metaint;
936 std::thread::Builder::new()
937 .name("gasm-media-net".into())
938 .spawn(move || pump(body, metaint, info, tx, closed))
939 .map_err(|e| format!("no thread: {e}"))?;
940 }
941 let pipe = Pipe { rx: Mutex::new(rx), chunk: Vec::new(), at: 0, closed: ctl.closed.clone() };
942 let mss = MediaSourceStream::new(Box::new(ReadOnlySource::new(pipe)), Default::default());
943 let mut hint = Hint::new();
944 match head.content_type.as_deref().map(|c| c.split(';').next().unwrap_or("").trim()) {
945 Some("audio/mpeg" | "audio/mp3" | "audio/mpeg3") => drop(hint.with_extension("mp3")),
946 Some("audio/aac" | "audio/aacp" | "audio/x-aac") => drop(hint.with_extension("aac")),
947 Some("audio/ogg" | "application/ogg" | "audio/vorbis") => drop(hint.with_extension("ogg")),
948 Some("audio/flac" | "audio/x-flac") => drop(hint.with_extension("flac")),
949 _ => {}
950 }
951 let mut format = symphonia::default::get_probe()
952 .probe(&hint, mss, FormatOptions::default(), MetadataOptions::default())
953 .map_err(|e| format!("{url}: not a stream gasm can play ({e})"))?;
954 let track = format.default_track(TrackType::Audio).ok_or_else(|| format!("{url}: no audio in it"))?;
955 let id = track.id;
956 let Some(CodecParameters::Audio(params)) = &track.codec_params else { return Err(format!("{url}: no audio in it").into()) };
957 let codec = symphonia::default::get_codecs()
958 .get_audio_decoder(params.codec)
959 .map(|c| c.codec.info.short_name.to_string())
960 .unwrap_or_else(|| "?".into());
961 let duration = match (track.num_frames, params.sample_rate) {
962 (Some(n), Some(r)) if !live && r > 0 => n as f64 / r as f64,
963 _ => -1.0,
964 };
965 let mut decoder = symphonia::default::get_codecs()
966 .make_audio_decoder(params, &AudioDecoderOptions::default())
967 .map_err(|e| format!("{url}: can't decode {codec} ({e})"))?;
968 ctl.set(|i| {
969 if i.codec.as_deref() != Some(codec.as_str()) {
970 i.codec = Some(codec.clone());
971 i.version = i.version.wrapping_add(1);
972 }
973 i.duration = duration;
974 i.caps = CAN_VOLUME | CAN_LOWPASS | CAN_METADATA;
975 });
976
977 let mut resampler = Resampler::new(ctl.rate);
978 let (mut src_rate, mut src_ch) = (0u32, 0usize);
979 let (mut samples, mut bytes, mut out) = (Vec::<f32>::new(), Vec::<u8>::new(), Vec::<f32>::new());
980 let cap = (BUFFER_S * ctl.rate as f64) as usize;
981 let start = (START_S * ctl.rate as f64) as usize;
982 let (mut announced, mut decoded) = (false, 0usize);
985 loop {
986 if ctl.closed() {
987 return Ok((End::Closed, live));
988 }
989 if !ctl.want.load(Ordering::Acquire) {
991 let since = *ctl.paused_at.lock().unwrap();
992 if live && since.is_some_and(|t| t.elapsed() >= PAUSE_DROP) {
993 ctl.voice.buf.lock().unwrap().clear();
994 return Ok((End::Released, live));
995 }
996 std::thread::sleep(Duration::from_millis(20));
997 continue;
998 }
999 let resumed = announced && !ctl.voice.playing.load(Ordering::Acquire) && ctl.voice.buffered() >= start;
1001 if (!announced && decoded >= start) || resumed {
1002 announced = true;
1003 ctl.voice.playing.store(true, Ordering::Release);
1004 ctl.set(|i| {
1005 i.state = PLAYING;
1006 i.error.clear();
1007 });
1008 }
1009 if ctl.voice.buffered() >= cap {
1010 std::thread::sleep(Duration::from_millis(20));
1011 continue;
1012 }
1013 let packet = match format.next_packet() {
1014 Ok(Some(p)) => p,
1015 Ok(None) => return Ok((End::Eof, live)),
1016 Err(symphonia::core::errors::Error::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok((End::Eof, live)),
1017 Err(e) => return Ok((End::Dropped(format!("{url}: {e}")), live)),
1018 };
1019 if packet.track_id != id {
1020 continue;
1021 }
1022 let buf = match decoder.decode(&packet) {
1023 Ok(b) => b,
1024 Err(symphonia::core::errors::Error::DecodeError(_)) => continue,
1026 Err(e) => return Ok((End::Dropped(format!("{url}: {e}")), live)),
1027 };
1028 let (rate, ch) = (buf.spec().rate(), buf.spec().channels().count());
1029 if ch == 0 || rate == 0 {
1030 continue;
1031 }
1032 if (rate, ch) != (src_rate, src_ch) {
1033 (src_rate, src_ch) = (rate, ch);
1034 resampler.configure(rate, ch.min(2) as u32);
1035 }
1036 samples.clear();
1037 buf.copy_to_vec_interleaved(&mut samples);
1038 bytes.clear();
1040 for frame in samples.chunks(ch) {
1041 for s in &frame[..ch.min(2)] {
1042 bytes.extend_from_slice(&s.to_le_bytes());
1043 }
1044 }
1045 out.clear();
1046 resampler.process(&bytes, &mut out);
1047 decoded += out.len() / 2;
1048 ctl.voice.buf.lock().unwrap().extend(out.iter().copied());
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 #[test]
1057 fn descriptions() {
1058 assert_eq!(parse_desc(r#"{"url":"https://radio.example/stream"}"#).unwrap(), "https://radio.example/stream");
1059 for bad in [r#"[]"#, r#"{}"#, r#"{"url":"http://radio.example/"}"#, r#"{"url":"https://"}"#, r#"{"url":"https://a b/"}"#] {
1060 assert!(parse_desc(bad).is_err(), "{bad}");
1061 }
1062 }
1063
1064 #[test]
1065 fn addresses() {
1066 for ip in ["127.0.0.1", "10.1.2.3", "172.16.0.1", "192.168.1.1", "169.254.0.1", "100.64.0.1", "0.0.0.0", "::1", "fc00::1", "fe80::1", "::ffff:192.168.0.1", "224.0.0.1"] {
1067 assert!(!is_public(ip.parse().unwrap()), "{ip}");
1068 }
1069 for ip in ["1.1.1.1", "8.8.8.8", "2606:4700::1111"] {
1070 assert!(is_public(ip.parse().unwrap()), "{ip}");
1071 }
1072 let p = MediaPolicy::new(Some(vec![]));
1073 assert!(p.check_url("https://127.0.0.1:8443/x").is_err());
1074 assert!(p.check_url("https://[::1]/x").is_err());
1075 let listed = MediaPolicy::new(Some(vec!["127.0.0.1".into()]));
1076 assert!(listed.check_url("https://127.0.0.1:8443/x").is_ok());
1077 assert!(listed.check_url("https://1.1.1.1/x").is_err(), "a list allows only its hosts");
1078 assert!(MediaPolicy::new(Some(vec![])).check_url("http://1.1.1.1/x").is_err());
1079 }
1080
1081 #[test]
1082 fn permission() {
1083 assert!(MediaPolicy::default().may_open().is_err(), "no manifest entry, no flag");
1084 let declared = MediaPolicy { declared: true, ..Default::default() };
1085 assert!(declared.may_open().is_err(), "headless: no one to ask");
1086 assert!(MediaPolicy::new(Some(vec![])).may_open().is_ok());
1087 }
1088
1089 #[test]
1090 fn icy_blocks() {
1091 let mut icy = Icy::new(4);
1092 let meta = b"StreamTitle='Band - Song';StreamUrl='';";
1093 let blocks = meta.len().div_ceil(16);
1094 let mut data = b"abcd".to_vec();
1095 data.push(blocks as u8);
1096 data.extend_from_slice(meta);
1097 data.resize(5 + blocks * 16, 0);
1098 data.extend_from_slice(b"efgh");
1099 data.push(0); data.extend_from_slice(b"ij");
1101 let (mut audio, mut titles) = (Vec::new(), Vec::new());
1103 for piece in data.chunks(3) {
1104 let (a, t) = icy.feed(piece);
1105 audio.extend(a);
1106 titles.extend(t);
1107 }
1108 assert_eq!(audio, b"abcdefghij");
1109 assert_eq!(titles, vec!["Band - Song".to_string()]);
1110 assert_eq!(stream_title(b"StreamTitle='Caf\xe9';"), Some("Café".into()));
1111 assert_eq!(stream_title(b"StreamTitle='';"), None);
1112 }
1113
1114 #[test]
1115 fn filter_is_unity_at_dc() {
1116 let [b0, b1, b2, a1, a2] = lowpass(1000.0, 48_000.0);
1117 assert!(((b0 + b1 + b2) / (1.0 + a1 + a2) - 1.0).abs() < 1e-4);
1118 }
1119}