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}