mirror of
https://github.com/jhbruhn/jellyfin-radio.git
synced 2025-03-14 19:45:50 +00:00
Add crappy support for timed interstitials
This commit is contained in:
parent
aa3518807a
commit
54c987e784
6 changed files with 331 additions and 24 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
interstitials
|
136
Cargo.lock
generated
136
Cargo.lock
generated
|
@ -17,6 +17,21 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.81"
|
version = "1.0.81"
|
||||||
|
@ -64,6 +79,7 @@ checksum = "c1e49f94356350dde8c54c5cbde90bdfa5642ab25f9c51c60a9ebdfdf3dbef87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -129,6 +145,20 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets 0.52.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
@ -160,6 +190,12 @@ version = "0.8.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.33"
|
version = "0.8.33"
|
||||||
|
@ -338,6 +374,17 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
|
@ -515,6 +562,29 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -541,6 +611,15 @@ version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
|
@ -555,13 +634,16 @@ dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"awedio",
|
"awedio",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"envconfig",
|
"envconfig",
|
||||||
"future-bool",
|
"future-bool",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.2.0",
|
"hyper 1.2.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
|
"itertools",
|
||||||
"mp3lame-encoder",
|
"mp3lame-encoder",
|
||||||
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
@ -682,6 +764,15 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -814,6 +905,12 @@ version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
|
@ -832,6 +929,36 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -1542,6 +1669,15 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
|
|
@ -8,10 +8,10 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.11", features = ["json", "stream", "native-tls-vendored"] }
|
reqwest = { version = "0.11", features = ["json", "stream", "native-tls-vendored"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
awedio = { version = "0.3", features = ["symphonia-all"], default-features = false }
|
awedio = { version = "0.3", features = ["symphonia-all", "async"], default-features = false }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
mp3lame-encoder = { git = "https://github.com/jhbruhn/mp3lame-encoder.git", branch = "arm" }
|
mp3lame-encoder = { git = "https://github.com/jhbruhn/mp3lame-encoder.git", branch = "arm" }
|
||||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
tokio-stream = { version = "0.1", features = ["sync", "fs"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
hyper = { version = "1.2", features = ["server", "http1"] }
|
hyper = { version = "1.2", features = ["server", "http1"] }
|
||||||
hyper-util = {version = "0.1", features = ["tokio"] }
|
hyper-util = {version = "0.1", features = ["tokio"] }
|
||||||
|
@ -22,3 +22,6 @@ future-bool = "0.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
symphonia = { version = "0.5.4", features = ["all"] }
|
symphonia = { version = "0.5.4", features = ["all"] }
|
||||||
envconfig = "0.10"
|
envconfig = "0.10"
|
||||||
|
chrono = "0.4.35"
|
||||||
|
itertools = "0.12.1"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
149
src/main.rs
149
src/main.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use awedio::Sound;
|
||||||
use envconfig::Envconfig;
|
use envconfig::Envconfig;
|
||||||
use hyper::server::conn::http1;
|
use hyper::server::conn::http1;
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
|
@ -29,6 +30,51 @@ struct Config {
|
||||||
pub song_prefetch: u32,
|
pub song_prefetch: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_time_file_map(
|
||||||
|
folder: &std::path::Path,
|
||||||
|
) -> std::collections::HashMap<chrono::NaiveTime, Vec<std::path::PathBuf>> {
|
||||||
|
// time announce logic
|
||||||
|
let time_files: Vec<std::path::PathBuf> = std::fs::read_dir(folder)
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|v| v.ok())
|
||||||
|
.filter(|v| !v.path().is_dir())
|
||||||
|
.map(|v| v.path().clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut time_map = std::collections::HashMap::new();
|
||||||
|
for path in time_files.iter() {
|
||||||
|
if let Ok((time, path)) = async {
|
||||||
|
let mut name_split = path
|
||||||
|
.file_stem()
|
||||||
|
.ok_or(anyhow::anyhow!("Wrong file stem!"))?
|
||||||
|
.to_str()
|
||||||
|
.ok_or(anyhow::anyhow!("Wrong file path!"))?
|
||||||
|
.split("_");
|
||||||
|
let hour = name_split
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No hour!"))?
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let minute = name_split
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No minute!"))?
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let time = chrono::NaiveTime::from_hms_opt(hour, minute, 0)
|
||||||
|
.ok_or(anyhow::anyhow!("Can't parse time"))?;
|
||||||
|
Ok::<_, anyhow::Error>((time, path))
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
time_map
|
||||||
|
.entry(time)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time_map
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let config = Config::init_from_env().unwrap();
|
let config = Config::init_from_env().unwrap();
|
||||||
|
@ -58,12 +104,19 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let (streamer_backend, mut streamer_manager) = streamer::StreamerBackend::start()?;
|
let (streamer_backend, mut streamer_manager) = streamer::StreamerBackend::start()?;
|
||||||
|
|
||||||
|
let (mixer, mixer_controller) = awedio::sounds::SoundMixer::new(2, 48_000).controllable();
|
||||||
|
// basic playlist playback
|
||||||
|
|
||||||
|
let (player, mut player_controller) = player::Player::new(config.song_prefetch);
|
||||||
|
let player = Box::new(player);
|
||||||
|
|
||||||
|
let mut player_mixer_controller = mixer_controller.clone();
|
||||||
|
player_mixer_controller.add(player);
|
||||||
|
let mut announce_downmix_player_controller = player_controller.clone();
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let (player, mut controller) = player::Player::new(config.song_prefetch);
|
|
||||||
let player = Box::new(player);
|
|
||||||
streamer_manager.play(player);
|
|
||||||
loop {
|
loop {
|
||||||
controller.wait_for_queue().await;
|
tokio::task::yield_now().await;
|
||||||
|
player_controller.wait_for_queue().await;
|
||||||
|
|
||||||
println!("Queuing song");
|
println!("Queuing song");
|
||||||
|
|
||||||
|
@ -77,9 +130,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let sound = client.fetch_audio(item).await?;
|
let sound = client.fetch_audio(item).await?;
|
||||||
println!("Fetched Song!");
|
println!("Fetched Song!");
|
||||||
if sound.channel_count() > 2 {
|
if sound.channel_count() > 2 {
|
||||||
anyhow::bail!("Too many channels");
|
anyhow::bail!("Too many channels, skipping!");
|
||||||
}
|
}
|
||||||
controller.add(Box::new(sound));
|
player_controller.add(Box::new(sound));
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
@ -93,6 +146,88 @@ async fn main() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut time_announce_mixer_controller = mixer_controller.clone();
|
||||||
|
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let time_file_map =
|
||||||
|
get_time_file_map(&std::path::PathBuf::from("./interstitials/time")).await;
|
||||||
|
loop {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
|
||||||
|
let fade_duration = Duration::from_secs(2);
|
||||||
|
let fade_steps = 100;
|
||||||
|
let fade_minimum_level = 0.1;
|
||||||
|
|
||||||
|
let fade_steps_max = fade_steps;
|
||||||
|
let fade_steps_min = (fade_minimum_level * fade_steps as f32) as u32;
|
||||||
|
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
use itertools::Itertools;
|
||||||
|
let next_time = if let Some(next_time) = time_file_map
|
||||||
|
.keys()
|
||||||
|
.sorted()
|
||||||
|
.filter(|k| **k > now.time())
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
Some(next_time)
|
||||||
|
} else {
|
||||||
|
time_file_map.keys().sorted().next()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(next_time) = next_time {
|
||||||
|
let paths = time_file_map.get(next_time).unwrap(); // definitely exists, we just did the math
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
let next_path = paths.choose(&mut rand::thread_rng());
|
||||||
|
if next_path.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let next_path = next_path.unwrap();
|
||||||
|
|
||||||
|
let interstitial_time = if *next_time > now.time() {
|
||||||
|
now.date_naive()
|
||||||
|
.and_time(*next_time)
|
||||||
|
.and_local_timezone(chrono::Local)
|
||||||
|
} else {
|
||||||
|
now.date_naive()
|
||||||
|
.checked_add_days(chrono::Days::new(1))
|
||||||
|
.unwrap()
|
||||||
|
.and_time(*next_time)
|
||||||
|
.and_local_timezone(chrono::Local)
|
||||||
|
}
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Next Internstitial time {}", interstitial_time);
|
||||||
|
|
||||||
|
tokio::time::sleep_until(
|
||||||
|
tokio::time::Instant::now() + (interstitial_time - now).to_std().unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
println!("Internstitial time {}", interstitial_time);
|
||||||
|
|
||||||
|
for v in (fade_steps_min..=fade_steps_max).rev() {
|
||||||
|
let volume = v as f32 / fade_steps as f32;
|
||||||
|
announce_downmix_player_controller.set_volume(volume);
|
||||||
|
tokio::time::sleep(fade_duration / (fade_steps_max - fade_steps_min)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (sound, completion_notifier) = awedio::sounds::open_file(next_path.as_path())
|
||||||
|
.unwrap()
|
||||||
|
.with_async_completion_notifier();
|
||||||
|
time_announce_mixer_controller.add(Box::new(sound));
|
||||||
|
let _ = completion_notifier.await;
|
||||||
|
|
||||||
|
for v in fade_steps_min..=fade_steps_max {
|
||||||
|
let volume = v as f32 / fade_steps as f32;
|
||||||
|
announce_downmix_player_controller.set_volume(volume);
|
||||||
|
tokio::time::sleep(fade_duration / (fade_steps_max - fade_steps_min)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
streamer_manager.play(Box::new(mixer));
|
||||||
|
|
||||||
let listener = TcpListener::bind(addr).await?;
|
let listener = TcpListener::bind(addr).await?;
|
||||||
println!("Listening on http://{}", addr);
|
println!("Listening on http://{}", addr);
|
||||||
loop {
|
loop {
|
||||||
|
@ -100,6 +235,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let io = TokioIo::new(tcp);
|
let io = TokioIo::new(tcp);
|
||||||
let backend = streamer_backend.clone();
|
let backend = streamer_backend.clone();
|
||||||
|
|
||||||
|
println!("New connection!");
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
if let Err(err) = http1::Builder::new().serve_connection(io, backend).await {
|
if let Err(err) = http1::Builder::new().serve_connection(io, backend).await {
|
||||||
println!("Error serving connection: {:?}", err);
|
println!("Error serving connection: {:?}", err);
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub struct Player {
|
||||||
sounds: Vec<Box<dyn Sound>>,
|
sounds: Vec<Box<dyn Sound>>,
|
||||||
was_empty: bool,
|
was_empty: bool,
|
||||||
song_prefetch: u32,
|
song_prefetch: u32,
|
||||||
|
volume_adjustment: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Command<S> = Box<dyn FnOnce(&mut S) + Send>;
|
type Command<S> = Box<dyn FnOnce(&mut S) + Send>;
|
||||||
|
@ -15,23 +16,24 @@ type Command<S> = Box<dyn FnOnce(&mut S) + Send>;
|
||||||
pub struct PlayerControllable {
|
pub struct PlayerControllable {
|
||||||
inner: Player,
|
inner: Player,
|
||||||
command_receiver: mpsc::Receiver<Command<Player>>,
|
command_receiver: mpsc::Receiver<Command<Player>>,
|
||||||
queue_next_song_sender: tokio::sync::mpsc::Sender<()>,
|
queue_next_song_sender: tokio::sync::watch::Sender<bool>,
|
||||||
finished: bool,
|
finished: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PlayerController {
|
pub struct PlayerController {
|
||||||
command_sender: mpsc::Sender<Command<Player>>,
|
command_sender: mpsc::Sender<Command<Player>>,
|
||||||
queue_next_song_receiver: tokio::sync::mpsc::Receiver<()>,
|
queue_next_song_receiver: tokio::sync::watch::Receiver<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
/// Create a new empty Player.
|
/// Create a new empty Player.
|
||||||
pub fn new(song_prefetch: u32) -> (PlayerControllable, PlayerController) {
|
pub fn new(song_prefetch: u32) -> (PlayerControllable, PlayerController) {
|
||||||
let (queue_next_song_sender, queue_next_song_receiver) = tokio::sync::mpsc::channel(1);
|
let (queue_next_song_sender, queue_next_song_receiver) = tokio::sync::watch::channel(false);
|
||||||
let inner = Player {
|
let inner = Player {
|
||||||
sounds: Vec::new(),
|
sounds: Vec::new(),
|
||||||
was_empty: false,
|
was_empty: false,
|
||||||
song_prefetch,
|
song_prefetch,
|
||||||
|
volume_adjustment: 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (command_sender, command_receiver) = mpsc::channel::<Command<Player>>();
|
let (command_sender, command_receiver) = mpsc::channel::<Command<Player>>();
|
||||||
|
@ -57,8 +59,12 @@ impl Player {
|
||||||
self.sounds.push(sound);
|
self.sounds.push(sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_song_playing_or_empty(&self) -> bool {
|
fn set_volume(&mut self, new: f32) {
|
||||||
self.sounds.len() <= self.song_prefetch as usize
|
self.volume_adjustment = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_prefetch(&self) -> bool {
|
||||||
|
self.sounds.len() < self.song_prefetch as usize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,8 +108,12 @@ impl Sound for Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = match next_sample {
|
let ret = match next_sample {
|
||||||
Ok(NextSample::Sample(_) | NextSample::MetadataChanged | NextSample::Paused) => next_sample.unwrap(),
|
Ok(NextSample::Sample(s)) => {
|
||||||
Ok(NextSample::Finished) | Err(_) => { // Just ignore the error
|
NextSample::Sample((s as f32 * self.volume_adjustment) as i16)
|
||||||
|
}
|
||||||
|
Ok(NextSample::MetadataChanged | NextSample::Paused) => next_sample.unwrap(),
|
||||||
|
Ok(NextSample::Finished) | Err(_) => {
|
||||||
|
// Just ignore the error
|
||||||
self.sounds.remove(0);
|
self.sounds.remove(0);
|
||||||
if self.sounds.is_empty() {
|
if self.sounds.is_empty() {
|
||||||
NextSample::Finished
|
NextSample::Finished
|
||||||
|
@ -159,9 +169,15 @@ impl Sound for PlayerControllable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.inner.last_song_playing_or_empty() {
|
|
||||||
let _ = self.queue_next_song_sender.try_send(());
|
let _ = self.queue_next_song_sender.send_if_modified(|v| {
|
||||||
}
|
if *v != self.inner.should_prefetch() {
|
||||||
|
*v = self.inner.should_prefetch();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
});
|
||||||
|
|
||||||
self.inner.on_start_of_batch();
|
self.inner.on_start_of_batch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,12 +191,25 @@ impl PlayerController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for PlayerController {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
command_sender: self.command_sender.clone(),
|
||||||
|
queue_next_song_receiver: self.queue_next_song_receiver.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PlayerController {
|
impl PlayerController {
|
||||||
pub fn add(&mut self, sound: Box<dyn Sound>) {
|
pub fn add(&mut self, sound: Box<dyn Sound>) {
|
||||||
self.send_command(Box::new(|s: &mut Player| s.add(sound)));
|
self.send_command(Box::new(|s: &mut Player| s.add(sound)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_volume(&mut self, new: f32) {
|
||||||
|
self.send_command(Box::new(move |s: &mut Player| s.set_volume(new)));
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn wait_for_queue(&mut self) {
|
pub async fn wait_for_queue(&mut self) {
|
||||||
self.queue_next_song_receiver.recv().await;
|
let _ = self.queue_next_song_receiver.wait_for(|v| *v == true).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,12 @@ use hyper::service::Service;
|
||||||
use hyper::{body, Request};
|
use hyper::{body, Request};
|
||||||
use hyper::{Response, StatusCode};
|
use hyper::{Response, StatusCode};
|
||||||
|
|
||||||
const SEGMENT_INTERVAL: u64 = 2; // seconds
|
|
||||||
const SAMPLE_RATE: u64 = 48000;
|
const SAMPLE_RATE: u64 = 48000;
|
||||||
const CHANNEL_COUNT: u64 = 2;
|
const CHANNEL_COUNT: u64 = 2;
|
||||||
|
|
||||||
type Chunk = [i16; (SEGMENT_INTERVAL * SAMPLE_RATE * CHANNEL_COUNT) as usize];
|
const BUFFER_SIZE: usize = 2000; // Should be an integer result of 48000 / 2 / x
|
||||||
|
|
||||||
|
type Chunk = [i16; BUFFER_SIZE];
|
||||||
|
|
||||||
pub struct StreamerBackend {
|
pub struct StreamerBackend {
|
||||||
stream_receiver: Receiver<Box<Chunk>>,
|
stream_receiver: Receiver<Box<Chunk>>,
|
||||||
|
@ -35,10 +36,10 @@ impl StreamerBackend {
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut stream = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
let mut stream = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
||||||
Duration::from_secs(SEGMENT_INTERVAL),
|
Duration::from_millis(1000 * BUFFER_SIZE as u64 / CHANNEL_COUNT / SAMPLE_RATE),
|
||||||
))
|
))
|
||||||
.map(move |_| {
|
.map(move |_| {
|
||||||
let mut buffer = [0_i16; (SAMPLE_RATE * SEGMENT_INTERVAL * CHANNEL_COUNT) as usize];
|
let mut buffer = [0_i16; BUFFER_SIZE];
|
||||||
renderer.on_start_of_batch();
|
renderer.on_start_of_batch();
|
||||||
buffer.fill_with(|| {
|
buffer.fill_with(|| {
|
||||||
let sample = renderer
|
let sample = renderer
|
||||||
|
|
Loading…
Reference in a new issue