tori_player/source.rs
1use crate::Result;
2use std::{
3 fs::File,
4 io,
5 path::Path,
6 process::{Command, Stdio},
7 thread,
8};
9
10use symphonia::core::{
11 codecs::{DecoderOptions, CODEC_TYPE_NULL},
12 errors::Error as SymError,
13 formats::FormatOptions,
14 io::{MediaSource, MediaSourceStream, ReadOnlySource},
15 meta::MetadataOptions,
16 probe::Hint,
17};
18
19use crate::output::CpalAudioOutput;
20
21// TODO: remove `expects` and `unwraps`
22pub fn start_player_thread(path: &str) {
23 let (mss, hint) = mss_from_path(path).unwrap();
24
25 // Use the default options for metadata and format readers.
26 let meta_opts: MetadataOptions = Default::default();
27 let fmt_opts = FormatOptions {
28 enable_gapless: true,
29 ..Default::default()
30 };
31
32 // Probe the media source.
33 let probed = symphonia::default::get_probe()
34 .format(&hint, mss, &fmt_opts, &meta_opts)
35 .expect("unsupported format");
36
37 // Get the instantiated format reader.
38 let mut format = probed.format;
39
40 // Find the first audio track with a known (decodeable) codec.
41 let track = format
42 .tracks()
43 .iter()
44 .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
45 .expect("no supported audio tracks");
46
47 // Use the default options for the decoder.
48 let dec_opts: DecoderOptions = Default::default();
49
50 // Create a decoder for the track.
51 let mut decoder = symphonia::default::get_codecs()
52 .make(&track.codec_params, &dec_opts)
53 .expect("unsupported codec");
54
55 // Store the track identifier, it will be used to filter packets.
56 let track_id = track.id;
57
58 // Spawn a thread to read packets from the format reader and send them to the decoder.
59 // TODO: if the user pauses the player, this thread continues to run. Should this really be the
60 // case?
61 thread::spawn(move || {
62 let mut audio_output = None;
63 loop {
64 let packet = match format.next_packet() {
65 Ok(packet) => packet,
66 Err(SymError::ResetRequired) => {
67 // The track list has been changed. Re-examine it and create a new set of decoders,
68 // then restart the decode loop. This is an advanced feature and it is not
69 // unreasonable to consider this "the end." As of v0.5.0, the only usage of this is
70 // for chained OGG physical streams.
71 unimplemented!();
72 }
73 Err(SymError::IoError(e))
74 if e.kind() == io::ErrorKind::UnexpectedEof
75 && e.to_string() == "end of stream" =>
76 {
77 // File ended
78 break;
79 }
80 Err(err) => {
81 // A unrecoverable error occurred, halt decoding.
82 panic!("{}", err);
83 }
84 };
85
86 // Consume any new metadata that has been read since the last packet.
87 while !format.metadata().is_latest() {
88 // Pop the old head of the metadata queue.
89 format.metadata().pop();
90
91 // Consume the new metadata at the head of the metadata queue.
92 eprintln!("Got new metadata! {:?}", format.metadata().current());
93 }
94
95 // If the packet does not belong to the selected track, skip over it.
96 if packet.track_id() != track_id {
97 continue;
98 }
99
100 match decoder.decode(&packet) {
101 Ok(decoded) => {
102 // If the audio output is not open, try to open it.
103 if audio_output.is_none() {
104 // Get the audio buffer specification. This is a description of the decoded
105 // audio buffer's sample format and sample rate.
106 let spec = *decoded.spec();
107
108 // Get the capacity of the decoded buffer. Note that this is capacity, not
109 // length! The capacity of the decoded buffer is constant for the life of the
110 // decoder, but the length is not.
111 let duration = decoded.capacity() as u64;
112
113 // Try to open the audio output.
114 audio_output.replace(CpalAudioOutput::try_open(spec, duration).unwrap());
115 } else {
116 // TODO: Check the audio spec. and duration hasn't changed.
117 }
118
119 // Write the decoded audio samples to the audio output if the presentation timestamp
120 // for the packet is >= the seeked position (0 if not seeking).
121 if let Some(audio_output) = audio_output.as_mut() {
122 audio_output.write(decoded).unwrap()
123 }
124 }
125 Err(SymError::IoError(_)) => {
126 // The packet failed to decode due to an IO error, skip the packet.
127 continue;
128 }
129 Err(SymError::DecodeError(_)) => {
130 // The packet failed to decode due to invalid data, skip the packet.
131 continue;
132 }
133 Err(err) => {
134 // An unrecoverable error occurred, halt decoding.
135 panic!("{}", err);
136 }
137 }
138 }
139 });
140}
141
142fn mss_from_path(mut path: &str) -> Result<(MediaSourceStream, Hint)> {
143 let mut force_ytdlp = false;
144 if let Some(url) = path.strip_prefix("ytdlp://") {
145 path = url;
146 force_ytdlp = true;
147 }
148
149 let mut hint = Hint::default();
150 let src: Box<dyn MediaSource> =
151 if force_ytdlp || path.starts_with("http://") || path.starts_with("https://") {
152 // Get urls from yt-dlp
153 let ytdlp_output = Command::new("yt-dlp")
154 .args(["-g", path])
155 .output()
156 .unwrap()
157 .stdout;
158 let ytdlp_output = String::from_utf8(ytdlp_output).unwrap();
159 let ytdlp_urls = ytdlp_output.lines();
160
161 // Get ffmpeg mpegts stream.
162 let mut ffmpeg = Command::new("ffmpeg");
163 for url in ytdlp_urls {
164 ffmpeg.args(["-i", url]);
165 }
166 ffmpeg
167 .args(["-f", "mp3"]) // FIXME: don't do this. If you know how to do better please tell me how. Symphonia still doesn't support opus afaik.
168 .arg("-")
169 .stdout(Stdio::piped())
170 .stderr(Stdio::null())
171 .stdin(Stdio::null());
172 let mut ffmpeg = ffmpeg.spawn().unwrap();
173 let src = ffmpeg.stdout.take().unwrap();
174
175 hint.with_extension("mp3");
176 Box::new(ReadOnlySource::new(src))
177 } else {
178 if let Some(ext) = Path::new(path).extension().and_then(|s| s.to_str()) {
179 hint.with_extension(ext);
180 }
181 Box::new(File::open(path).expect("failed to open media"))
182 };
183
184 let mss = MediaSourceStream::new(src, Default::default());
185 Ok((mss, hint))
186}