tools: add sync-version.js to sync package.json -> Tauri files
- Add tools/sync-version.js script to read root package.json version and update src-tauri/tauri.conf.json and src-tauri/Cargo.toml. - Update only the [package] version line in Cargo.toml to preserve formatting. - Include JSON read/write helpers and basic error handling/reporting.
This commit is contained in:
@@ -11,6 +11,21 @@ use std::time::Duration;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use ringbuf::HeapRb;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
fn command_hidden(program: &OsString) -> Command {
|
||||
let mut cmd = Command::new(program);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PlayerStatus {
|
||||
@@ -53,8 +68,16 @@ impl PlayerShared {
|
||||
#[derive(Debug)]
|
||||
pub enum PlayerCommand {
|
||||
Play { url: String },
|
||||
// Cast-only playback: decode to PCM and keep it available for cast taps,
|
||||
// but do not open a CPAL output stream.
|
||||
PlayCast { url: String },
|
||||
Stop,
|
||||
SetVolume { volume: f32 },
|
||||
CastTapStart {
|
||||
port: u16,
|
||||
reply: mpsc::Sender<Result<(), String>>,
|
||||
},
|
||||
CastTapStop,
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
@@ -103,7 +126,7 @@ fn set_error(shared: &'static PlayerShared, message: String) {
|
||||
s.error = Some(message);
|
||||
}
|
||||
|
||||
fn ffmpeg_command() -> OsString {
|
||||
pub(crate) fn ffmpeg_command() -> OsString {
|
||||
// Step 2: external ffmpeg binary.
|
||||
// Lookup order:
|
||||
// 1) RADIOPLAYER_FFMPEG (absolute or relative)
|
||||
@@ -139,19 +162,9 @@ fn ffmpeg_command() -> OsString {
|
||||
OsString::from(local_name)
|
||||
}
|
||||
|
||||
pub fn preflight_check() -> Result<(), String> {
|
||||
// Ensure we have an output device up-front so UI gets a synchronous error.
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| "No default audio output device".to_string())?;
|
||||
let _ = device
|
||||
.default_output_config()
|
||||
.map_err(|e| format!("Failed to get output config: {e}"))?;
|
||||
|
||||
// Ensure ffmpeg can be executed.
|
||||
pub fn preflight_ffmpeg_only() -> Result<(), String> {
|
||||
let ffmpeg = ffmpeg_command();
|
||||
let status = Command::new(&ffmpeg)
|
||||
let status = command_hidden(&ffmpeg)
|
||||
.arg("-version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -165,38 +178,82 @@ pub fn preflight_check() -> Result<(), String> {
|
||||
if !status.success() {
|
||||
return Err("FFmpeg exists but returned non-zero for -version".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn preflight_check() -> Result<(), String> {
|
||||
// Ensure we have an output device up-front so UI gets a synchronous error.
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| "No default audio output device".to_string())?;
|
||||
let _ = device
|
||||
.default_output_config()
|
||||
.map_err(|e| format!("Failed to get output config: {e}"))?;
|
||||
|
||||
preflight_ffmpeg_only()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PipelineMode {
|
||||
WithOutput,
|
||||
Headless,
|
||||
}
|
||||
|
||||
struct CastTapProc {
|
||||
child: std::process::Child,
|
||||
writer_join: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
struct Pipeline {
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
volume_bits: Arc<AtomicU32>,
|
||||
_stream: cpal::Stream,
|
||||
_stream: Option<cpal::Stream>,
|
||||
decoder_join: Option<std::thread::JoinHandle<()>>,
|
||||
cast_tx: Arc<Mutex<Option<mpsc::SyncSender<Vec<u8>>>>>,
|
||||
cast_proc: Option<CastTapProc>,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
fn start(shared: &'static PlayerShared, url: String) -> Result<Self, String> {
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| "No default audio output device".to_string())?;
|
||||
|
||||
let default_cfg = device
|
||||
.default_output_config()
|
||||
.map_err(|e| format!("Failed to get output config: {e}"))?;
|
||||
let sample_format = default_cfg.sample_format();
|
||||
let cfg = default_cfg.config();
|
||||
let sample_rate = cfg.sample_rate.0;
|
||||
let channels = cfg.channels as u16;
|
||||
fn start(shared: &'static PlayerShared, url: String, mode: PipelineMode) -> Result<Self, String> {
|
||||
let (device, sample_format, cfg, sample_rate, channels) = match mode {
|
||||
PipelineMode::WithOutput => {
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| "No default audio output device".to_string())?;
|
||||
let default_cfg = device
|
||||
.default_output_config()
|
||||
.map_err(|e| format!("Failed to get output config: {e}"))?;
|
||||
let sample_format = default_cfg.sample_format();
|
||||
let cfg = default_cfg.config();
|
||||
let sample_rate = cfg.sample_rate.0;
|
||||
let channels = cfg.channels as u16;
|
||||
(Some(device), Some(sample_format), Some(cfg), sample_rate, channels)
|
||||
}
|
||||
PipelineMode::Headless => {
|
||||
// For cast-only, pick a sane, widely-supported PCM format.
|
||||
// This does not depend on an audio device.
|
||||
(None, None, None, 48_000u32, 2u16)
|
||||
}
|
||||
};
|
||||
|
||||
// 5 seconds of PCM buffering (i16 samples)
|
||||
let capacity_samples = (sample_rate as usize)
|
||||
.saturating_mul(cfg.channels as usize)
|
||||
.saturating_mul(5);
|
||||
let rb = HeapRb::<i16>::new(capacity_samples);
|
||||
let (mut prod, mut cons) = rb.split();
|
||||
let (mut prod_opt, mut cons_opt) = if mode == PipelineMode::WithOutput {
|
||||
let cfg = cfg.as_ref().expect("cfg must exist for WithOutput");
|
||||
let capacity_samples = (sample_rate as usize)
|
||||
.saturating_mul(cfg.channels as usize)
|
||||
.saturating_mul(5);
|
||||
let rb = HeapRb::<i16>::new(capacity_samples);
|
||||
let (prod, cons) = rb.split();
|
||||
(Some(prod), Some(cons))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let volume_bits = Arc::new(AtomicU32::new({
|
||||
@@ -204,15 +261,18 @@ impl Pipeline {
|
||||
volume_to_bits(s.volume)
|
||||
}));
|
||||
|
||||
let cast_tx: Arc<Mutex<Option<mpsc::SyncSender<Vec<u8>>>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
// Decoder thread: spawns ffmpeg, reads PCM, writes into ring buffer.
|
||||
let stop_for_decoder = Arc::clone(&stop_flag);
|
||||
let shared_for_decoder = shared;
|
||||
let decoder_url = url.clone();
|
||||
let cast_tx_for_decoder = Arc::clone(&cast_tx);
|
||||
let decoder_join = std::thread::spawn(move || {
|
||||
let mut backoff_ms: u64 = 250;
|
||||
let mut pushed_since_start: usize = 0;
|
||||
let playing_threshold_samples = (sample_rate as usize)
|
||||
.saturating_mul(cfg.channels as usize)
|
||||
.saturating_mul(channels as usize)
|
||||
.saturating_div(4); // ~250ms
|
||||
|
||||
'outer: loop {
|
||||
@@ -224,7 +284,7 @@ impl Pipeline {
|
||||
|
||||
let ffmpeg = ffmpeg_command();
|
||||
let ffmpeg_disp = ffmpeg.to_string_lossy();
|
||||
let mut child = match Command::new(&ffmpeg)
|
||||
let mut child = match command_hidden(&ffmpeg)
|
||||
.arg("-nostdin")
|
||||
.arg("-hide_banner")
|
||||
.arg("-loglevel")
|
||||
@@ -303,13 +363,21 @@ impl Pipeline {
|
||||
|
||||
backoff_ms = 250;
|
||||
|
||||
// Forward raw PCM bytes to cast tap (if enabled).
|
||||
if let Some(tx) = cast_tx_for_decoder.lock().unwrap().as_ref() {
|
||||
// Best-effort: never block local playback.
|
||||
let _ = tx.try_send(buf[..n].to_vec());
|
||||
}
|
||||
|
||||
// Convert bytes to i16 LE samples
|
||||
let mut i = 0usize;
|
||||
if let Some(b0) = leftover.take() {
|
||||
if n >= 1 {
|
||||
let b1 = buf[0];
|
||||
let sample = i16::from_le_bytes([b0, b1]);
|
||||
let _ = prod.push(sample);
|
||||
if let Some(prod) = prod_opt.as_mut() {
|
||||
let _ = prod.push(sample);
|
||||
}
|
||||
pushed_since_start += 1;
|
||||
i = 1;
|
||||
} else {
|
||||
@@ -319,9 +387,10 @@ impl Pipeline {
|
||||
|
||||
while i + 1 < n {
|
||||
let sample = i16::from_le_bytes([buf[i], buf[i + 1]]);
|
||||
if prod.push(sample).is_ok() {
|
||||
pushed_since_start += 1;
|
||||
if let Some(prod) = prod_opt.as_mut() {
|
||||
let _ = prod.push(sample);
|
||||
}
|
||||
pushed_since_start += 1;
|
||||
i += 2;
|
||||
}
|
||||
|
||||
@@ -337,146 +406,256 @@ impl Pipeline {
|
||||
}
|
||||
});
|
||||
|
||||
// Audio callback: drain ring buffer and write to output.
|
||||
let shared_for_cb = shared;
|
||||
let stop_for_cb = Arc::clone(&stop_flag);
|
||||
let volume_for_cb = Arc::clone(&volume_bits);
|
||||
let stream = if mode == PipelineMode::WithOutput {
|
||||
let device = device.expect("device must exist for WithOutput");
|
||||
let sample_format = sample_format.expect("sample_format must exist for WithOutput");
|
||||
let cfg = cfg.expect("cfg must exist for WithOutput");
|
||||
let mut cons = cons_opt.take().expect("cons must exist for WithOutput");
|
||||
|
||||
let mut last_was_underrun = false;
|
||||
// Audio callback: drain ring buffer and write to output.
|
||||
let shared_for_cb = shared;
|
||||
let stop_for_cb = Arc::clone(&stop_flag);
|
||||
let volume_for_cb = Arc::clone(&volume_bits);
|
||||
|
||||
let err_fn = move |err| {
|
||||
let msg = format!("Audio output error: {err}");
|
||||
set_error(shared_for_cb, msg);
|
||||
let mut last_was_underrun = false;
|
||||
|
||||
let err_fn = move |err| {
|
||||
let msg = format!("Audio output error: {err}");
|
||||
set_error(shared_for_cb, msg);
|
||||
};
|
||||
|
||||
let built = match sample_format {
|
||||
cpal::SampleFormat::F32 => device.build_output_stream(
|
||||
&cfg,
|
||||
move |data: &mut [f32], _| {
|
||||
if stop_for_cb.load(Ordering::Relaxed) {
|
||||
for s in data.iter_mut() {
|
||||
*s = 0.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let vol = volume_from_bits(volume_for_cb.load(Ordering::Relaxed));
|
||||
let mut underrun = false;
|
||||
for s in data.iter_mut() {
|
||||
if let Some(v) = cons.pop() {
|
||||
*s = (v as f32 / 32768.0) * vol;
|
||||
} else {
|
||||
*s = 0.0;
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
if underrun != last_was_underrun {
|
||||
last_was_underrun = underrun;
|
||||
set_status(
|
||||
shared_for_cb,
|
||||
if underrun {
|
||||
PlayerStatus::Buffering
|
||||
} else {
|
||||
PlayerStatus::Playing
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
),
|
||||
cpal::SampleFormat::I16 => device.build_output_stream(
|
||||
&cfg,
|
||||
move |data: &mut [i16], _| {
|
||||
if stop_for_cb.load(Ordering::Relaxed) {
|
||||
for s in data.iter_mut() {
|
||||
*s = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let vol = volume_from_bits(volume_for_cb.load(Ordering::Relaxed));
|
||||
let mut underrun = false;
|
||||
for s in data.iter_mut() {
|
||||
if let Some(v) = cons.pop() {
|
||||
let scaled =
|
||||
(v as f32 * vol).clamp(i16::MIN as f32, i16::MAX as f32);
|
||||
*s = scaled as i16;
|
||||
} else {
|
||||
*s = 0;
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
if underrun != last_was_underrun {
|
||||
last_was_underrun = underrun;
|
||||
set_status(
|
||||
shared_for_cb,
|
||||
if underrun {
|
||||
PlayerStatus::Buffering
|
||||
} else {
|
||||
PlayerStatus::Playing
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
),
|
||||
cpal::SampleFormat::U16 => device.build_output_stream(
|
||||
&cfg,
|
||||
move |data: &mut [u16], _| {
|
||||
if stop_for_cb.load(Ordering::Relaxed) {
|
||||
for s in data.iter_mut() {
|
||||
*s = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let vol = volume_from_bits(volume_for_cb.load(Ordering::Relaxed));
|
||||
let mut underrun = false;
|
||||
for s in data.iter_mut() {
|
||||
if let Some(v) = cons.pop() {
|
||||
// Convert signed i16 to unsigned with bias.
|
||||
let f = (v as f32 / 32768.0) * vol;
|
||||
let scaled = (f * 32767.0 + 32768.0).clamp(0.0, 65535.0);
|
||||
*s = scaled as u16;
|
||||
} else {
|
||||
*s = 0;
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
if underrun != last_was_underrun {
|
||||
last_was_underrun = underrun;
|
||||
set_status(
|
||||
shared_for_cb,
|
||||
if underrun {
|
||||
PlayerStatus::Buffering
|
||||
} else {
|
||||
PlayerStatus::Playing
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
),
|
||||
_ => return Err("Unsupported output sample format".to_string()),
|
||||
}
|
||||
.map_err(|e| format!("Failed to create output stream: {e}"))?;
|
||||
|
||||
built
|
||||
.play()
|
||||
.map_err(|e| format!("Failed to start output stream: {e}"))?;
|
||||
Some(built)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let stream = match sample_format {
|
||||
cpal::SampleFormat::F32 => device.build_output_stream(
|
||||
&cfg,
|
||||
move |data: &mut [f32], _| {
|
||||
if stop_for_cb.load(Ordering::Relaxed) {
|
||||
for s in data.iter_mut() {
|
||||
*s = 0.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let vol = volume_from_bits(volume_for_cb.load(Ordering::Relaxed));
|
||||
let mut underrun = false;
|
||||
for s in data.iter_mut() {
|
||||
if let Some(v) = cons.pop() {
|
||||
*s = (v as f32 / 32768.0) * vol;
|
||||
} else {
|
||||
*s = 0.0;
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
if underrun != last_was_underrun {
|
||||
last_was_underrun = underrun;
|
||||
set_status(
|
||||
shared_for_cb,
|
||||
if underrun {
|
||||
PlayerStatus::Buffering
|
||||
} else {
|
||||
PlayerStatus::Playing
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
),
|
||||
cpal::SampleFormat::I16 => device.build_output_stream(
|
||||
&cfg,
|
||||
move |data: &mut [i16], _| {
|
||||
if stop_for_cb.load(Ordering::Relaxed) {
|
||||
for s in data.iter_mut() {
|
||||
*s = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let vol = volume_from_bits(volume_for_cb.load(Ordering::Relaxed));
|
||||
let mut underrun = false;
|
||||
for s in data.iter_mut() {
|
||||
if let Some(v) = cons.pop() {
|
||||
let scaled = (v as f32 * vol).clamp(i16::MIN as f32, i16::MAX as f32);
|
||||
*s = scaled as i16;
|
||||
} else {
|
||||
*s = 0;
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
if underrun != last_was_underrun {
|
||||
last_was_underrun = underrun;
|
||||
set_status(
|
||||
shared_for_cb,
|
||||
if underrun {
|
||||
PlayerStatus::Buffering
|
||||
} else {
|
||||
PlayerStatus::Playing
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
),
|
||||
cpal::SampleFormat::U16 => device.build_output_stream(
|
||||
&cfg,
|
||||
move |data: &mut [u16], _| {
|
||||
if stop_for_cb.load(Ordering::Relaxed) {
|
||||
for s in data.iter_mut() {
|
||||
*s = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let vol = volume_from_bits(volume_for_cb.load(Ordering::Relaxed));
|
||||
let mut underrun = false;
|
||||
for s in data.iter_mut() {
|
||||
if let Some(v) = cons.pop() {
|
||||
// Convert signed i16 to unsigned with bias.
|
||||
let f = (v as f32 / 32768.0) * vol;
|
||||
let scaled = (f * 32767.0 + 32768.0).clamp(0.0, 65535.0);
|
||||
*s = scaled as u16;
|
||||
} else {
|
||||
*s = 0;
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
if underrun != last_was_underrun {
|
||||
last_was_underrun = underrun;
|
||||
set_status(
|
||||
shared_for_cb,
|
||||
if underrun {
|
||||
PlayerStatus::Buffering
|
||||
} else {
|
||||
PlayerStatus::Playing
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
),
|
||||
_ => return Err("Unsupported output sample format".to_string()),
|
||||
}
|
||||
.map_err(|e| format!("Failed to create output stream: {e}"))?;
|
||||
|
||||
stream
|
||||
.play()
|
||||
.map_err(|e| format!("Failed to start output stream: {e}"))?;
|
||||
|
||||
Ok(Self {
|
||||
stop_flag,
|
||||
volume_bits,
|
||||
_stream: stream,
|
||||
decoder_join: Some(decoder_join),
|
||||
cast_tx,
|
||||
cast_proc: None,
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
|
||||
fn start_cast_tap(&mut self, port: u16, sample_rate: u32, channels: u16) -> Result<(), String> {
|
||||
// Stop existing tap first.
|
||||
self.stop_cast_tap();
|
||||
|
||||
let ffmpeg = ffmpeg_command();
|
||||
let ffmpeg_disp = ffmpeg.to_string_lossy();
|
||||
|
||||
let spawn = |codec: &str| -> Result<std::process::Child, String> {
|
||||
command_hidden(&ffmpeg)
|
||||
.arg("-nostdin")
|
||||
.arg("-hide_banner")
|
||||
.arg("-loglevel")
|
||||
.arg("warning")
|
||||
.arg("-f")
|
||||
.arg("s16le")
|
||||
.arg("-ac")
|
||||
.arg(channels.to_string())
|
||||
.arg("-ar")
|
||||
.arg(sample_rate.to_string())
|
||||
.arg("-i")
|
||||
.arg("pipe:0")
|
||||
.arg("-vn")
|
||||
.arg("-c:a")
|
||||
.arg(codec)
|
||||
.arg("-b:a")
|
||||
.arg("128k")
|
||||
.arg("-f")
|
||||
.arg("mp3")
|
||||
.arg("-content_type")
|
||||
.arg("audio/mpeg")
|
||||
.arg("-listen")
|
||||
.arg("1")
|
||||
.arg(format!("http://0.0.0.0:{port}/stream.mp3"))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to start ffmpeg cast tap ({ffmpeg_disp}): {e}. Set RADIOPLAYER_FFMPEG, bundle ffmpeg next to the app, or install ffmpeg on PATH."
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
let mut child = spawn("libmp3lame")?;
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
if !status.success() {
|
||||
// Some builds lack libmp3lame; fall back to built-in encoder.
|
||||
child = spawn("mp3")?;
|
||||
}
|
||||
}
|
||||
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| "ffmpeg cast tap stdin not available".to_string())?;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(256);
|
||||
*self.cast_tx.lock().unwrap() = Some(tx);
|
||||
|
||||
let writer_join = std::thread::spawn(move || {
|
||||
use std::io::Write;
|
||||
let mut stdin = stdin;
|
||||
while let Ok(chunk) = rx.recv() {
|
||||
if chunk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if stdin.write_all(&chunk).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = stdin.flush();
|
||||
});
|
||||
|
||||
self.cast_proc = Some(CastTapProc {
|
||||
child,
|
||||
writer_join: Some(writer_join),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_cast_tap(&mut self) {
|
||||
*self.cast_tx.lock().unwrap() = None;
|
||||
if let Some(mut proc) = self.cast_proc.take() {
|
||||
let _ = proc.child.kill();
|
||||
let _ = proc.child.wait();
|
||||
if let Some(j) = proc.writer_join.take() {
|
||||
let _ = j.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(mut self, shared: &'static PlayerShared) {
|
||||
self.stop_flag.store(true, Ordering::SeqCst);
|
||||
self.stop_cast_tap();
|
||||
// dropping stream stops audio
|
||||
if let Some(j) = self.decoder_join.take() {
|
||||
let _ = j.join();
|
||||
@@ -492,6 +671,7 @@ impl Pipeline {
|
||||
fn player_thread(shared: &'static PlayerShared, rx: mpsc::Receiver<PlayerCommand>) {
|
||||
// Step 2: FFmpeg decode + CPAL playback.
|
||||
let mut pipeline: Option<Pipeline> = None;
|
||||
let mut pipeline_cast_owned = false;
|
||||
while let Ok(cmd) = rx.recv() {
|
||||
match cmd {
|
||||
PlayerCommand::Play { url } => {
|
||||
@@ -499,6 +679,8 @@ fn player_thread(shared: &'static PlayerShared, rx: mpsc::Receiver<PlayerCommand
|
||||
p.stop(shared);
|
||||
}
|
||||
|
||||
pipeline_cast_owned = false;
|
||||
|
||||
{
|
||||
let mut s = shared.state.lock().unwrap();
|
||||
s.error = None;
|
||||
@@ -506,7 +688,7 @@ fn player_thread(shared: &'static PlayerShared, rx: mpsc::Receiver<PlayerCommand
|
||||
s.status = PlayerStatus::Buffering;
|
||||
}
|
||||
|
||||
match Pipeline::start(shared, url) {
|
||||
match Pipeline::start(shared, url, PipelineMode::WithOutput) {
|
||||
Ok(p) => {
|
||||
// Apply current volume to pipeline atomics.
|
||||
let vol = { shared.state.lock().unwrap().volume };
|
||||
@@ -519,6 +701,32 @@ fn player_thread(shared: &'static PlayerShared, rx: mpsc::Receiver<PlayerCommand
|
||||
}
|
||||
}
|
||||
}
|
||||
PlayerCommand::PlayCast { url } => {
|
||||
if let Some(p) = pipeline.take() {
|
||||
p.stop(shared);
|
||||
}
|
||||
|
||||
pipeline_cast_owned = true;
|
||||
|
||||
{
|
||||
let mut s = shared.state.lock().unwrap();
|
||||
s.error = None;
|
||||
s.url = Some(url.clone());
|
||||
s.status = PlayerStatus::Buffering;
|
||||
}
|
||||
|
||||
match Pipeline::start(shared, url, PipelineMode::Headless) {
|
||||
Ok(p) => {
|
||||
let vol = { shared.state.lock().unwrap().volume };
|
||||
p.set_volume(vol);
|
||||
pipeline = Some(p);
|
||||
}
|
||||
Err(e) => {
|
||||
set_error(shared, e);
|
||||
pipeline = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
PlayerCommand::Stop => {
|
||||
if let Some(p) = pipeline.take() {
|
||||
p.stop(shared);
|
||||
@@ -527,6 +735,7 @@ fn player_thread(shared: &'static PlayerShared, rx: mpsc::Receiver<PlayerCommand
|
||||
s.status = PlayerStatus::Stopped;
|
||||
s.error = None;
|
||||
}
|
||||
pipeline_cast_owned = false;
|
||||
}
|
||||
PlayerCommand::SetVolume { volume } => {
|
||||
let v = clamp01(volume);
|
||||
@@ -538,6 +747,26 @@ fn player_thread(shared: &'static PlayerShared, rx: mpsc::Receiver<PlayerCommand
|
||||
p.set_volume(v);
|
||||
}
|
||||
}
|
||||
PlayerCommand::CastTapStart { port, reply } => {
|
||||
if let Some(p) = pipeline.as_mut() {
|
||||
// Current pipeline sample format is always s16le.
|
||||
let res = p.start_cast_tap(port, p.sample_rate, p.channels);
|
||||
let _ = reply.send(res);
|
||||
} else {
|
||||
let _ = reply.send(Err("No active decoder pipeline".to_string()));
|
||||
}
|
||||
}
|
||||
PlayerCommand::CastTapStop => {
|
||||
if let Some(p) = pipeline.as_mut() {
|
||||
p.stop_cast_tap();
|
||||
}
|
||||
if pipeline_cast_owned {
|
||||
if let Some(p) = pipeline.take() {
|
||||
p.stop(shared);
|
||||
}
|
||||
pipeline_cast_owned = false;
|
||||
}
|
||||
}
|
||||
PlayerCommand::Shutdown => break,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user