aede_core/playback/
output.rs1use std::ffi::OsString;
9use std::fmt;
10use std::io::{self, Write};
11use std::path::PathBuf;
12use std::process::{Child, ChildStdin, Command, Stdio};
13
14pub use super::format::PcmStreamFormat as OutputFormat;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum OutputChange {
19 Opened,
21 Reused,
23 ReopenedForFormat,
25}
26
27#[derive(Debug)]
29pub enum OutputError {
30 InvalidLayout,
32 Start(io::Error),
34 MissingInput,
36 Device(io::Error),
38 Exit(std::process::ExitStatus),
40}
41
42impl fmt::Display for OutputError {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::InvalidLayout => f.write_str("audio output needs a known channel layout"),
46 Self::Start(error) => write!(
47 f,
48 "cannot start ffplay: {error}; install ffplay for local audio output"
49 ),
50 Self::MissingInput => f.write_str("ffplay did not open its PCM input"),
51 Self::Device(error) => write!(f, "audio output failed: {error}"),
52 Self::Exit(status) => write!(
53 f,
54 "ffplay stopped with {status}; check the audio output device"
55 ),
56 }
57 }
58}
59
60impl std::error::Error for OutputError {}
61
62pub struct OutputSession {
69 program: PathBuf,
70 process: Option<Child>,
71 input: Option<ChildStdin>,
72 format: Option<OutputFormat>,
73}
74
75impl Default for OutputSession {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl OutputSession {
82 pub fn new() -> Self {
84 Self::with_program(PathBuf::from("ffplay"))
85 }
86
87 fn with_program(program: PathBuf) -> Self {
88 Self {
89 program,
90 process: None,
91 input: None,
92 format: None,
93 }
94 }
95
96 pub fn format(&self) -> Option<OutputFormat> {
98 self.format
99 }
100
101 pub fn process_id(&self) -> Option<u32> {
103 self.process.as_ref().map(Child::id)
104 }
105
106 pub fn prepare(&mut self, format: OutputFormat) -> Result<OutputChange, OutputError> {
108 let layout = format
109 .layout()
110 .ffmpeg_name()
111 .ok_or(OutputError::InvalidLayout)?;
112 if self.format == Some(format) && self.input.is_some() && !self.poll_finished()? {
113 return Ok(OutputChange::Reused);
114 }
115 let changed = self.format.is_some_and(|held| held != format);
116 self.finish()?;
117 let mut command = Command::new(&self.program);
118 command
119 .args([
120 OsString::from("-hide_banner"),
121 OsString::from("-loglevel"),
122 OsString::from("error"),
123 OsString::from("-nodisp"),
124 OsString::from("-autoexit"),
125 OsString::from("-nostats"),
126 OsString::from("-f"),
127 OsString::from("f32le"),
128 OsString::from("-sample_rate"),
129 OsString::from(format.sample_rate().to_string()),
130 OsString::from("-ch_layout"),
131 OsString::from(layout),
132 OsString::from("-i"),
133 OsString::from("pipe:0"),
134 ])
135 .stdin(Stdio::piped())
136 .stdout(Stdio::null());
137 let mut process = command.spawn().map_err(OutputError::Start)?;
138 let Some(input) = process.stdin.take() else {
139 let _ = process.kill();
140 let _ = process.wait();
141 return Err(OutputError::MissingInput);
142 };
143 self.process = Some(process);
144 self.input = Some(input);
145 self.format = Some(format);
146 Ok(if changed {
147 OutputChange::ReopenedForFormat
148 } else {
149 OutputChange::Opened
150 })
151 }
152
153 pub fn close_input(&mut self) {
155 drop(self.input.take());
156 }
157
158 pub fn poll_finished(&mut self) -> Result<bool, OutputError> {
160 let Some(process) = self.process.as_mut() else {
161 return Ok(true);
162 };
163 let Some(status) = process.try_wait().map_err(OutputError::Device)? else {
164 return Ok(false);
165 };
166 drop(self.input.take());
167 drop(self.process.take());
168 self.format = None;
169 if !status.success() {
170 return Err(OutputError::Exit(status));
171 }
172 Ok(true)
173 }
174
175 pub fn finish(&mut self) -> Result<(), OutputError> {
177 self.close_input();
178 let Some(mut process) = self.process.take() else {
179 self.format = None;
180 return Ok(());
181 };
182 let status = process.wait().map_err(OutputError::Device)?;
183 self.format = None;
184 if !status.success() {
185 return Err(OutputError::Exit(status));
186 }
187 Ok(())
188 }
189
190 pub fn abort(&mut self) -> Result<(), OutputError> {
192 self.close_input();
193 let Some(mut process) = self.process.take() else {
194 self.format = None;
195 return Ok(());
196 };
197 let result = process.kill();
198 let waited = process.wait();
199 self.format = None;
200 if let Err(error) = result
201 && error.kind() != io::ErrorKind::InvalidInput
202 {
203 return Err(OutputError::Device(error));
204 }
205 waited.map_err(OutputError::Device)?;
206 Ok(())
207 }
208}
209
210impl Write for OutputSession {
211 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
212 self.input
213 .as_mut()
214 .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "audio output is closed"))?
215 .write(bytes)
216 }
217
218 fn flush(&mut self) -> io::Result<()> {
219 self.input
220 .as_mut()
221 .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "audio output is closed"))?
222 .flush()
223 }
224}
225
226impl Drop for OutputSession {
227 fn drop(&mut self) {
228 let _ = self.abort();
229 }
230}
231
232#[cfg(test)]
233#[path = "output_tests.rs"]
234mod tests;