From f0a3000a64ebcd6398ad503617d046a720a3165b Mon Sep 17 00:00:00 2001 From: Remi D'Almeida Date: Thu, 9 Apr 2026 16:29:58 +0300 Subject: [PATCH 01/12] chore: upgrade Rodio to 0.22.2 and implement HTTP streaming support Replace direct Symphonia dependency with Rodio's built-in codec features. Implement HTTP streaming via custom `HttpAudioReader` to avoid downloading entire files. Switch from `Sink` to `Player` API and `MixerDeviceSink` for audio output. Remove audio output thread in favor of direct device sink. Add source descriptor abstraction to support both local files and remote URLs with metadata probing. Improve error handling for seek operations and source reopening. --- Cargo.lock | 387 ++++++----------- crates/audio-player/Cargo.toml | 3 +- crates/audio-player/src/player.rs | 556 +++++++++++++++++------- crates/audio-player/src/transitions.rs | 13 +- examples/tauri-app/src-tauri/Cargo.lock | 387 ++++++----------- 5 files changed, 682 insertions(+), 664 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27e1a34..89da95d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", "bitflags 2.11.0", @@ -46,9 +46,9 @@ dependencies = [ [[package]] name = "alsa-sys" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" dependencies = [ "libc", "pkg-config", @@ -110,7 +110,6 @@ version = "0.1.0" dependencies = [ "rodio", "serde", - "symphonia", "thiserror 2.0.18", "tracing", "ureq", @@ -134,24 +133,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -322,8 +303,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -333,15 +312,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfb" version = "0.7.3" @@ -381,23 +351,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.9", -] - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - [[package]] name = "combine" version = "4.6.7" @@ -466,45 +419,46 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586" dependencies = [ - "bindgen", + "bitflags 2.11.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ "alsa", - "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jni", "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.54.0", + "windows", ] [[package]] @@ -812,12 +766,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "embed-resource" version = "3.0.7" @@ -1398,12 +1346,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "html5ever" version = "0.29.1" @@ -1705,15 +1647,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.17" @@ -1765,16 +1698,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.91" @@ -1842,17 +1765,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libappindicator" version = "0.9.0" @@ -1873,7 +1785,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading 0.7.4", + "libloading", "once_cell", ] @@ -1893,16 +1805,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - [[package]] name = "libredox" version = "0.1.14" @@ -1941,9 +1843,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mach2" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" dependencies = [ "libc", ] @@ -2011,12 +1913,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2059,20 +1955,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.11.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2082,7 +1964,7 @@ dependencies = [ "bitflags 2.11.0", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -2094,15 +1976,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2125,13 +1998,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] -name = "nom" -version = "7.1.3" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "memchr", - "minimal-lexical", + "num-integer", + "num-traits", ] [[package]] @@ -2151,6 +2024,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2205,6 +2098,54 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -2212,7 +2153,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -2252,6 +2195,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2305,38 +2249,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -2978,16 +2890,15 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb" dependencies = [ - "claxon", "cpal", - "hound", - "lewton", + "dasp_sample", + "num-rational", "symphonia", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -3391,7 +3302,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", "js-sys", - "ndk 0.9.0", + "ndk", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3520,6 +3431,7 @@ dependencies = [ "symphonia-bundle-mp3", "symphonia-codec-aac", "symphonia-codec-pcm", + "symphonia-codec-vorbis", "symphonia-core", "symphonia-format-isomp4", "symphonia-format-ogg", @@ -3572,6 +3484,17 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-core" version = "0.5.5" @@ -3719,9 +3642,9 @@ dependencies = [ "jni", "libc", "log", - "ndk 0.9.0", + "ndk", "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3731,7 +3654,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3802,7 +3725,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.61.3", + "windows", ] [[package]] @@ -3921,7 +3844,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", ] [[package]] @@ -3946,7 +3869,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", "wry", ] @@ -4091,21 +4014,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.50.0" @@ -4764,7 +4672,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -4788,7 +4696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows 0.61.3", + "windows", "windows-core 0.61.2", ] @@ -4838,16 +4746,6 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -4870,16 +4768,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -4961,15 +4849,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -5397,7 +5276,7 @@ dependencies = [ "javascriptcore-rs", "jni", "libc", - "ndk 0.9.0", + "ndk", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -5415,7 +5294,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/crates/audio-player/Cargo.toml b/crates/audio-player/Cargo.toml index 1665b5f..f93b766 100644 --- a/crates/audio-player/Cargo.toml +++ b/crates/audio-player/Cargo.toml @@ -7,8 +7,7 @@ edition = "2024" rust-version = "1.89" [dependencies] -rodio = "0.19.0" -symphonia = { version = "0.5.5", default-features = false, features = ["mp3", "wav", "flac", "ogg", "isomp4", "aac", "pcm"] } +rodio = { version = "0.22.2", default-features = false, features = ["playback", "symphonia-mp3", "symphonia-wav", "symphonia-flac", "symphonia-ogg", "symphonia-vorbis", "symphonia-isomp4", "symphonia-aac", "symphonia-pcm"] } serde = { version = "1.0.228", features = ["derive"] } thiserror = "2.0.17" tracing = { version = "0.1.41", default-features = false, features = ["std", "log"] } diff --git a/crates/audio-player/src/player.rs b/crates/audio-player/src/player.rs index 7ebb46d..7e333a6 100644 --- a/crates/audio-player/src/player.rs +++ b/crates/audio-player/src/player.rs @@ -1,9 +1,12 @@ -use std::io::{Cursor, Read}; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; -use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink, Source}; +use rodio::stream::{DeviceSinkBuilder, MixerDeviceSink}; +use rodio::{Decoder, Player, Sample, Source}; use tracing::warn; use crate::error::{Error, Result}; @@ -11,22 +14,17 @@ use crate::models::{AudioActionResponse, AudioMetadata, PlaybackStatus, PlayerSt use crate::net::reject_private_host; use crate::{OnChanged, OnTimeUpdate, transitions}; -/// Maximum audio download size (100 MiB). -const MAX_DOWNLOAD_BYTES: u64 = 100 * 1024 * 1024; - /// HTTP request timeout (connect + read combined). const HTTP_TIMEOUT: Duration = Duration::from_secs(30); /// Audio player backed by Rodio for cross-platform desktop playback. /// -/// Manages a dedicated audio output thread, a playback monitor for time updates +/// Manages audio output, a playback monitor for time updates /// and end-of-track detection, and a state machine matching the plugin's /// [`PlaybackStatus`] model. pub struct RodioAudioPlayer { inner: Arc>, - stream_handle: OutputStreamHandle, - /// Dropping this sender signals the audio output thread to exit. - _stream_keep_alive: std::sync::mpsc::Sender<()>, + output_sink: MixerDeviceSink, on_changed: OnChanged, on_time_update: OnTimeUpdate, } @@ -38,21 +36,47 @@ struct Inner { } struct PlaybackContext { - sink: Sink, - /// Raw audio bytes kept for looping re-append and replay from Ended. - /// Wrapped in `Arc` so re-append clones are cheap reference count bumps - /// instead of multi-megabyte copies. - source_data: Arc<[u8]>, + sink: Player, + source: SourceDescriptor, duration: f64, } +#[derive(Clone)] +enum SourceDescriptor { + Local { path: PathBuf }, + Remote(RemoteSourceDescriptor), +} + +#[derive(Clone)] +struct RemoteSourceDescriptor { + url: String, + byte_len: Option, + mime_type: Option, + hint: Option, +} + +struct HttpAudioReader { + url: String, + position: u64, + byte_len: Option, + reader: Option, + reached_eof: bool, +} + +struct HttpResponseReader { + inner: Mutex>, +} + +type BoxedSource = Box + Send>; + impl RodioAudioPlayer { /// Creates a new Rodio-backed audio player. /// - /// Opens the default audio output device on a dedicated thread. Returns an error + /// Opens the default audio output device. Returns an error /// if no audio device is available. pub fn new(on_changed: OnChanged, on_time_update: OnTimeUpdate) -> Result { - let stream_handle = open_audio_output()?; + let mut output_sink = open_audio_output()?; + output_sink.log_on_drop(false); Ok(Self { inner: Arc::new(Mutex::new(Inner { @@ -60,8 +84,7 @@ impl RodioAudioPlayer { playback: None, monitor_stop: Arc::new(AtomicBool::new(true)), })), - stream_handle: stream_handle.handle, - _stream_keep_alive: stream_handle.keep_alive, + output_sink, on_changed, on_time_update, }) @@ -136,21 +159,16 @@ impl RodioAudioPlayer { /// Inner load logic that may fail. Separated so `load()` can catch errors /// and transition to the Error state before propagating. fn load_inner(&self, src: &str, meta: &AudioMetadata) -> Result { - // Fetch audio data (may block on file I/O or HTTP download). - let data: Arc<[u8]> = load_source_data(src)?.into(); - - // Decode audio and extract duration. - let source = Decoder::new(Cursor::new(Arc::clone(&data))) - .map_err(|e| Error::Audio(format!("Failed to decode audio: {e}")))?; + let descriptor = load_source_descriptor(src)?; + let source = open_source(&descriptor)?; let duration = source .total_duration() .map(|d| d.as_secs_f64()) - .unwrap_or_else(|| probe_duration(&data).unwrap_or(0.0)); + .unwrap_or(0.0); // Create a new sink, append the decoded source, and pause immediately // so playback waits for an explicit play() call. - let sink = Sink::try_new(&self.stream_handle) - .map_err(|e| Error::Audio(format!("Failed to create audio sink: {e}")))?; + let sink = Player::connect_new(self.output_sink.mixer()); sink.pause(); sink.append(source); @@ -168,7 +186,7 @@ impl RodioAudioPlayer { inner.playback = Some(PlaybackContext { sink, - source_data: data, + source: descriptor, duration, }); @@ -179,20 +197,22 @@ impl RodioAudioPlayer { let snapshot = { let mut inner = lock_inner(&self.inner); let is_ended = inner.state.status == PlaybackStatus::Ended; + let mut replayed_from_start = false; // Re-append source for replay from Ended before the transition // mutates status. if is_ended && let Some(ctx) = &inner.playback && ctx.sink.empty() - && let Some(source) = decode_arc(&ctx.source_data) { + let source = open_source(&ctx.source)?; ctx.sink.append(source); + replayed_from_start = true; } transitions::play(&mut inner.state)?; - if is_ended { + if replayed_from_start { inner.state.current_time = 0.0; } @@ -251,23 +271,36 @@ impl RodioAudioPlayer { let snapshot = { let mut inner = lock_inner(&self.inner); let was_ended = inner.state.status == PlaybackStatus::Ended; + let previous_time = inner.state.current_time; transitions::seek(&mut inner.state, position)?; if let Some(ctx) = &inner.playback { + let mut reopened_source = false; + // If ended, re-append the source so we have something to seek within. - if was_ended { - if let Some(source) = decode_arc(&ctx.source_data) { - ctx.sink.append(source); - } + if was_ended && ctx.sink.empty() { + let source = match open_source(&ctx.source) { + Ok(source) => source, + Err(error) => { + inner.state.current_time = previous_time; + return Err(error); + } + }; + ctx.sink.append(source); ctx.sink.pause(); + reopened_source = true; } if let Err(e) = ctx .sink .try_seek(Duration::from_secs_f64(inner.state.current_time)) { - warn!("Seek failed: {e}"); + if reopened_source { + ctx.sink.stop(); + } + inner.state.current_time = previous_time; + return Err(Error::Audio(format!("Failed to seek audio: {e}"))); } } @@ -334,46 +367,13 @@ impl RodioAudioPlayer { } // --------------------------------------------------------------------------- -// Audio output thread +// Audio output // --------------------------------------------------------------------------- -struct AudioOutput { - handle: OutputStreamHandle, - keep_alive: std::sync::mpsc::Sender<()>, -} - -/// Opens the default audio output device on a dedicated thread. -/// -/// The [`OutputStream`] must remain on the thread that created it (platform -/// requirement on some backends). We keep it alive via a channel — dropping the -/// returned sender signals the thread to exit. -fn open_audio_output() -> Result { - let (result_tx, result_rx) = std::sync::mpsc::sync_channel(1); - let (keep_alive_tx, keep_alive_rx) = std::sync::mpsc::channel::<()>(); - - std::thread::Builder::new() - .name("audio-output".into()) - .spawn(move || match OutputStream::try_default() { - Ok((_stream, handle)) => { - let _ = result_tx.send(Ok(handle)); - // Block until the keep_alive sender is dropped. - let _ = keep_alive_rx.recv(); - } - Err(e) => { - let _ = result_tx.send(Err(e)); - } - }) - .map_err(|e| Error::Audio(format!("Failed to spawn audio thread: {e}")))?; - - let handle = result_rx - .recv() - .map_err(|_| Error::Audio("Audio thread terminated unexpectedly".into()))? - .map_err(|e| Error::Audio(format!("Failed to open audio device: {e}")))?; - - Ok(AudioOutput { - handle, - keep_alive: keep_alive_tx, - }) +/// Opens the default audio output device for playback. +fn open_audio_output() -> Result { + DeviceSinkBuilder::open_default_sink() + .map_err(|e| Error::Audio(format!("Failed to open audio device: {e}"))) } // --------------------------------------------------------------------------- @@ -397,21 +397,21 @@ fn monitor_loop( let mut guard = lock_inner(&inner); let (pos, duration, is_empty) = match &guard.playback { - Some(ctx) => ( - ctx.sink.get_pos().as_secs_f64(), - ctx.duration, - ctx.sink.empty(), - ), + Some(ctx) => { + let pos = ctx.sink.get_pos().as_secs_f64() * guard.state.playback_rate; + (pos, ctx.duration, ctx.sink.empty()) + } None => break, }; if is_empty { if guard.state.looping { // Re-append source for seamless (best-effort) loop. - if let Some(ctx) = &guard.playback - && let Some(source) = decode_arc(&ctx.source_data) - { - ctx.sink.append(source); + if let Some(ctx) = &guard.playback { + match open_source(&ctx.source) { + Ok(source) => ctx.sink.append(source), + Err(e) => warn!("Failed to reopen loop source: {e}"), + } } guard.state.current_time = 0.0; drop(guard); @@ -421,7 +421,7 @@ fn monitor_loop( }); } else { guard.state.status = PlaybackStatus::Ended; - guard.state.current_time = duration; + guard.state.current_time = if duration > 0.0 { duration } else { pos }; let snapshot = guard.state.clone(); drop(guard); on_changed(&snapshot); @@ -451,11 +451,6 @@ fn lock_inner(mutex: &Mutex) -> MutexGuard<'_, Inner> { mutex.lock().unwrap_or_else(|e| e.into_inner()) } -/// Creates a new decoder from shared audio data (cheap Arc clone, no byte copy). -fn decode_arc(data: &Arc<[u8]>) -> Option>>> { - Decoder::new(Cursor::new(Arc::clone(data))).ok() -} - /// Resolves the effective sink volume, accounting for the mute flag. fn effective_volume(state: &PlayerState) -> f32 { if state.muted { @@ -465,79 +460,334 @@ fn effective_volume(state: &PlayerState) -> f32 { } } -/// Probes audio data with symphonia to determine duration from container metadata. -/// -/// This succeeds for most common formats (MP3, FLAC, WAV, OGG, AAC) where -/// `rodio::Decoder::total_duration()` returns `None`. -fn probe_duration(data: &Arc<[u8]>) -> Option { - use symphonia::core::formats::FormatOptions; - use symphonia::core::io::MediaSourceStream; - use symphonia::core::meta::MetadataOptions; - use symphonia::core::probe::Hint; - - let cursor = Cursor::new(Arc::clone(data)); - let mss = MediaSourceStream::new(Box::new(cursor), Default::default()); - - let probed = symphonia::default::get_probe() - .format( - &Hint::new(), - mss, - &FormatOptions::default(), - &MetadataOptions::default(), - ) - .ok()?; - - let track = probed.format.default_track()?; - let time_base = track.codec_params.time_base?; - let n_frames = track.codec_params.n_frames?; - let time = time_base.calc_time(n_frames); - - Some(time.seconds as f64 + time.frac) -} - -/// Loads raw audio bytes from a file path or HTTP(S) URL. -fn load_source_data(src: &str) -> Result> { +fn load_source_descriptor(src: &str) -> Result { if src.starts_with("http://") || src.starts_with("https://") { reject_private_host(src)?; + Ok(SourceDescriptor::Remote(fetch_remote_source_descriptor( + src, + )?)) + } else { + if src.contains("://") || src.starts_with("data:") { + return Err(Error::Http(format!("Unsupported URL scheme: {src}"))); + } - let resp = ureq::AgentBuilder::new() - .timeout(HTTP_TIMEOUT) - .redirects(0) - .build() - .get(src) - .call() - .map_err(|e| Error::Http(format!("Failed to fetch {src}: {e}")))?; - - // Reject early if Content-Length exceeds the limit. - if let Some(len) = resp - .header("content-length") - .and_then(|v| v.parse::().ok()) - && len > MAX_DOWNLOAD_BYTES - { - return Err(Error::Http(format!( - "Response too large ({len} bytes, max {MAX_DOWNLOAD_BYTES})" - ))); + Ok(SourceDescriptor::Local { + path: PathBuf::from(src), + }) + } +} + +fn open_source(source: &SourceDescriptor) -> Result { + match source { + SourceDescriptor::Local { path } => { + let file = File::open(path).map_err(Error::Io)?; + let decoder = Decoder::try_from(file) + .map_err(|e| Error::Audio(format!("Failed to decode audio: {e}")))?; + Ok(Box::new(decoder)) } + SourceDescriptor::Remote(remote) => { + let mut builder = Decoder::builder() + .with_data(HttpAudioReader::new(remote.url.clone(), remote.byte_len)) + .with_seekable(true); - // Enforce the limit regardless of Content-Length (it can be absent or spoofed). - let mut bytes = Vec::new(); - resp - .into_reader() - .take(MAX_DOWNLOAD_BYTES + 1) - .read_to_end(&mut bytes) - .map_err(Error::Io)?; - - if bytes.len() as u64 > MAX_DOWNLOAD_BYTES { - return Err(Error::Http(format!( - "Response exceeded maximum size of {MAX_DOWNLOAD_BYTES} bytes" - ))); + if let Some(byte_len) = remote.byte_len { + builder = builder.with_byte_len(byte_len); + } + if let Some(hint) = remote.hint.as_deref() { + builder = builder.with_hint(hint); + } + if let Some(mime_type) = remote.mime_type.as_deref() { + builder = builder.with_mime_type(mime_type); + } + + let decoder = builder + .build() + .map_err(|e| Error::Audio(format!("Failed to decode audio: {e}")))?; + Ok(Box::new(decoder)) } + } +} + +fn fetch_remote_source_descriptor(src: &str) -> Result { + let resp = match descriptor_probe_request(src, true) { + Ok(resp) => resp, + Err(ureq::Error::Status(_, _)) => descriptor_probe_request(src, false) + .map_err(|e| Error::Http(format!("Failed to fetch {src}: {e}")))?, + Err(e) => return Err(Error::Http(format!("Failed to fetch {src}: {e}"))), + }; + + Ok(RemoteSourceDescriptor { + url: src.to_string(), + byte_len: parse_byte_len(&resp), + mime_type: resp.header("content-type").map(str::to_string), + hint: infer_hint(src), + }) +} - Ok(bytes) +fn descriptor_probe_request(src: &str, use_range: bool) -> std::result::Result { + let request = http_agent().get(src).set("Accept-Encoding", "identity"); + let request = if use_range { + request.set("Range", "bytes=0-0") } else { - if src.contains("://") || src.starts_with("data:") { - return Err(Error::Http(format!("Unsupported URL scheme: {src}"))); + request + }; + + request.call() +} + +fn infer_hint(src: &str) -> Option { + let path = src.split('?').next().unwrap_or(src); + let path = path.split('#').next().unwrap_or(path); + + PathBuf::from(path) + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()) +} + +fn http_agent() -> ureq::Agent { + ureq::AgentBuilder::new() + .timeout(HTTP_TIMEOUT) + .redirects(0) + .build() +} + +fn parse_byte_len(resp: &ureq::Response) -> Option { + resp + .header("content-range") + .and_then(parse_content_range_len) + .or_else(|| { + resp + .header("content-length") + .and_then(|value| value.parse::().ok()) + }) +} + +fn parse_content_range_len(value: &str) -> Option { + value.rsplit('/').next()?.parse::().ok() +} + +fn open_http_stream(url: &str, position: u64) -> Result<(HttpResponseReader, Option)> { + let request = http_agent().get(url).set("Accept-Encoding", "identity"); + let request = if position > 0 { + request.set("Range", &format!("bytes={position}-")) + } else { + request + }; + + let resp = request + .call() + .map_err(|e| Error::Http(format!("Failed to fetch {url}: {e}")))?; + let status = resp.status(); + let byte_len = parse_byte_len(&resp); + let mut reader = HttpResponseReader::new(resp); + + if position > 0 && status != 206 { + skip_bytes(&mut reader, position).map_err(Error::Io)?; + } + + Ok((reader, byte_len)) +} + +fn skip_bytes(reader: &mut R, mut remaining: u64) -> std::io::Result<()> { + let mut buffer = [0_u8; 8192]; + + while remaining > 0 { + let chunk_len = usize::try_from(remaining.min(buffer.len() as u64)).unwrap_or(buffer.len()); + let read = reader.read(&mut buffer[..chunk_len])?; + if read == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Unexpected EOF while skipping remote stream", + )); + } + remaining -= read as u64; + } + + Ok(()) +} + +fn http_to_io_error(error: Error) -> std::io::Error { + std::io::Error::other(error.to_string()) +} + +impl HttpResponseReader { + fn new(response: ureq::Response) -> Self { + Self { + inner: Mutex::new(Box::new(response.into_reader())), + } + } +} + +impl Read for HttpResponseReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self + .inner + .lock() + .unwrap_or_else(|e| e.into_inner()) + .read(buf) + } +} + +impl HttpAudioReader { + fn new(url: String, byte_len: Option) -> Self { + Self { + url, + position: 0, + byte_len, + reader: None, + reached_eof: false, + } + } + + fn ensure_reader(&mut self) -> std::io::Result<()> { + if self.reader.is_none() && !self.reached_eof { + let (reader, byte_len) = + open_http_stream(&self.url, self.position).map_err(http_to_io_error)?; + if self.byte_len.is_none() { + self.byte_len = byte_len; + } + self.reader = Some(reader); + } + + Ok(()) + } +} + +impl Read for HttpAudioReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.ensure_reader()?; + + let Some(reader) = &mut self.reader else { + return Ok(0); + }; + + let read = reader.read(buf)?; + if read == 0 { + self.reader = None; + self.reached_eof = true; + } else { + self.position += read as u64; + } + + Ok(read) + } +} + +impl Seek for HttpAudioReader { + fn seek(&mut self, position: SeekFrom) -> std::io::Result { + let next = match position { + SeekFrom::Start(offset) => offset as i128, + SeekFrom::Current(offset) => self.position as i128 + offset as i128, + SeekFrom::End(offset) => match self.byte_len { + Some(byte_len) => byte_len as i128 + offset as i128, + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Cannot seek from end without a known content length", + )); + } + }, + }; + + if next < 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot seek before the start of the stream", + )); } - std::fs::read(src).map_err(Error::Io) + + self.position = next as u64; + self.reader = None; + self.reached_eof = false; + Ok(self.position) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::sync::mpsc; + use std::thread; + + fn spawn_http_server( + responses: Vec<(String, Vec)>, + ) -> (String, mpsc::Receiver, thread::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let base_url = format!("http://{}", listener.local_addr().unwrap()); + let (request_tx, request_rx) = mpsc::channel(); + + let handle = thread::spawn(move || { + for (head, body) in responses { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = Vec::new(); + let mut buffer = [0_u8; 4096]; + + loop { + let read = stream.read(&mut buffer).unwrap(); + if read == 0 { + break; + } + request.extend_from_slice(&buffer[..read]); + if request.windows(4).any(|chunk| chunk == b"\r\n\r\n") { + break; + } + } + + request_tx + .send(String::from_utf8_lossy(&request).into_owned()) + .unwrap(); + + let response = format!( + "{head}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(response.as_bytes()).unwrap(); + stream.write_all(&body).unwrap(); + } + }); + + (base_url, request_rx, handle) + } + + #[test] + fn fetch_remote_source_descriptor_falls_back_to_plain_request() { + let responses = vec![ + ("HTTP/1.1 416 Range Not Satisfiable".to_string(), Vec::new()), + ( + "HTTP/1.1 200 OK\r\nContent-Type: audio/mpeg".to_string(), + b"abcde".to_vec(), + ), + ]; + let (url, request_rx, handle) = spawn_http_server(responses); + + let descriptor = fetch_remote_source_descriptor(&url).unwrap(); + let first_request = request_rx.recv().unwrap(); + let second_request = request_rx.recv().unwrap(); + handle.join().unwrap(); + + assert!(first_request.contains("Range: bytes=0-0")); + assert!(!second_request.contains("Range:")); + assert_eq!(descriptor.byte_len, Some(5)); + assert_eq!(descriptor.mime_type.as_deref(), Some("audio/mpeg")); + } + + #[test] + fn open_http_stream_skips_bytes_when_server_ignores_range() { + let responses = vec![("HTTP/1.1 200 OK".to_string(), b"abcdef".to_vec())]; + let (url, request_rx, handle) = spawn_http_server(responses); + + let (mut reader, byte_len) = open_http_stream(&url, 2).unwrap(); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).unwrap(); + let request = request_rx.recv().unwrap(); + handle.join().unwrap(); + + assert!(request.contains("Range: bytes=2-")); + assert_eq!(byte_len, Some(6)); + assert_eq!(bytes, b"cdef"); } } diff --git a/crates/audio-player/src/transitions.rs b/crates/audio-player/src/transitions.rs index 58bb27b..e39dab7 100644 --- a/crates/audio-player/src/transitions.rs +++ b/crates/audio-player/src/transitions.rs @@ -139,7 +139,11 @@ pub fn seek(state: &mut PlayerState, position: f64) -> Result<()> { ))); } } - state.current_time = position.clamp(0.0, state.duration); + state.current_time = if state.duration.is_finite() && state.duration > 0.0 { + position.clamp(0.0, state.duration) + } else { + position.max(0.0) + }; Ok(()) } @@ -541,6 +545,13 @@ mod tests { assert_eq!(s.current_time, 120.0); } + #[test] + fn seek_does_not_clamp_when_duration_unknown() { + let mut s = state_with_duration(PlaybackStatus::Ready, 0.0); + seek(&mut s, 45.0).unwrap(); + assert_eq!(s.current_time, 45.0); + } + #[test] fn seek_rejected_from_idle() { let mut s = state_with_status(PlaybackStatus::Idle); diff --git a/examples/tauri-app/src-tauri/Cargo.lock b/examples/tauri-app/src-tauri/Cargo.lock index 636e9f2..cd8a733 100644 --- a/examples/tauri-app/src-tauri/Cargo.lock +++ b/examples/tauri-app/src-tauri/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", "bitflags 2.11.0", @@ -46,9 +46,9 @@ dependencies = [ [[package]] name = "alsa-sys" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" dependencies = [ "libc", "pkg-config", @@ -110,7 +110,6 @@ version = "0.1.0" dependencies = [ "rodio", "serde", - "symphonia", "thiserror 2.0.18", "tracing", "ureq", @@ -134,24 +133,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -322,8 +303,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -333,15 +312,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfb" version = "0.7.3" @@ -381,23 +351,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.9", -] - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - [[package]] name = "combine" version = "4.6.7" @@ -466,45 +419,46 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586" dependencies = [ - "bindgen", + "bitflags 2.11.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ "alsa", - "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jni", "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.54.0", + "windows", ] [[package]] @@ -812,12 +766,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "embed-resource" version = "3.0.7" @@ -1398,12 +1346,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "html5ever" version = "0.29.1" @@ -1705,15 +1647,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.17" @@ -1765,16 +1698,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.91" @@ -1842,17 +1765,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libappindicator" version = "0.9.0" @@ -1873,7 +1785,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading 0.7.4", + "libloading", "once_cell", ] @@ -1893,16 +1805,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - [[package]] name = "libredox" version = "0.1.14" @@ -1941,9 +1843,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mach2" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" dependencies = [ "libc", ] @@ -2011,12 +1913,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2059,20 +1955,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.11.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2082,7 +1964,7 @@ dependencies = [ "bitflags 2.11.0", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -2094,15 +1976,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2125,13 +1998,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] -name = "nom" -version = "7.1.3" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "memchr", - "minimal-lexical", + "num-integer", + "num-traits", ] [[package]] @@ -2151,6 +2024,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2205,6 +2098,54 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -2212,7 +2153,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -2252,6 +2195,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2305,38 +2249,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -2978,16 +2890,15 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb" dependencies = [ - "claxon", "cpal", - "hound", - "lewton", + "dasp_sample", + "num-rational", "symphonia", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -3391,7 +3302,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", "js-sys", - "ndk 0.9.0", + "ndk", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3520,6 +3431,7 @@ dependencies = [ "symphonia-bundle-mp3", "symphonia-codec-aac", "symphonia-codec-pcm", + "symphonia-codec-vorbis", "symphonia-core", "symphonia-format-isomp4", "symphonia-format-ogg", @@ -3572,6 +3484,17 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-core" version = "0.5.5" @@ -3719,9 +3642,9 @@ dependencies = [ "jni", "libc", "log", - "ndk 0.9.0", + "ndk", "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3731,7 +3654,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3802,7 +3725,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.61.3", + "windows", ] [[package]] @@ -3929,7 +3852,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", ] [[package]] @@ -3954,7 +3877,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", "wry", ] @@ -4109,21 +4032,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.50.0" @@ -4770,7 +4678,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -4794,7 +4702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows 0.61.3", + "windows", "windows-core 0.61.2", ] @@ -4844,16 +4752,6 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -4876,16 +4774,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -4967,15 +4855,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -5409,7 +5288,7 @@ dependencies = [ "javascriptcore-rs", "jni", "libc", - "ndk 0.9.0", + "ndk", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -5427,7 +5306,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", From a2e3df1ea8a6fedcfe62e6d22384236c3d99aadd Mon Sep 17 00:00:00 2001 From: Remi D'Almeida Date: Thu, 9 Apr 2026 17:11:25 +0300 Subject: [PATCH 02/12] refactor: rename `load` action to `prepare` throughout codebase --- README.md | 12 ++--- build.rs | 2 +- crates/audio-player/src/models.rs | 2 +- crates/audio-player/src/player.rs | 10 ++-- crates/audio-player/src/transitions.rs | 46 +++++++++---------- examples/tauri-app/src/App.vue | 12 ++--- guest-js/actions.ts | 4 +- guest-js/index.test.ts | 42 ++++++++--------- guest-js/index.ts | 2 +- guest-js/types.ts | 16 +++---- permissions/autogenerated/commands/load.toml | 13 ------ .../autogenerated/commands/prepare.toml | 13 ++++++ permissions/autogenerated/reference.md | 26 +++++------ permissions/default.toml | 2 +- permissions/schemas/schema.json | 28 +++++------ src/commands.rs | 4 +- src/lib.rs | 2 +- 17 files changed, 118 insertions(+), 118 deletions(-) delete mode 100644 permissions/autogenerated/commands/load.toml create mode 100644 permissions/autogenerated/commands/prepare.toml diff --git a/README.md b/README.md index 6eee1ef..c8011e5 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ async function checkPlayer() { } ``` -#### Load, play, pause, stop, or seek +#### Prepare, play, pause, stop, or seek The API uses discriminated unions with type guards for compile-time safety. Only valid transport actions are available based on the player's status. @@ -158,11 +158,11 @@ import { getPlayer, PlaybackStatus, hasAction, AudioAction, } from '@silvermine/tauri-plugin-audio'; -async function loadAndPlay() { +async function prepareAndPlay() { const player = await getPlayer(); if (player.status === PlaybackStatus.Idle) { - const { player: ready } = await player.load( + const { player: ready } = await player.prepare( 'https://example.com/song.mp3', { title: 'My Song', @@ -260,13 +260,13 @@ the current `PlaybackStatus`: | Status | Allowed Actions | | --------- | ---------------------------- | -| Idle | load | +| Idle | prepare | | Loading | stop | | Ready | play, seek, stop | | Playing | pause, seek, stop | | Paused | play, seek, stop | -| Ended | play, seek, load, stop | -| Error | load | +| Ended | play, seek, prepare, stop | +| Error | prepare | Settings (`setVolume`, `setMuted`, `setPlaybackRate`, `setLoop`), `listen`, and `onTimeUpdate` are always available regardless of diff --git a/build.rs b/build.rs index 33ab4f7..994bac3 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,5 @@ const COMMANDS: &[&str] = &[ - "load", + "prepare", "play", "pause", "stop", diff --git a/crates/audio-player/src/models.rs b/crates/audio-player/src/models.rs index 3970d25..be49c65 100644 --- a/crates/audio-player/src/models.rs +++ b/crates/audio-player/src/models.rs @@ -96,7 +96,7 @@ pub struct TimeUpdate { pub duration: f64, } -/// Response from a transport action (load, play, pause, stop, seek). +/// Response from a transport action (prepare, play, pause, stop, seek). /// /// Wraps the resulting [`PlayerState`] with status-expectation metadata so the /// TypeScript layer can detect unexpected state transitions. diff --git a/crates/audio-player/src/player.rs b/crates/audio-player/src/player.rs index 7e333a6..6bc59e1 100644 --- a/crates/audio-player/src/player.rs +++ b/crates/audio-player/src/player.rs @@ -124,7 +124,7 @@ impl RodioAudioPlayer { lock_inner(&self.inner).state.clone() } - pub fn load(&self, src: &str, metadata: Option) -> Result { + pub fn prepare(&self, src: &str, metadata: Option) -> Result { let meta = metadata.unwrap_or_default(); // Transition to Loading and notify the frontend before starting I/O. @@ -138,7 +138,7 @@ impl RodioAudioPlayer { // Perform I/O, decoding, and sink creation. If any step fails, // transition to Error so the frontend can recover from the Loading state. - let result = self.load_inner(src, &meta); + let result = self.prepare_inner(src, &meta); match result { Ok(snapshot) => { @@ -156,9 +156,9 @@ impl RodioAudioPlayer { } } - /// Inner load logic that may fail. Separated so `load()` can catch errors + /// Inner prepare logic that may fail. Separated so `prepare()` can catch errors /// and transition to the Error state before propagating. - fn load_inner(&self, src: &str, meta: &AudioMetadata) -> Result { + fn prepare_inner(&self, src: &str, meta: &AudioMetadata) -> Result { let descriptor = load_source_descriptor(src)?; let source = open_source(&descriptor)?; let duration = source @@ -176,7 +176,7 @@ impl RodioAudioPlayer { let mut inner = lock_inner(&self.inner); // Re-check after I/O — another thread may have changed the state. - transitions::load(&mut inner.state, src, meta, duration)?; + transitions::prepare(&mut inner.state, src, meta, duration)?; Self::stop_monitor(&inner); diff --git a/crates/audio-player/src/transitions.rs b/crates/audio-player/src/transitions.rs index e39dab7..c685f8b 100644 --- a/crates/audio-player/src/transitions.rs +++ b/crates/audio-player/src/transitions.rs @@ -14,13 +14,13 @@ use crate::models::{AudioMetadata, PlaybackStatus, PlayerState}; /// Transitions to [`PlaybackStatus::Loading`] and stores metadata. /// /// Call this before starting I/O so the frontend can show a loading indicator. -/// After the I/O completes, call [`load`] to finalize the transition to `Ready`. +/// After the I/O completes, call [`prepare`] to finalize the transition to `Ready`. pub fn begin_load(state: &mut PlayerState, src: &str, meta: &AudioMetadata) -> Result<()> { match state.status { PlaybackStatus::Idle | PlaybackStatus::Ended | PlaybackStatus::Error => {} _ => { return Err(Error::InvalidState(format!( - "Cannot load in {:?} state", + "Cannot prepare in {:?} state", state.status ))); } @@ -37,10 +37,10 @@ pub fn begin_load(state: &mut PlayerState, src: &str, meta: &AudioMetadata) -> R Ok(()) } -/// Finalizes a load by transitioning from `Loading` to `Ready` with the +/// Finalizes prepare by transitioning from `Loading` to `Ready` with the /// decoded duration. Also accepts `Idle`, `Ended`, and `Error` in case /// `begin_load` was skipped (e.g. instant local file loads). -pub fn load(state: &mut PlayerState, src: &str, meta: &AudioMetadata, duration: f64) -> Result<()> { +pub fn prepare(state: &mut PlayerState, src: &str, meta: &AudioMetadata, duration: f64) -> Result<()> { match state.status { PlaybackStatus::Loading | PlaybackStatus::Idle @@ -48,7 +48,7 @@ pub fn load(state: &mut PlayerState, src: &str, meta: &AudioMetadata, duration: | PlaybackStatus::Error => {} _ => { return Err(Error::InvalidState(format!( - "Cannot load in {:?} state", + "Cannot prepare in {:?} state", state.status ))); } @@ -149,8 +149,8 @@ pub fn seek(state: &mut PlayerState, position: f64) -> Result<()> { /// Transitions to [`PlaybackStatus::Error`] with a message. /// -/// Valid from `Loading` (I/O or decode failure during load). Other statuses are -/// left unchanged — callers should only invoke this when a load operation fails +/// Valid from `Loading` (I/O or decode failure during prepare). Other statuses are +/// left unchanged — callers should only invoke this when a prepare operation fails /// after `begin_load` has already moved the state to `Loading`. pub fn error(state: &mut PlayerState, message: String) { if state.status == PlaybackStatus::Loading { @@ -276,12 +276,12 @@ mod tests { assert!(begin_load(&mut s, "a.mp3", &AudioMetadata::default()).is_err()); } - // -- load (finalize) -- + // -- prepare (finalize) -- #[test] - fn load_from_loading() { + fn prepare_from_loading() { let mut s = state_with_status(PlaybackStatus::Loading); - load(&mut s, "test.mp3", &meta("Song"), 120.0).unwrap(); + prepare(&mut s, "test.mp3", &meta("Song"), 120.0).unwrap(); assert_eq!(s.status, PlaybackStatus::Ready); assert_eq!(s.src.as_deref(), Some("test.mp3")); @@ -292,42 +292,42 @@ mod tests { } #[test] - fn load_from_idle() { + fn prepare_from_idle() { let mut s = state_with_status(PlaybackStatus::Idle); - load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).unwrap(); + prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).unwrap(); assert_eq!(s.status, PlaybackStatus::Ready); } #[test] - fn load_from_ended() { + fn prepare_from_ended() { let mut s = state_with_status(PlaybackStatus::Ended); - assert!(load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_ok()); + assert!(prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_ok()); assert_eq!(s.status, PlaybackStatus::Ready); } #[test] - fn load_from_error() { + fn prepare_from_error() { let mut s = state_with_status(PlaybackStatus::Error); - assert!(load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_ok()); + assert!(prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_ok()); assert_eq!(s.status, PlaybackStatus::Ready); } #[test] - fn load_rejected_from_ready() { + fn prepare_rejected_from_ready() { let mut s = state_with_status(PlaybackStatus::Ready); - assert!(load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_err()); + assert!(prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_err()); } #[test] - fn load_rejected_from_playing() { + fn prepare_rejected_from_playing() { let mut s = state_with_status(PlaybackStatus::Playing); - assert!(load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_err()); + assert!(prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_err()); } #[test] - fn load_rejected_from_paused() { + fn prepare_rejected_from_paused() { let mut s = state_with_status(PlaybackStatus::Paused); - assert!(load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_err()); + assert!(prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0).is_err()); } // -- play -- @@ -680,7 +680,7 @@ mod tests { assert_eq!(s.status, before.status); let mut s = state_with_status(PlaybackStatus::Playing); - let _ = load(&mut s, "a.mp3", &AudioMetadata::default(), 0.0); + let _ = prepare(&mut s, "a.mp3", &AudioMetadata::default(), 0.0); assert_eq!(s.status, PlaybackStatus::Playing); } } diff --git a/examples/tauri-app/src/App.vue b/examples/tauri-app/src/App.vue index b0b8bb5..aef1e03 100644 --- a/examples/tauri-app/src/App.vue +++ b/examples/tauri-app/src/App.vue @@ -201,7 +201,7 @@