diff --git a/.github/workflows/build-natives.yaml b/.github/workflows/build-natives.yaml new file mode 100644 index 0000000..69eb540 --- /dev/null +++ b/.github/workflows/build-natives.yaml @@ -0,0 +1,71 @@ +name: Build Native Libraries + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: aarch64-apple-darwin + resource_dir: darwin-aarch64 + lib: libnucleus_rodio.dylib + - os: macos-15-intel + target: x86_64-apple-darwin + resource_dir: darwin-x64 + lib: libnucleus_rodio.dylib + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + resource_dir: linux-x64 + lib: libnucleus_rodio.so + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + resource_dir: linux-aarch64 + lib: libnucleus_rodio.so + - os: windows-latest + target: x86_64-pc-windows-msvc + resource_dir: win32-x64 + lib: nucleus_rodio.dll + - os: windows-11-arm + target: aarch64-pc-windows-msvc + resource_dir: win32-aarch64 + lib: nucleus_rodio.dll + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Linux audio dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libasound2-dev pkg-config + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Build nucleus_rodio + working-directory: rodio/src/main/native + run: cargo build --release --target ${{ matrix.target }} + + - name: Stage native into resources + shell: bash + run: | + mkdir -p "rodio/src/main/resources/nucleus/native/${{ matrix.resource_dir }}" + cp "rodio/src/main/native/target/${{ matrix.target }}/release/${{ matrix.lib }}" \ + "rodio/src/main/resources/nucleus/native/${{ matrix.resource_dir }}/${{ matrix.lib }}" + ls -lh "rodio/src/main/resources/nucleus/native/${{ matrix.resource_dir }}/${{ matrix.lib }}" + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.resource_dir }} + path: rodio/src/main/resources/nucleus/native/${{ matrix.resource_dir }}/${{ matrix.lib }} + retention-days: 1 \ No newline at end of file diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml new file mode 100644 index 0000000..8b7099e --- /dev/null +++ b/.github/workflows/pre-merge.yaml @@ -0,0 +1,44 @@ +name: Pre-merge smoke + +on: + pull_request: + branches: [main] + +jobs: + smoke: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Build the host native only (the per-platform build scripts handle + # cross-arch in CI via build-natives.yaml). compileKotlin + the sample + # JVM target is enough to catch Kotlin/JNI wiring regressions. + os: [macos-latest, ubuntu-latest, windows-latest] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Linux audio dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libasound2-dev pkg-config + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: "temurin" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Make Gradle wrapper executable + if: runner.os != 'Windows' + run: chmod +x gradlew + + - name: Compile Kotlin (builds host native via processResources) + run: ./gradlew :rodio:compileKotlin --no-configuration-cache \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2ebbd83..ea23019 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,89 +5,14 @@ on: types: [published] jobs: - build-src: + build-natives: if: startsWith(github.event.release.tag_name, 'v') - name: Build native (${{ matrix.target }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: macos-latest - target: aarch64-apple-darwin - rodio_artifact: rodio-native-darwin-aarch64 - rodio_lib: librodio_kt.dylib - souvlaki_artifact: souvlaki-native-darwin-aarch64 - souvlaki_lib: libsouvlaki_kt.dylib - - os: macos-15-intel - target: x86_64-apple-darwin - rodio_artifact: rodio-native-darwin-x86_64 - rodio_lib: librodio_kt.dylib - souvlaki_artifact: souvlaki-native-darwin-x86_64 - souvlaki_lib: libsouvlaki_kt.dylib - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - rodio_artifact: rodio-native-linux-x86_64 - rodio_lib: librodio_kt.so - souvlaki_artifact: souvlaki-native-linux-x86_64 - souvlaki_lib: libsouvlaki_kt.so - - os: ubuntu-24.04-arm - target: aarch64-unknown-linux-gnu - rodio_artifact: rodio-native-linux-aarch64 - rodio_lib: librodio_kt.so - souvlaki_artifact: souvlaki-native-linux-aarch64 - souvlaki_lib: libsouvlaki_kt.so - - os: windows-latest - target: x86_64-pc-windows-msvc - rodio_artifact: rodio-native-windows-x86_64 - rodio_lib: rodio_kt.dll - souvlaki_artifact: souvlaki-native-windows-x86_64 - souvlaki_lib: souvlaki_kt.dll - - os: windows-11-arm - target: aarch64-pc-windows-msvc - rodio_artifact: rodio-native-windows-aarch64 - rodio_lib: rodio_kt.dll - souvlaki_artifact: souvlaki-native-windows-aarch64 - souvlaki_lib: souvlaki_kt.dll - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Linux audio dependencies - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends libasound2-dev pkg-config - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - - - name: Build Rodio library - working-directory: rodio - run: cargo build --release --target ${{ matrix.target }} - - - name: Build Souvlaki library - working-directory: souvlaki - run: cargo build --release --target ${{ matrix.target }} - - - name: Upload Rodio native library - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.rodio_artifact }} - path: rodio/target/${{ matrix.target }}/release/${{ matrix.rodio_lib }} - retention-days: 1 - - - name: Upload Souvlaki native library - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.souvlaki_artifact }} - path: souvlaki/target/${{ matrix.target }}/release/${{ matrix.souvlaki_lib }} - retention-days: 1 + uses: ./.github/workflows/build-natives.yaml publish: if: startsWith(github.event.release.tag_name, 'v') - needs: build-src - runs-on: macos-latest + needs: build-natives + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -96,178 +21,24 @@ jobs: run: | TAG="${{ github.event.release.tag_name }}" VERSION_NAME="${TAG#v}" - echo "VERSION_NAME=$VERSION_NAME" >> "$GITHUB_ENV" sed -i.bak "s/^VERSION_NAME=.*/VERSION_NAME=$VERSION_NAME/" gradle.properties rm -f gradle.properties.bak + # build.gradle.kts derives publishVersion from GITHUB_REF + echo "GITHUB_REF=refs/tags/${TAG}" >> "$GITHUB_ENV" - - name: Download macOS aarch64 native library - uses: actions/download-artifact@v4 - with: - name: rodio-native-darwin-aarch64 - path: rodio/target/aarch64-apple-darwin/release/ - - - name: Download macOS x86_64 native library - uses: actions/download-artifact@v4 - with: - name: rodio-native-darwin-x86_64 - path: rodio/target/x86_64-apple-darwin/release/ - - - name: Download Linux x86_64 native library - uses: actions/download-artifact@v4 - with: - name: rodio-native-linux-x86_64 - path: rodio/target/x86_64-unknown-linux-gnu/release/ - - - name: Download Linux aarch64 native library - uses: actions/download-artifact@v4 - with: - name: rodio-native-linux-aarch64 - path: rodio/target/aarch64-unknown-linux-gnu/release/ - - - name: Download Windows x86_64 native library - uses: actions/download-artifact@v4 - with: - name: rodio-native-windows-x86_64 - path: rodio/target/x86_64-pc-windows-msvc/release/ - - - name: Download Windows aarch64 native library + - name: Download all native artifacts uses: actions/download-artifact@v4 with: - name: rodio-native-windows-aarch64 - path: rodio/target/aarch64-pc-windows-msvc/release/ + path: natives - - name: Download macOS aarch64 Souvlaki native library - uses: actions/download-artifact@v4 - with: - name: souvlaki-native-darwin-aarch64 - path: souvlaki/target/aarch64-apple-darwin/release/ - - - name: Download macOS x86_64 Souvlaki native library - uses: actions/download-artifact@v4 - with: - name: souvlaki-native-darwin-x86_64 - path: souvlaki/target/x86_64-apple-darwin/release/ - - - name: Download Linux x86_64 Souvlaki native library - uses: actions/download-artifact@v4 - with: - name: souvlaki-native-linux-x86_64 - path: souvlaki/target/x86_64-unknown-linux-gnu/release/ - - - name: Download Linux aarch64 Souvlaki native library - uses: actions/download-artifact@v4 - with: - name: souvlaki-native-linux-aarch64 - path: souvlaki/target/aarch64-unknown-linux-gnu/release/ - - - name: Download Windows x86_64 Souvlaki native library - uses: actions/download-artifact@v4 - with: - name: souvlaki-native-windows-x86_64 - path: souvlaki/target/x86_64-pc-windows-msvc/release/ - - - name: Download Windows aarch64 Souvlaki native library - uses: actions/download-artifact@v4 - with: - name: souvlaki-native-windows-aarch64 - path: souvlaki/target/aarch64-pc-windows-msvc/release/ - - - name: Verify native libraries + - name: Stage natives into rodio resources run: | - echo "=== Native libraries downloaded ===" - ls -la rodio/target/aarch64-apple-darwin/release/ - ls -la rodio/target/x86_64-apple-darwin/release/ - ls -la rodio/target/x86_64-unknown-linux-gnu/release/ - ls -la rodio/target/aarch64-unknown-linux-gnu/release/ - ls -la rodio/target/x86_64-pc-windows-msvc/release/ - ls -la rodio/target/aarch64-pc-windows-msvc/release/ - ls -la souvlaki/target/aarch64-apple-darwin/release/ - ls -la souvlaki/target/x86_64-apple-darwin/release/ - ls -la souvlaki/target/x86_64-unknown-linux-gnu/release/ - ls -la souvlaki/target/aarch64-unknown-linux-gnu/release/ - ls -la souvlaki/target/x86_64-pc-windows-msvc/release/ - ls -la souvlaki/target/aarch64-pc-windows-msvc/release/ - - - name: Prepare JVM native resources - run: | - mkdir -p rodio/src/main/resources/darwin-aarch64 - mkdir -p rodio/src/main/resources/darwin-x86-64 - mkdir -p rodio/src/main/resources/linux-x86-64 - mkdir -p rodio/src/main/resources/linux-arm64 - mkdir -p rodio/src/main/resources/win32-x86-64 - mkdir -p rodio/src/main/resources/win32-arm64 - cp rodio/target/aarch64-apple-darwin/release/librodio_kt.dylib \ - rodio/src/main/resources/darwin-aarch64/ - cp rodio/target/x86_64-apple-darwin/release/librodio_kt.dylib \ - rodio/src/main/resources/darwin-x86-64/ - cp rodio/target/x86_64-unknown-linux-gnu/release/librodio_kt.so \ - rodio/src/main/resources/linux-x86-64/ - cp rodio/target/aarch64-unknown-linux-gnu/release/librodio_kt.so \ - rodio/src/main/resources/linux-arm64/ - cp rodio/target/x86_64-pc-windows-msvc/release/rodio_kt.dll \ - rodio/src/main/resources/win32-x86-64/ - cp rodio/target/aarch64-pc-windows-msvc/release/rodio_kt.dll \ - rodio/src/main/resources/win32-arm64/ - mkdir -p souvlaki/src/main/resources/darwin-aarch64 - mkdir -p souvlaki/src/main/resources/darwin-x86-64 - mkdir -p souvlaki/src/main/resources/linux-x86-64 - mkdir -p souvlaki/src/main/resources/linux-arm64 - mkdir -p souvlaki/src/main/resources/win32-x86-64 - mkdir -p souvlaki/src/main/resources/win32-arm64 - cp souvlaki/target/aarch64-apple-darwin/release/libsouvlaki_kt.dylib \ - souvlaki/src/main/resources/darwin-aarch64/ - cp souvlaki/target/x86_64-apple-darwin/release/libsouvlaki_kt.dylib \ - souvlaki/src/main/resources/darwin-x86-64/ - cp souvlaki/target/x86_64-unknown-linux-gnu/release/libsouvlaki_kt.so \ - souvlaki/src/main/resources/linux-x86-64/ - cp souvlaki/target/aarch64-unknown-linux-gnu/release/libsouvlaki_kt.so \ - souvlaki/src/main/resources/linux-arm64/ - cp souvlaki/target/x86_64-pc-windows-msvc/release/souvlaki_kt.dll \ - souvlaki/src/main/resources/win32-x86-64/ - cp souvlaki/target/aarch64-pc-windows-msvc/release/souvlaki_kt.dll \ - souvlaki/src/main/resources/win32-arm64/ - - - name: Verify JVM resources - run: | - echo "=== JVM native resources ===" - ls -la rodio/src/main/resources/darwin-aarch64/ - ls -la rodio/src/main/resources/darwin-x86-64/ - ls -la rodio/src/main/resources/linux-x86-64/ - ls -la rodio/src/main/resources/linux-arm64/ - ls -la rodio/src/main/resources/win32-x86-64/ - ls -la rodio/src/main/resources/win32-arm64/ - ls -la souvlaki/src/main/resources/darwin-aarch64/ - ls -la souvlaki/src/main/resources/darwin-x86-64/ - ls -la souvlaki/src/main/resources/linux-x86-64/ - ls -la souvlaki/src/main/resources/linux-arm64/ - ls -la souvlaki/src/main/resources/win32-x86-64/ - ls -la souvlaki/src/main/resources/win32-arm64/ - - - name: Create Gobley cache files - run: | - # Create cache files pointing to prebuilt directories for non-host targets - mkdir -p rodio/build/taskOutputCache/cargoBuildMacOSX64Release - mkdir -p rodio/build/taskOutputCache/cargoBuildLinuxX64Release - mkdir -p rodio/build/taskOutputCache/cargoBuildLinuxArm64Release - mkdir -p rodio/build/taskOutputCache/cargoBuildMinGWX64Release - mkdir -p rodio/build/taskOutputCache/cargoBuildMinGWArm64Release - - echo "$PWD/rodio/target/x86_64-apple-darwin/release" > rodio/build/taskOutputCache/cargoBuildMacOSX64Release/buildScriptOutputDirectories - echo "$PWD/rodio/target/x86_64-unknown-linux-gnu/release" > rodio/build/taskOutputCache/cargoBuildLinuxX64Release/buildScriptOutputDirectories - echo "$PWD/rodio/target/aarch64-unknown-linux-gnu/release" > rodio/build/taskOutputCache/cargoBuildLinuxArm64Release/buildScriptOutputDirectories - echo "$PWD/rodio/target/x86_64-pc-windows-msvc/release" > rodio/build/taskOutputCache/cargoBuildMinGWX64Release/buildScriptOutputDirectories - echo "$PWD/rodio/target/aarch64-pc-windows-msvc/release" > rodio/build/taskOutputCache/cargoBuildMinGWArm64Release/buildScriptOutputDirectories - mkdir -p souvlaki/build/taskOutputCache/cargoBuildMacOSX64Release - mkdir -p souvlaki/build/taskOutputCache/cargoBuildLinuxX64Release - mkdir -p souvlaki/build/taskOutputCache/cargoBuildLinuxArm64Release - mkdir -p souvlaki/build/taskOutputCache/cargoBuildMinGWX64Release - mkdir -p souvlaki/build/taskOutputCache/cargoBuildMinGWArm64Release - - echo "$PWD/souvlaki/target/x86_64-apple-darwin/release" > souvlaki/build/taskOutputCache/cargoBuildMacOSX64Release/buildScriptOutputDirectories - echo "$PWD/souvlaki/target/x86_64-unknown-linux-gnu/release" > souvlaki/build/taskOutputCache/cargoBuildLinuxX64Release/buildScriptOutputDirectories - echo "$PWD/souvlaki/target/aarch64-unknown-linux-gnu/release" > souvlaki/build/taskOutputCache/cargoBuildLinuxArm64Release/buildScriptOutputDirectories - echo "$PWD/souvlaki/target/x86_64-pc-windows-msvc/release" > souvlaki/build/taskOutputCache/cargoBuildMinGWX64Release/buildScriptOutputDirectories - echo "$PWD/souvlaki/target/aarch64-pc-windows-msvc/release" > souvlaki/build/taskOutputCache/cargoBuildMinGWArm64Release/buildScriptOutputDirectories + for dir in darwin-aarch64 darwin-x64 linux-x64 linux-aarch64 win32-x64 win32-aarch64; do + mkdir -p "rodio/src/main/resources/nucleus/native/$dir" + cp "natives/native-$dir/"* "rodio/src/main/resources/nucleus/native/$dir/" 2>/dev/null || true + done + echo "=== Staged natives ===" + find rodio/src/main/resources/nucleus/native -type f -exec ls -lh {} \; - name: Set up JDK uses: actions/setup-java@v4 @@ -275,9 +46,6 @@ jobs: java-version: "17" distribution: "temurin" - - name: Setup Rust (for UniFFI bindgen) - uses: dtolnay/rust-toolchain@stable - - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 @@ -285,10 +53,10 @@ jobs: run: chmod +x gradlew - name: Publish to Maven Central - run: ./gradlew :rodio:publishAndReleaseToMavenCentral :souvlaki:publishAndReleaseToMavenCentral --no-configuration-cache + run: ./gradlew :rodio:publishAndReleaseToMavenCentral --no-configuration-cache env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 065134c..9b820f6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ obj/ **/target/ *.rs.bk +# Native libraries (built on CI via build-natives.yaml, not committed) +**/src/main/resources/nucleus/native/ + # OS .DS_Store diff --git a/build.gradle.kts b/build.gradle.kts index 5a4948c..08e42c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,4 @@ plugins { alias(libs.plugins.android.library).apply(false) alias(libs.plugins.maven.publish).apply(false) alias(libs.plugins.android.application).apply(false) - alias(libs.plugins.kotlinAtomicfu).apply(false) - alias(libs.plugins.gobleyCargo).apply(false) - alias(libs.plugins.gobleyRust).apply(false) - alias(libs.plugins.gobleyUniffi).apply(false) -} +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e7f8af2..f4636c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,16 +17,16 @@ android.useAndroidX=true android.nonTransitiveRClass=true #Maven Publishing -GROUP=io.github.kdroidfilter +GROUP=dev.nucleusframework VERSION_NAME=1.0.0 POM_INCEPTION_YEAR=2025 -POM_URL=https://github.com/kdroidFilter/RodioKt +POM_URL=https://github.com/NucleusFramework/RodioKt POM_LICENSE_NAME=MIT License POM_LICENSE_URL=https://opensource.org/licenses/MIT POM_LICENSE_DIST=repo -POM_SCM_URL=https://github.com/kdroidFilter/RodioKt -POM_SCM_CONNECTION=scm:git:git://github.com/kdroidFilter/RodioKt.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/kdroidFilter/RodioKt.git -POM_DEVELOPER_ID=kdroidFilter -POM_DEVELOPER_NAME=kdroidFilter -POM_DEVELOPER_URL=https://github.com/kdroidFilter +POM_SCM_URL=https://github.com/NucleusFramework/RodioKt +POM_SCM_CONNECTION=scm:git:git://github.com/NucleusFramework/RodioKt.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/NucleusFramework/RodioKt.git +POM_DEVELOPER_ID=nucleusframework +POM_DEVELOPER_NAME=NucleusFramework +POM_DEVELOPER_URL=https://github.com/NucleusFramework diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4fe7ba..03d54d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,7 @@ maven-publish = "0.34.0" kotlinx-coroutines = "1.10.2" compose = "1.9.3" androidx-activityCompose = "1.10.1" -gobley = "0.3.7" -jna = "5.18.1" +core-runtime = "2.0.0-alpha-202607120006" [libraries] @@ -16,7 +15,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } +core-runtime = { module = "dev.nucleusframework:nucleus.core-runtime", version.ref = "core-runtime" } [plugins] @@ -26,8 +25,4 @@ android-library = { id = "com.android.library", version.ref = "agp" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -android-application = { id = "com.android.application", version.ref = "agp" } -kotlinAtomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } -gobleyCargo = { id = "dev.gobley.cargo", version.ref = "gobley" } -gobleyRust = { id = "dev.gobley.rust", version.ref = "gobley" } -gobleyUniffi = { id = "dev.gobley.uniffi", version.ref = "gobley" } +android-application = { id = "com.android.application", version.ref = "agp" } \ No newline at end of file diff --git a/rodio/build.gradle.kts b/rodio/build.gradle.kts index c3b2989..1a09e6d 100644 --- a/rodio/build.gradle.kts +++ b/rodio/build.gradle.kts @@ -1,172 +1,130 @@ -import com.vanniktech.maven.publish.JavaLibrary -import com.vanniktech.maven.publish.JavadocJar -import gobley.gradle.GobleyHost -import gobley.gradle.Variant -import gobley.gradle.cargo.dsl.jvm -import gobley.gradle.cargo.tasks.CargoBuildTask -import gobley.gradle.cargo.tasks.FindDynamicLibrariesTask -import gobley.gradle.cargo.tasks.RustUpTargetAddTask -import org.gradle.api.Project -import org.gradle.api.publish.maven.MavenPublication +import org.apache.tools.ant.taskdefs.condition.Os import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import java.io.File - -fun rustLibraryName(triple: String): String = when { - triple.contains("windows") -> "rodio_kt.dll" - triple.contains("darwin") || triple.contains("apple") -> "librodio_kt.dylib" - else -> "librodio_kt.so" -} - -fun Project.prebuiltRustLibrary(triple: String): File = - layout.projectDirectory.dir("target/$triple/release").file(rustLibraryName(triple)).asFile - -// Only these targets are built in CI - skip others to avoid cross-compilation errors -val supportedTargets = setOf( - "aarch64-apple-darwin", - "x86_64-apple-darwin", - "x86_64-unknown-linux-gnu", - "aarch64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", - "aarch64-pc-windows-msvc" -) plugins { alias(libs.plugins.kotlinJvm) - alias(libs.plugins.kotlinAtomicfu) - alias(libs.plugins.gobleyCargo) - alias(libs.plugins.gobleyRust) - alias(libs.plugins.gobleyUniffi) alias(libs.plugins.maven.publish) } -cargo { - jvmVariant.set(Variant.Release) - builds.jvm { - // Only embed for the current host platform; other platforms are packed as resources. - embedRustLibrary = GobleyHost.current.rustTarget == rustTarget - } -} - -rust { - val userHome = System.getProperty("user.home") - val cargoBin = file("$userHome/.cargo/bin") - val rustupToolchainBin = file( - "$userHome/.rustup/toolchains/stable-${GobleyHost.current.rustTarget.rustTriple}/bin", - ) - when { - cargoBin.resolve("rustc").exists() -> toolchainDirectory.set(cargoBin) - rustupToolchainBin.resolve("rustc").exists() -> toolchainDirectory.set(rustupToolchainBin) - } -} - -uniffi { - generateFromLibrary { - build.set(GobleyHost.current.rustTarget) - } -} +val publishVersion = + providers + .environmentVariable("GITHUB_REF") + .orNull + ?.removePrefix("refs/tags/v") + ?: "1.0.0" kotlin { - jvmToolchain(17) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } } -sourceSets { - main { - resources.srcDir("src/main/resources") - } +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { - implementation(libs.jna) implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.swing) + implementation(libs.core.runtime) testImplementation(kotlin("test")) testImplementation(libs.kotlinx.coroutines.test) } -tasks.withType().configureEach { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) +// ── Native build ──────────────────────────────────────────────────────────── +// The Rust `nucleus_rodio` crate (rodio + reqwest + hand-written JNI). Native +// binaries ship in src/main/resources/nucleus/native// and are +// extracted at runtime by NativeLibraryLoader (from core-runtime). + +val buildNativeMacOs by tasks.registering(Exec::class) { + description = "Compiles the Rust JNI bridge into macOS dylibs (arm64 + x86_64)" + group = "build" + val outputDir = file("src/main/resources/nucleus/native") + val checkFile = File(outputDir, "darwin-aarch64/libnucleus_rodio.dylib") + onlyIf { Os.isFamily(Os.FAMILY_MAC) && !checkFile.exists() } + inputs.dir(file("src/main/native/src")) + inputs.file(file("src/main/native/Cargo.toml")) + outputs.dir(outputDir) + workingDir(file("src/main/native/macos")) + commandLine("bash", "build.sh") +} + +val buildNativeWindows by tasks.registering(Exec::class) { + description = "Compiles the Rust JNI bridge into Windows DLLs (x64 + ARM64)" + group = "build" + val outputDir = file("src/main/resources/nucleus/native") + val checkFile = File(outputDir, "win32-x64/nucleus_rodio.dll") + onlyIf { Os.isFamily(Os.FAMILY_WINDOWS) && !checkFile.exists() } + inputs.dir(file("src/main/native/src")) + inputs.file(file("src/main/native/Cargo.toml")) + outputs.dir(outputDir) + workingDir(file("src/main/native/windows")) + commandLine("cmd", "/c", ".\\build.bat") +} + +val buildNativeLinux by tasks.registering(Exec::class) { + description = "Compiles the Rust JNI bridge into a Linux .so (host arch)" + group = "build" + val outputDir = file("src/main/resources/nucleus/native") + val arch = System.getProperty("os.arch").lowercase() + val archDir = if (arch.contains("aarch64") || arch.contains("arm64")) "linux-aarch64" else "linux-x64" + val checkFile = File(outputDir, "$archDir/libnucleus_rodio.so") + onlyIf { Os.isFamily(Os.FAMILY_UNIX) && !Os.isFamily(Os.FAMILY_MAC) && !checkFile.exists() } + inputs.dir(file("src/main/native/src")) + inputs.file(file("src/main/native/Cargo.toml")) + outputs.dir(outputDir) + workingDir(file("src/main/native/linux")) + commandLine("bash", "build.sh") +} + +tasks.processResources { + dependsOn(buildNativeMacOs) + dependsOn(buildNativeWindows) + dependsOn(buildNativeLinux) +} + +tasks.configureEach { + if (name == "sourcesJar") { + dependsOn(buildNativeMacOs) + dependsOn(buildNativeWindows) + dependsOn(buildNativeLinux) } } -tasks.withType().configureEach { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} +// ── Maven publication ────────────────────────────────────────────────────── -tasks.withType().configureEach { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - -tasks.withType().configureEach { - onlyIf { - val rustTarget = target.orNull ?: return@onlyIf true - val triple = rustTarget.rustTriple - // Skip unsupported targets entirely - if (triple !in supportedTargets) return@onlyIf false - val prebuiltLib = project.prebuiltRustLibrary(triple) - val isHostTarget = GobleyHost.current.rustTarget.rustTriple == triple - !prebuiltLib.exists() || isHostTarget - } -} +mavenPublishing { + coordinates("dev.nucleusframework", "nucleus.rodio", publishVersion) -tasks.withType().configureEach { - val rustTarget = rustTarget.orNull ?: return@configureEach - val triple = rustTarget.rustTriple - val prebuiltLib = project.prebuiltRustLibrary(triple) - val isHostTarget = GobleyHost.current.rustTarget.rustTriple == triple - if (prebuiltLib.exists() && !isHostTarget) { - searchPaths.set(listOf(prebuiltLib.parentFile)) - } -} + pom { + name.set("Nucleus Rodio") + description.set("Kotlin audio playback library powered by the Rust rodio crate via hand-written JNI.") + url.set("https://github.com/NucleusFramework/RodioKt") + + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + } + } -tasks.withType().configureEach { - onlyIf { - val rustTarget = rustTarget.orNull ?: return@onlyIf true - val triple = rustTarget.rustTriple - // Skip unsupported targets entirely - if (triple !in supportedTargets) return@onlyIf false - val prebuiltLib = project.prebuiltRustLibrary(triple) - val isHostTarget = GobleyHost.current.rustTarget.rustTriple == triple - !prebuiltLib.exists() || isHostTarget - } -} + developers { + developer { + id.set("nucleusframework") + name.set("NucleusFramework") + url.set("https://github.com/NucleusFramework") + } + } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) + scm { + url.set("https://github.com/NucleusFramework/RodioKt") + connection.set("scm:git:git://github.com/NucleusFramework/RodioKt.git") + developerConnection.set("scm:git:ssh://git@github.com/NucleusFramework/RodioKt.git") + } } -} -mavenPublishing { - configure(JavaLibrary(javadocJar = JavadocJar.Empty(), sourcesJar = true)) publishToMavenCentral() - if (project.findProperty("signingInMemoryKey") != null) { + if (project.hasProperty("signingInMemoryKey")) { signAllPublications() } - coordinates(artifactId = "rodio") - pom { - name.set("RodioKt") - description.set("Kotlin audio playback library powered by Rust Rodio") - } -} - -// Publish native runtime JARs as additional artifacts -afterEvaluate { - publishing { - publications.withType().configureEach { - if (name == "maven") { - // Add native runtime JARs for each platform - val nativeJars = layout.buildDirectory.dir("libs").get().asFile.listFiles() - ?.filter { it.name.startsWith("rodio-") && it.name.endsWith(".jar") && !it.name.contains("sources") && !it.name.contains("javadoc") } - ?: emptyList() - - nativeJars.forEach { jar -> - val classifier = jar.name.removePrefix("rodio-").removeSuffix(".jar") - artifact(jar) { - this.classifier = classifier - } - } - } - } - } -} +} \ No newline at end of file diff --git a/rodio/src/main/kotlin/dev/nucleusframework/rodio/PlaybackCallback.kt b/rodio/src/main/kotlin/dev/nucleusframework/rodio/PlaybackCallback.kt new file mode 100644 index 0000000..16eedc3 --- /dev/null +++ b/rodio/src/main/kotlin/dev/nucleusframework/rodio/PlaybackCallback.kt @@ -0,0 +1,16 @@ +package dev.nucleusframework.rodio + +/** + * Receives playback notifications from the native rodio backend. + * + * Implementations are invoked from rodio's internal decoder/audio thread + * (and, for ICY metadata, the stream read thread) — they must be thread-safe + * and must not block. Marshal UI work to the appropriate dispatcher. + */ +interface PlaybackCallback { + fun onEvent(event: PlaybackEvent) + + fun onMetadata(key: String, value: String) + + fun onError(message: String) +} \ No newline at end of file diff --git a/rodio/src/main/kotlin/dev/nucleusframework/rodio/PlaybackEvent.kt b/rodio/src/main/kotlin/dev/nucleusframework/rodio/PlaybackEvent.kt new file mode 100644 index 0000000..8a635ed --- /dev/null +++ b/rodio/src/main/kotlin/dev/nucleusframework/rodio/PlaybackEvent.kt @@ -0,0 +1,19 @@ +package dev.nucleusframework.rodio + +/** + * Playback state reported via [PlaybackCallback.onEvent]. + * + * Ordinals are pinned (0..3) because they cross the JNI boundary as a raw + * `jint` — see `PlaybackEvent` in `nucleus_rodio` (`lib.rs`). Do not reorder. + */ +enum class PlaybackEvent { + CONNECTING, + PLAYING, + PAUSED, + STOPPED, + ; + + internal companion object { + fun fromInt(value: Int): PlaybackEvent = entries.getOrElse(value) { STOPPED } + } +} \ No newline at end of file diff --git a/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioException.kt b/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioException.kt new file mode 100644 index 0000000..38d6721 --- /dev/null +++ b/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioException.kt @@ -0,0 +1,7 @@ +package dev.nucleusframework.rodio + +/** + * Raised by the native rodio backend. Thrown from JNI + * (`dev.nucleusframework.rodio.RodioException`) when a native call fails. + */ +class RodioException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioHttp.kt b/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioHttp.kt new file mode 100644 index 0000000..437c778 --- /dev/null +++ b/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioHttp.kt @@ -0,0 +1,23 @@ +package dev.nucleusframework.rodio + +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeHttpAddRootCertPem +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeHttpClearRootCerts +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeHttpSetAllowInvalidCerts + +/** + * Global HTTP options applied to every rodio network request (streaming and + * download). Settings are process-wide and take effect on the next request. + */ +object RodioHttp { + fun setAllowInvalidCerts(allow: Boolean) { + nativeHttpSetAllowInvalidCerts(allow) + } + + fun addRootCertPem(pem: String) { + nativeHttpAddRootCertPem(pem) + } + + fun clearRootCerts() { + nativeHttpClearRootCerts() + } +} \ No newline at end of file diff --git a/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioPlayer.kt b/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioPlayer.kt new file mode 100644 index 0000000..41b7f86 --- /dev/null +++ b/rodio/src/main/kotlin/dev/nucleusframework/rodio/RodioPlayer.kt @@ -0,0 +1,160 @@ +package dev.nucleusframework.rodio + +import dev.nucleusframework.rodio.internal.NativePlaybackCallbackRaw +import dev.nucleusframework.rodio.internal.NativeRodioBridge +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeClear +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeClearCallback +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeCreatePlayer +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeCreatePlayerWithBufferSizeFrames +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeDestroyPlayer +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeGetDurationMs +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeGetPositionMs +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeIsEmpty +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeIsPaused +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeIsSeekable +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativePause +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativePlay +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativePlayFile +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativePlayRadio +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativePlaySine +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativePlayUrl +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeSeekPositionMs +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeSetCallback +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeSetVolume +import dev.nucleusframework.rodio.internal.NativeRodioBridge.nativeStop +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Audio player backed by the Rust `rodio` crate over hand-written JNI. + * + * Decoding and streaming run on rodio's internal threads; the public methods + * here are thin synchronous wrappers. The `*Async` variants hop off the + * caller thread onto [Dispatchers.Default] — useful for the blocking + * network/file reads in `playUrl`/`playRadio`/`playFile`. + * + * Remember to call [close] to release the native output stream. + */ +class RodioPlayer( + /** Optional output buffer size in frames. Larger values raise latency but reduce underruns. */ + bufferSizeFrames: Int? = null, +) { + private companion object { + val playbackDispatcher = Dispatchers.Default + } + + private var handle: Long = when { + bufferSizeFrames == null -> nativeCreatePlayer() + bufferSizeFrames <= 0 -> throw IllegalArgumentException("bufferSizeFrames must be > 0") + else -> nativeCreatePlayerWithBufferSizeFrames(bufferSizeFrames) + } + private var closed = false + + private fun requireHandle(): Long { + check(!closed) { "RodioPlayer is closed" } + return handle + } + + fun playFile(path: String, loop: Boolean) { + nativePlayFile(requireHandle(), path, loop) + } + + suspend fun playFileAsync(path: String, loop: Boolean) { + withContext(playbackDispatcher) { playFile(path, loop) } + } + + fun playUrl(url: String, loop: Boolean = false, callback: PlaybackCallback? = null) { + if (callback != null) setCallback(callback) + nativePlayUrl(requireHandle(), url, loop) + } + + suspend fun playUrlAsync(url: String, loop: Boolean = false, callback: PlaybackCallback? = null) { + withContext(playbackDispatcher) { playUrl(url, loop, callback) } + } + + fun playRadio(url: String, callback: PlaybackCallback? = null) { + if (callback != null) setCallback(callback) + nativePlayRadio(requireHandle(), url) + } + + suspend fun playRadioAsync(url: String, callback: PlaybackCallback? = null) { + withContext(playbackDispatcher) { playRadio(url, callback) } + } + + fun playSine(frequencyHz: Float, durationMs: Long) { + require(durationMs > 0) { "durationMs must be > 0" } + nativePlaySine(requireHandle(), frequencyHz, durationMs) + } + + fun play() { + nativePlay(requireHandle()) + } + + fun pause() { + nativePause(requireHandle()) + } + + fun stop() { + nativeStop(requireHandle()) + } + + fun clear() { + nativeClear(requireHandle()) + } + + fun getPositionMs(): Long = nativeGetPositionMs(requireHandle()) + + fun getDurationMs(): Long? { + val ms = nativeGetDurationMs(requireHandle()) + return if (ms < 0) null else ms + } + + fun seekToMs(positionMs: Long) { + require(positionMs >= 0) { "positionMs must be >= 0" } + nativeSeekPositionMs(requireHandle(), positionMs) + } + + fun isSeekable(): Boolean = nativeIsSeekable(requireHandle()) + + fun setVolume(volume: Float) { + nativeSetVolume(requireHandle(), volume) + } + + fun setCallback(callback: PlaybackCallback?) { + if (callback == null) { + nativeClearCallback(requireHandle()) + } else { + nativeSetCallback(requireHandle(), callback.toRaw()) + } + } + + fun clearCallback() { + nativeClearCallback(requireHandle()) + } + + fun isPaused(): Boolean = nativeIsPaused(requireHandle()) + + fun isEmpty(): Boolean = nativeIsEmpty(requireHandle()) + + fun close() { + if (closed) return + nativeDestroyPlayer(handle) + closed = true + } +} + +/** Adapts the public [PlaybackCallback] (enum-typed) to the raw JNI interface. */ +private fun PlaybackCallback.toRaw(): NativePlaybackCallbackRaw = + object : NativePlaybackCallbackRaw { + override fun onEvent(event: Int) { + this@toRaw.onEvent(PlaybackEvent.fromInt(event)) + } + + override fun onMetadata(key: String, value: String) { + this@toRaw.onMetadata(key, value) + } + + override fun onError(message: String) { + this@toRaw.onError(message) + } + } \ No newline at end of file diff --git a/rodio/src/main/kotlin/dev/nucleusframework/rodio/internal/NativeRodioBridge.kt b/rodio/src/main/kotlin/dev/nucleusframework/rodio/internal/NativeRodioBridge.kt new file mode 100644 index 0000000..7460cf4 --- /dev/null +++ b/rodio/src/main/kotlin/dev/nucleusframework/rodio/internal/NativeRodioBridge.kt @@ -0,0 +1,78 @@ +package dev.nucleusframework.rodio.internal + +import dev.nucleusframework.core.runtime.NativeLibraryLoader + +private const val LIBRARY_NAME = "nucleus_rodio" + +/** + * JNI-friendly callback interface: rodio calls back with a raw `Int` event + * ordinal (see [dev.nucleusframework.rodio.PlaybackEvent]) and `String` + * payloads. The native side pins a single instance per player via a JNI + * `GlobalRef` and invokes it from non-JVM threads after attaching them. + */ +internal interface NativePlaybackCallbackRaw { + fun onEvent(event: Int) + + fun onMetadata(key: String, value: String) + + fun onError(message: String) +} + +/** + * Direct JNI bridge over the Rust `rodio` crate. Symbols live in the native + * library `nucleus_rodio` under `dev.nucleusframework.rodio.internal.NativeRodioBridge`. + * + * Duration getters encode `None` as `-1L` (see `nativeGetDurationMs`). + */ +@Suppress("TooManyFunctions") +internal object NativeRodioBridge { + private val loaded = NativeLibraryLoader.load(LIBRARY_NAME, NativeRodioBridge::class.java) + + val isLoaded: Boolean get() = loaded + + @JvmStatic external fun nativeCreatePlayer(): Long + + @JvmStatic external fun nativeCreatePlayerWithBufferSizeFrames(bufferSizeFrames: Int): Long + + @JvmStatic external fun nativeDestroyPlayer(id: Long) + + @JvmStatic external fun nativeSetCallback(id: Long, callback: NativePlaybackCallbackRaw) + + @JvmStatic external fun nativeClearCallback(id: Long) + + @JvmStatic external fun nativePlayFile(id: Long, path: String, loop: Boolean) + + @JvmStatic external fun nativePlayUrl(id: Long, url: String, loop: Boolean) + + @JvmStatic external fun nativePlayRadio(id: Long, url: String) + + @JvmStatic external fun nativePlaySine(id: Long, frequencyHz: Float, durationMs: Long) + + @JvmStatic external fun nativePlay(id: Long) + + @JvmStatic external fun nativePause(id: Long) + + @JvmStatic external fun nativeStop(id: Long) + + @JvmStatic external fun nativeClear(id: Long) + + @JvmStatic external fun nativeIsPaused(id: Long): Boolean + + @JvmStatic external fun nativeIsEmpty(id: Long): Boolean + + @JvmStatic external fun nativeGetPositionMs(id: Long): Long + + @JvmStatic external fun nativeSeekPositionMs(id: Long, positionMs: Long) + + @JvmStatic external fun nativeGetDurationMs(id: Long): Long + + @JvmStatic external fun nativeIsSeekable(id: Long): Boolean + + @JvmStatic external fun nativeSetVolume(id: Long, volume: Float) + + @JvmStatic external fun nativeHttpSetAllowInvalidCerts(allow: Boolean) + + @JvmStatic external fun nativeHttpAddRootCertPem(pem: String) + + @JvmStatic external fun nativeHttpClearRootCerts() +} \ No newline at end of file diff --git a/rodio/src/main/kotlin/io/github/kdroidfilter/rodio/Rodio.kt b/rodio/src/main/kotlin/io/github/kdroidfilter/rodio/Rodio.kt deleted file mode 100644 index 840304c..0000000 --- a/rodio/src/main/kotlin/io/github/kdroidfilter/rodio/Rodio.kt +++ /dev/null @@ -1,138 +0,0 @@ -package io.github.kdroidfilter.rodio - -import io.github.kdroidfilter.rodio.native.createPlayer -import io.github.kdroidfilter.rodio.native.createPlayerWithBufferSizeFrames -import io.github.kdroidfilter.rodio.native.destroyPlayer -import io.github.kdroidfilter.rodio.native.playerClear -import io.github.kdroidfilter.rodio.native.playerClearCallback -import io.github.kdroidfilter.rodio.native.playerGetDurationMs -import io.github.kdroidfilter.rodio.native.playerGetPositionMs -import io.github.kdroidfilter.rodio.native.playerIsEmpty -import io.github.kdroidfilter.rodio.native.playerIsPaused -import io.github.kdroidfilter.rodio.native.playerPause -import io.github.kdroidfilter.rodio.native.playerPlay -import io.github.kdroidfilter.rodio.native.playerPlayFile -import io.github.kdroidfilter.rodio.native.playerPlayRadio -import io.github.kdroidfilter.rodio.native.playerPlaySine -import io.github.kdroidfilter.rodio.native.playerPlayUrl -import io.github.kdroidfilter.rodio.native.playerIsSeekable -import io.github.kdroidfilter.rodio.native.playerSeekPositionMs -import io.github.kdroidfilter.rodio.native.playerSetCallback -import io.github.kdroidfilter.rodio.native.playerSetVolume -import io.github.kdroidfilter.rodio.native.playerStop -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -typealias PlaybackCallback = io.github.kdroidfilter.rodio.native.PlaybackCallback -typealias PlaybackEvent = io.github.kdroidfilter.rodio.native.PlaybackEvent - -class RodioPlayer( - /** Optional output buffer size in frames. Larger values raise latency but reduce underruns. */ - bufferSizeFrames: Int? = null, -) { - private companion object { - val playbackDispatcher = Dispatchers.Default - } - - private var handle: ULong = when { - bufferSizeFrames == null -> createPlayer() - bufferSizeFrames <= 0 -> throw IllegalArgumentException("bufferSizeFrames must be > 0") - else -> createPlayerWithBufferSizeFrames(bufferSizeFrames.toUInt()) - } - private var closed = false - - private fun requireHandle(): ULong { - check(!closed) { "RodioPlayer is closed" } - return handle - } - - fun playFile(path: String, loop: Boolean) { - playerPlayFile(requireHandle(), path, loop) - } - - suspend fun playFileAsync(path: String, loop: Boolean) { - withContext(playbackDispatcher) { - playFile(path, loop) - } - } - - fun playUrl(url: String, loop: Boolean = false, callback: PlaybackCallback? = null) { - if (callback != null) setCallback(callback) - playerPlayUrl(requireHandle(), url, loop) - } - - suspend fun playUrlAsync(url: String, loop: Boolean = false, callback: PlaybackCallback? = null) { - withContext(playbackDispatcher) { - playUrl(url, loop, callback) - } - } - - fun playRadio(url: String, callback: PlaybackCallback? = null) { - if (callback != null) setCallback(callback) - playerPlayRadio(requireHandle(), url) - } - - suspend fun playRadioAsync(url: String, callback: PlaybackCallback? = null) { - withContext(playbackDispatcher) { - playRadio(url, callback) - } - } - - fun playSine(frequencyHz: Float, durationMs: Long) { - require(durationMs > 0) { "durationMs must be > 0" } - playerPlaySine(requireHandle(), frequencyHz, durationMs.toULong()) - } - - fun play() { - playerPlay(requireHandle()) - } - - fun pause() { - playerPause(requireHandle()) - } - - fun stop() { - playerStop(requireHandle()) - } - - fun clear() { - playerClear(requireHandle()) - } - - fun getPositionMs(): Long = playerGetPositionMs(requireHandle()).toLong() - - fun getDurationMs(): Long? = playerGetDurationMs(requireHandle())?.toLong() - - fun seekToMs(positionMs: Long) { - require(positionMs >= 0) { "positionMs must be >= 0" } - playerSeekPositionMs(requireHandle(), positionMs.toULong()) - } - - fun isSeekable(): Boolean = playerIsSeekable(requireHandle()) - - fun setVolume(volume: Float) { - playerSetVolume(requireHandle(), volume) - } - - fun setCallback(callback: PlaybackCallback?) { - if (callback == null) { - playerClearCallback(requireHandle()) - } else { - playerSetCallback(requireHandle(), callback) - } - } - - fun clearCallback() { - playerClearCallback(requireHandle()) - } - - fun isPaused(): Boolean = playerIsPaused(requireHandle()) - - fun isEmpty(): Boolean = playerIsEmpty(requireHandle()) - - fun close() { - if (closed) return - destroyPlayer(handle) - closed = true - } -} diff --git a/rodio/src/main/kotlin/io/github/kdroidfilter/rodio/RodioHttp.kt b/rodio/src/main/kotlin/io/github/kdroidfilter/rodio/RodioHttp.kt deleted file mode 100644 index bd0c79e..0000000 --- a/rodio/src/main/kotlin/io/github/kdroidfilter/rodio/RodioHttp.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.kdroidfilter.rodio - -import io.github.kdroidfilter.rodio.native.httpAddRootCertPem -import io.github.kdroidfilter.rodio.native.httpClearRootCerts -import io.github.kdroidfilter.rodio.native.httpSetAllowInvalidCerts - -object RodioHttp { - fun setAllowInvalidCerts(allow: Boolean) { - httpSetAllowInvalidCerts(allow) - } - - fun addRootCertPem(pem: String) { - httpAddRootCertPem(pem) - } - - fun clearRootCerts() { - httpClearRootCerts() - } -} \ No newline at end of file diff --git a/rodio/src/main/native/Cargo.lock b/rodio/src/main/native/Cargo.lock new file mode 100644 index 0000000..65effed --- /dev/null +++ b/rodio/src/main/native/Cargo.lock @@ -0,0 +1,2290 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.13.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "from_map" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f31122ab0445ff8cee420b805f24e07683073815de1dd276ee7d588d301700" +dependencies = [ + "hashmap_derive", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashmap_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb30bf173e72cc31b5265dac095423ca14e7789ff7c3b0e6096a37a996f12883" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hls_m3u8" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3540d6aab90788c46dab12263fb70b25f5b16e0eee6a07bbdc9960057947e128" +dependencies = [ + "derive_builder", + "derive_more", + "hex", + "shorthand", + "stable-vec", + "strum", + "thiserror 1.0.69", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nucleus_rodio" +version = "0.1.0" +dependencies = [ + "hls_m3u8", + "jni", + "once_cell", + "reqwest", + "rodio", + "thiserror 2.0.18", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "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", +] + +[[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.13.0", + "objc2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "objc2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rodio" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" +dependencies = [ + "cpal", + "dasp_sample", + "num-rational", + "symphonia", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "shorthand" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474f77f985d8212610f170332eaf173e768404c0c1d4deb041f32c297cf18931" +dependencies = [ + "from_map", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable-vec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dac7bc0f7d0d44329b200020effbc25a534d89fa142af95e3ddf76113412a5e" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.118", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rodio/src/main/native/Cargo.toml b/rodio/src/main/native/Cargo.toml new file mode 100644 index 0000000..cd8f010 --- /dev/null +++ b/rodio/src/main/native/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "nucleus_rodio" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "nucleus_rodio" +crate-type = ["cdylib", "staticlib"] +path = "src/lib.rs" + +[dependencies] +rodio = "0.21.1" +reqwest = { version = "0.12.28", default-features = false, features = ["blocking", "rustls-tls-native-roots", "rustls-tls-webpki-roots"] } +thiserror = "2.0.11" +hls_m3u8 = "0.5.1" +jni = "0.21" +once_cell = "1.20" + +[profile.release] +opt-level = "z" +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = "debuginfo" +incremental = false + +[profile.dev] +panic = "abort" \ No newline at end of file diff --git a/rodio/src/main/native/linux/build.sh b/rodio/src/main/native/linux/build.sh new file mode 100644 index 0000000..1751f3f --- /dev/null +++ b/rodio/src/main/native/linux/build.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Compiles the Rust JNI bridge (nucleus_rodio) into a Linux shared library and +# copies it into the host-architecture resource folder consumed by +# NativeLibraryLoader: +# src/main/resources/nucleus/native/{linux-x64,linux-aarch64}/libnucleus_rodio.so +# +# Only the host architecture is built here — cross-building aarch64 from +# x86_64 (or vice-versa) needs a proper ALSA sysroot for the target, which is +# not reasonable to assume in every dev environment. CI builds both slots. +# +# Prerequisites: +# - rustup with the matching host target (x86_64- or aarch64-unknown-linux-gnu) +# - ALSA development headers (libasound2-dev / alsa-lib-devel) — rodio's cpal +# - pkg-config +# Usage: ./build.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +NATIVE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +RESOURCE_DIR="$NATIVE_DIR/../resources/nucleus/native" +OUT_DIR_X64="$RESOURCE_DIR/linux-x64" +OUT_DIR_ARM64="$RESOURCE_DIR/linux-aarch64" + +mkdir -p "$OUT_DIR_X64" "$OUT_DIR_ARM64" + +HOST_ARCH="$(uname -m)" + +if ! command -v cargo >/dev/null 2>&1; then + echo "ERROR: cargo not found. Install rustup from https://rustup.rs/" >&2 + exit 1 +fi + +pushd "$NATIVE_DIR" >/dev/null + +case "$HOST_ARCH" in + x86_64) + rustup target add x86_64-unknown-linux-gnu >/dev/null + cargo build --release --target x86_64-unknown-linux-gnu + cp "target/x86_64-unknown-linux-gnu/release/libnucleus_rodio.so" \ + "$OUT_DIR_X64/libnucleus_rodio.so" + strip --strip-unneeded "$OUT_DIR_X64/libnucleus_rodio.so" || true + ;; + aarch64|arm64) + rustup target add aarch64-unknown-linux-gnu >/dev/null + cargo build --release --target aarch64-unknown-linux-gnu + cp "target/aarch64-unknown-linux-gnu/release/libnucleus_rodio.so" \ + "$OUT_DIR_ARM64/libnucleus_rodio.so" + strip --strip-unneeded "$OUT_DIR_ARM64/libnucleus_rodio.so" || true + ;; + *) + echo "ERROR: unsupported host arch '$HOST_ARCH'" >&2 + exit 1 + ;; +esac + +popd >/dev/null + +# ── Clear NativeLibraryLoader cache so fresh .so's are picked up ─────────── +for CACHE_DIR in "$HOME/.cache/nucleus/native"; do + if [ -d "$CACHE_DIR" ]; then + rm -rf "$CACHE_DIR" + echo "Cleared NativeLibraryLoader cache: $CACHE_DIR" + fi +done + +echo "Built Linux native library:" +case "$HOST_ARCH" in + x86_64) ls -lh "$OUT_DIR_X64"/libnucleus_rodio.so ;; + aarch64|arm64) ls -lh "$OUT_DIR_ARM64"/libnucleus_rodio.so ;; +esac \ No newline at end of file diff --git a/rodio/src/main/native/macos/build.sh b/rodio/src/main/native/macos/build.sh new file mode 100644 index 0000000..56f1bad --- /dev/null +++ b/rodio/src/main/native/macos/build.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Compiles the Rust JNI bridge (nucleus_rodio) into per-architecture macOS +# dylibs and copies them into the resource folders consumed by +# NativeLibraryLoader: +# src/main/resources/nucleus/native/{darwin-aarch64,darwin-x64}/libnucleus_rodio.dylib +# +# Prerequisites: +# - rustup with the targets aarch64-apple-darwin and x86_64-apple-darwin +# - Xcode command-line tools (clang) — only needed for `strip` +# Usage: ./build.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +NATIVE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +RESOURCE_DIR="$NATIVE_DIR/../resources/nucleus/native" +OUT_DIR_ARM64="$RESOURCE_DIR/darwin-aarch64" +OUT_DIR_X64="$RESOURCE_DIR/darwin-x64" + +mkdir -p "$OUT_DIR_ARM64" "$OUT_DIR_X64" + +if ! command -v cargo >/dev/null 2>&1; then + echo "ERROR: cargo not found. Install rustup from https://rustup.rs/" >&2 + exit 1 +fi + +rustup target add aarch64-apple-darwin >/dev/null +rustup target add x86_64-apple-darwin >/dev/null + +pushd "$NATIVE_DIR" >/dev/null + +cargo build --release --target aarch64-apple-darwin +cp "target/aarch64-apple-darwin/release/libnucleus_rodio.dylib" "$OUT_DIR_ARM64/libnucleus_rodio.dylib" +strip -x "$OUT_DIR_ARM64/libnucleus_rodio.dylib" + +cargo build --release --target x86_64-apple-darwin +cp "target/x86_64-apple-darwin/release/libnucleus_rodio.dylib" "$OUT_DIR_X64/libnucleus_rodio.dylib" +strip -x "$OUT_DIR_X64/libnucleus_rodio.dylib" + +popd >/dev/null + +# ── Clear NativeLibraryLoader cache so fresh dylibs are picked up ─────────── +# macOS cache lives under ~/Library/Caches/nucleus/native. +for CACHE_DIR in "$HOME/Library/Caches/nucleus/native" "$HOME/.cache/nucleus/native"; do + if [ -d "$CACHE_DIR" ]; then + rm -rf "$CACHE_DIR" + echo "Cleared NativeLibraryLoader cache: $CACHE_DIR" + fi +done + +echo "Built per-architecture dylibs:" +ls -lh "$OUT_DIR_ARM64"/libnucleus_rodio.dylib "$OUT_DIR_X64"/libnucleus_rodio.dylib \ No newline at end of file diff --git a/rodio/src/main/rust/error.rs b/rodio/src/main/native/src/error.rs similarity index 98% rename from rodio/src/main/rust/error.rs rename to rodio/src/main/native/src/error.rs index 0a3750a..996a422 100644 --- a/rodio/src/main/rust/error.rs +++ b/rodio/src/main/native/src/error.rs @@ -3,7 +3,7 @@ use std::error::Error as _; /// Errors that can occur when working with Rodio. -#[derive(Debug, thiserror::Error, uniffi::Error)] +#[derive(Debug, thiserror::Error)] pub enum RodioError { #[error("player {0} not found")] PlayerNotFound(u64), diff --git a/rodio/src/main/rust/lib.rs b/rodio/src/main/native/src/lib.rs similarity index 62% rename from rodio/src/main/rust/lib.rs rename to rodio/src/main/native/src/lib.rs index 4558a52..e04e8f4 100644 --- a/rodio/src/main/rust/lib.rs +++ b/rodio/src/main/native/src/lib.rs @@ -1,4 +1,11 @@ -//! Rodio Kotlin bindings via UniFFI. +//! nucleus_rodio — hand-written JNI bridge over the Rust `rodio` crate for the +//! `dev.nucleusframework.rodio` Kotlin library. Symbols follow the package +//! `dev.nucleusframework.rodio.internal.NativeRodioBridge`. +//! +//! Callbacks (event / metadata / error) are delivered to a Kotlin +//! `NativePlaybackCallbackRaw` instance pinned via a `GlobalRef`. Rodio invokes +//! the ICY-metadata read on its internal decoder thread, so every callback +//! dispatch attaches the current thread to the JVM before calling back. mod error; mod state; @@ -10,6 +17,9 @@ use std::time::Duration; use hls_m3u8::tags::VariantStream; use hls_m3u8::{MasterPlaylist, MediaPlaylist}; +use jni::objects::{JClass, JObject, JString, JValue}; +use jni::sys::{jboolean, jfloat, jint, jlong, JNI_FALSE, JNI_TRUE, JNI_VERSION_1_8}; +use jni::{JNIEnv, JavaVM}; use reqwest::blocking::{Client, ClientBuilder, Response}; use reqwest::header::{HeaderMap, CONTENT_TYPE, USER_AGENT}; use reqwest::Certificate; @@ -19,37 +29,83 @@ use rodio::source::SineWave; use rodio::Source; pub use error::RodioError; -use state::{register, unregister, with_player, with_player_mut, PlayerState}; +use state::{register, unregister, with_player, with_player_mut, CallbackRef, PlayerState}; const MAX_SEEKABLE_HTTP_BYTES: u64 = 25 * 1024 * 1024; // 25MB cap to avoid buffering huge streams -#[derive(Clone, Copy, Debug, uniffi::Enum)] +/// JVM handle captured in `JNI_OnLoad`. Each `CallbackRef` also carries its own +/// `JavaVM` (taken from the `JNIEnv` of `nativeSetCallback`) so callbacks work +/// even if `JNI_OnLoad` has not been observed for some reason. +static JAVA_VM: OnceLock = OnceLock::new(); + +/// Mirrors the ordinal of the Kotlin `PlaybackEvent` enum. +#[derive(Clone, Copy, Debug)] +#[repr(i32)] pub enum PlaybackEvent { - Connecting, - Playing, - Paused, - Stopped, + Connecting = 0, + Playing = 1, + Paused = 2, + Stopped = 3, } -#[uniffi::export(callback_interface)] -pub trait PlaybackCallback: Send + Sync { - fn on_event(&self, event: PlaybackEvent); - fn on_metadata(&self, key: String, value: String); - fn on_error(&self, message: String); +impl PlaybackEvent { + fn as_int(self) -> jint { + self as jint + } } -fn notify_event(callback: &Option>, event: PlaybackEvent) { - if let Some(callback) = callback { - callback.on_event(event); - } +#[no_mangle] +pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut std::ffi::c_void) -> jint { + let _ = JAVA_VM.set(vm); + JNI_VERSION_1_8 +} + +/// Throws `dev.nucleusframework.rodio.RodioException` with the error message. +/// The JVM raises the pending exception when the JNI export returns, so callers +/// must still return a safe default value. +fn throw(env: &mut JNIEnv, error: &RodioError) { + let _ = env.throw_new("dev/nucleusframework/rodio/RodioException", error.to_string()); } -fn notify_error(callback: &Option>, error: &RodioError) { - if let Some(callback) = callback { - callback.on_error(error.to_string()); +fn notify_event(callback: &Option>, event: PlaybackEvent) { + let Some(cb) = callback else { return }; + let Ok(mut env) = cb.vm.attach_current_thread_permanently() else { return }; + let _ = env.call_method( + cb.global.as_obj(), + "onEvent", + "(I)V", + &[JValue::Int(event.as_int())], + ); + if env.exception_check().unwrap_or(false) { + let _ = env.exception_clear(); } } +fn notify_error(callback: &Option>, error: &RodioError) { + let Some(cb) = callback else { return }; + let Ok(mut env) = cb.vm.attach_current_thread_permanently() else { return }; + let Ok(msg) = env.new_string(error.to_string()) else { return }; + let _ = env.call_method( + cb.global.as_obj(), + "onError", + "(Ljava/lang/String;)V", + &[JValue::Object(&msg)], + ); +} + +fn notify_metadata(callback: &Option>, key: String, value: String) { + let Some(cb) = callback else { return }; + let Ok(mut env) = cb.vm.attach_current_thread_permanently() else { return }; + let Ok(k) = env.new_string(key) else { return }; + let Ok(v) = env.new_string(value) else { return }; + let _ = env.call_method( + cb.global.as_obj(), + "onMetadata", + "(Ljava/lang/String;Ljava/lang/String;)V", + &[JValue::Object(&k), JValue::Object(&v)], + ); +} + fn reset_sink(state: &mut PlayerState) { state.sink.clear(); state.sink.play(); @@ -88,7 +144,7 @@ fn apply_http_options( builder } -fn player_callback(id: u64) -> Result>, RodioError> { +fn player_callback(id: u64) -> Result>, RodioError> { with_player(id, |state| Ok(state.callback.clone())) } @@ -96,11 +152,11 @@ struct IcyMetadataReader { inner: R, meta_interval: Option, remaining_until_meta: usize, - callback: Option>, + callback: Option>, } impl IcyMetadataReader { - fn new(inner: R, meta_interval: Option, callback: Option>) -> Self { + fn new(inner: R, meta_interval: Option, callback: Option>) -> Self { let interval = meta_interval.filter(|value| *value > 0); Self { inner, @@ -125,9 +181,7 @@ impl Read for IcyMetadataReader { let mut metadata = vec![0u8; meta_len]; self.inner.read_exact(&mut metadata)?; for (key, value) in parse_icy_metadata_block(&metadata) { - if let Some(callback) = &self.callback { - callback.on_metadata(key, value); - } + notify_metadata(&self.callback, key, value); } } self.remaining_until_meta = interval; @@ -152,7 +206,7 @@ impl StreamReader { fn new( response: Response, meta_interval: Option, - callback: Option>, + callback: Option>, ) -> Self { Self { inner: Mutex::new(IcyMetadataReader::new(response, meta_interval, callback)), @@ -757,45 +811,119 @@ fn play_hls_stream(id: u64, url: &str) -> Result<(), RodioError> { }) } -#[uniffi::export] -pub fn create_player() -> Result { - let (player, stream) = PlayerState::new()?; - Ok(register(player, stream)) +/// Reads a Kotlin `String` into a Rust `String`. On failure a JNI exception is +/// already pending, so callers should just return their safe default — the JVM +/// raises it when the export returns. +fn jstring(env: &mut JNIEnv, s: &JString) -> Option { + env.get_string(s).ok().map(|java_str| java_str.into()) } -#[uniffi::export] -pub fn create_player_with_buffer_size_frames(buffer_size_frames: u32) -> Result { - let (player, stream) = PlayerState::new_with_buffer_size_frames(buffer_size_frames)?; - Ok(register(player, stream)) +/// JNI exports. Symbols follow `dev.nucleusframework.rodio.internal.NativeRodioBridge`. +/// Argument mapping: `u64 id` ↔ `jlong`; `bool` ↔ `jboolean`; `f32` ↔ `jfloat`; +/// `u32` ↔ `jint`; `String` ↔ `JString`; `Option` duration ↔ `jlong` (`-1` = None). + +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeCreatePlayer( + mut env: JNIEnv, + _class: JClass, +) -> jlong { + match PlayerState::new() { + Ok((player, stream)) => register(player, stream) as jlong, + Err(e) => { + throw(&mut env, &e); + 0 + } + } } -#[uniffi::export] -pub fn destroy_player(id: u64) -> Result<(), RodioError> { - unregister(id) +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeCreatePlayerWithBufferSizeFrames( + mut env: JNIEnv, + _class: JClass, + buffer_size_frames: jint, +) -> jlong { + // Treat negative ints as invalid (the Kotlin API surface is unsigned). + let buffer_size_frames = buffer_size_frames.max(0) as u32; + match PlayerState::new_with_buffer_size_frames(buffer_size_frames) { + Ok((player, stream)) => register(player, stream) as jlong, + Err(e) => { + throw(&mut env, &e); + 0 + } + } } -#[uniffi::export] -pub fn player_set_callback( - id: u64, - callback: Box, -) -> Result<(), RodioError> { - with_player_mut(id, |state| { - state.callback = Some(Arc::from(callback)); +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeDestroyPlayer( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) { + if let Err(e) = unregister(id as u64) { + throw(&mut env, &e); + } +} + +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeSetCallback( + mut env: JNIEnv, + _class: JClass, + id: jlong, + callback: JObject, +) { + // Capture the VM from the calling JNIEnv (more robust than relying on JNI_OnLoad) + // and pin the callback with a GlobalRef so it survives across native threads. + let vm = match env.get_java_vm() { + Ok(vm) => vm, + Err(_) => return, + }; + let global = match env.new_global_ref(&callback) { + Ok(global) => global, + Err(_) => return, + }; + let callback_ref = Arc::new(CallbackRef { global, vm }); + if let Err(e) = with_player_mut(id as u64, |state| { + state.callback = Some(callback_ref); Ok(()) - }) + }) { + throw(&mut env, &e); + } } -#[uniffi::export] -pub fn player_clear_callback(id: u64) -> Result<(), RodioError> { - with_player_mut(id, |state| { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeClearCallback( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) { + if let Err(e) = with_player_mut(id as u64, |state| { state.callback = None; Ok(()) - }) + }) { + throw(&mut env, &e); + } } -#[uniffi::export] -pub fn player_play_file(id: u64, path: String, looped: bool) -> Result<(), RodioError> { - let callback = player_callback(id)?; +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativePlayFile( + mut env: JNIEnv, + _class: JClass, + id: jlong, + path: JString, + looped: jboolean, +) { + let path = match jstring(&mut env, &path) { + Some(path) => path, + None => return, + }; + let looped = looped != JNI_FALSE; + let callback = match player_callback(id as u64) { + Ok(callback) => callback, + Err(e) => { + throw(&mut env, &e); + return; + } + }; let result = (|| { let file = File::open(&path)?; let len = file.metadata()?.len(); @@ -805,69 +933,104 @@ pub fn player_play_file(id: u64, path: String, looped: bool) -> Result<(), Rodio if let Some(hint) = hint_from_url(&path) { builder = builder.with_hint(hint); } - let mut decoder = builder.build()?; - let seekable = decoder.try_seek(Duration::from_millis(0)).is_ok(); - let duration = if looped { - None + if looped { + // `repeat_infinite` yields no samples on some decoders (e.g. symphonia/ogg); + // `LoopedDecoder` seeks back to the start instead, which is also gapless. + let decoder = builder.build_looped()?; + with_player_mut(id as u64, |state| { + reset_sink(state); + state.current_duration = None; + state.seekable = false; + state.sink.append(decoder); + Ok(()) + }) } else { - decoder + let mut decoder = builder.build()?; + let seekable = decoder.try_seek(Duration::from_millis(0)).is_ok(); + let duration = decoder .total_duration() - .or_else(|| approximate_file_duration(&path)) - }; - with_player_mut(id, |state| { - reset_sink(state); - state.current_duration = duration; - state.seekable = seekable && !looped; - if looped { - state.sink.append(decoder.repeat_infinite()); - } else { + .or_else(|| approximate_file_duration(&path)); + with_player_mut(id as u64, |state| { + reset_sink(state); + state.current_duration = duration; + state.seekable = seekable; state.sink.append(decoder); - } - Ok(()) - }) + Ok(()) + }) + } })(); - if let Err(error) = &result { - notify_error(&callback, error); - } else { - notify_event(&callback, PlaybackEvent::Playing); + match &result { + Err(error) => notify_error(&callback, error), + Ok(()) => notify_event(&callback, PlaybackEvent::Playing), + } + if let Err(e) = &result { + throw(&mut env, e); } - result } -#[uniffi::export] -pub fn player_play_sine( - id: u64, - frequency_hz: f32, - duration_ms: u64, -) -> Result<(), RodioError> { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativePlaySine( + mut env: JNIEnv, + _class: JClass, + id: jlong, + frequency_hz: jfloat, + duration_ms: jlong, +) { + let frequency_hz = frequency_hz as f32; + let duration_ms = duration_ms as u64; if frequency_hz <= 0.0 { - return Err(RodioError::InvalidFrequency(frequency_hz)); + throw(&mut env, &RodioError::InvalidFrequency(frequency_hz)); + return; } if duration_ms == 0 { - return Err(RodioError::InvalidDuration(duration_ms)); + throw(&mut env, &RodioError::InvalidDuration(duration_ms)); + return; } - let callback = player_callback(id)?; + let callback = match player_callback(id as u64) { + Ok(callback) => callback, + Err(e) => { + throw(&mut env, &e); + return; + } + }; let duration = Duration::from_millis(duration_ms); - let result = with_player_mut(id, |state| { + let result = with_player_mut(id as u64, |state| { reset_sink(state); state.current_duration = Some(duration); state.seekable = false; - let source = SineWave::new(frequency_hz) - .take_duration(duration); + let source = SineWave::new(frequency_hz).take_duration(duration); state.sink.append(source); Ok(()) }); - if let Err(error) = &result { - notify_error(&callback, error); - } else { - notify_event(&callback, PlaybackEvent::Playing); + match &result { + Err(error) => notify_error(&callback, error), + Ok(()) => notify_event(&callback, PlaybackEvent::Playing), + } + if let Err(e) = &result { + throw(&mut env, e); } - result } -#[uniffi::export] -pub fn player_play_url(id: u64, url: String, looped: bool) -> Result<(), RodioError> { - let callback = player_callback(id)?; +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativePlayUrl( + mut env: JNIEnv, + _class: JClass, + id: jlong, + url: JString, + looped: jboolean, +) { + let url = match jstring(&mut env, &url) { + Some(url) => url, + None => return, + }; + let looped = looped != JNI_FALSE; + let callback = match player_callback(id as u64) { + Ok(callback) => callback, + Err(e) => { + throw(&mut env, &e); + return; + } + }; notify_event(&callback, PlaybackEvent::Connecting); let result = (|| { if looped { @@ -879,7 +1042,7 @@ pub fn player_play_url(id: u64, url: String, looped: bool) -> Result<(), RodioEr let bytes = download_bytes(&url)?; let cursor = Cursor::new(bytes); let decoder = Decoder::new_looped(cursor)?; - return with_player_mut(id, |state| { + return with_player_mut(id as u64, |state| { reset_sink(state); state.current_duration = None; state.sink.append(decoder); @@ -888,13 +1051,13 @@ pub fn player_play_url(id: u64, url: String, looped: bool) -> Result<(), RodioEr } if is_hls_playlist(&url, None) { - return play_hls_stream(id, &url); + return play_hls_stream(id as u64, &url); } let response = request_stream(&url, false)?; let content_type = response_content_type(&response); if is_hls_playlist(&url, content_type.as_deref()) { - return play_hls_stream(id, &url); + return play_hls_stream(id as u64, &url); } if let Some(len) = response.content_length() { if len > 0 @@ -902,14 +1065,14 @@ pub fn player_play_url(id: u64, url: String, looped: bool) -> Result<(), RodioEr && is_seekable_http_format(&url, content_type.as_deref()) { let bytes = response.bytes()?; - return play_seekable_http_bytes(id, &url, bytes.to_vec(), content_type.as_deref()); + return play_seekable_http_bytes(id as u64, &url, bytes.to_vec(), content_type.as_deref()); } } let meta_interval = icy_metaint(response.headers()); let reader = StreamReader::new(response, meta_interval, callback.clone()); let decoder = build_stream_decoder(reader, content_type.as_deref(), &url)?; let duration = decoder.total_duration(); - with_player_mut(id, |state| { + with_player_mut(id as u64, |state| { reset_sink(state); state.current_duration = duration; state.seekable = false; @@ -917,49 +1080,37 @@ pub fn player_play_url(id: u64, url: String, looped: bool) -> Result<(), RodioEr Ok(()) }) })(); - if let Err(error) = &result { - notify_error(&callback, error); - } else { - notify_event(&callback, PlaybackEvent::Playing); + match &result { + Err(error) => notify_error(&callback, error), + Ok(()) => notify_event(&callback, PlaybackEvent::Playing), + } + if let Err(e) = &result { + throw(&mut env, e); } - result -} - -#[uniffi::export] -pub fn http_set_allow_invalid_certs(allow: bool) -> Result<(), RodioError> { - let mut guard = http_options() - .lock() - .map_err(|_| RodioError::Internal("http options lock failed".to_string()))?; - guard.allow_invalid_certs = allow; - Ok(()) -} - -#[uniffi::export] -pub fn http_add_root_cert_pem(pem: String) -> Result<(), RodioError> { - let certs = Certificate::from_pem_bundle(pem.as_bytes())?; - let mut guard = http_options() - .lock() - .map_err(|_| RodioError::Internal("http options lock failed".to_string()))?; - guard.extra_roots.extend(certs); - Ok(()) -} - -#[uniffi::export] -pub fn http_clear_root_certs() -> Result<(), RodioError> { - let mut guard = http_options() - .lock() - .map_err(|_| RodioError::Internal("http options lock failed".to_string()))?; - guard.extra_roots.clear(); - Ok(()) } -#[uniffi::export] -pub fn player_play_radio(id: u64, url: String) -> Result<(), RodioError> { - let callback = player_callback(id)?; +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativePlayRadio( + mut env: JNIEnv, + _class: JClass, + id: jlong, + url: JString, +) { + let url = match jstring(&mut env, &url) { + Some(url) => url, + None => return, + }; + let callback = match player_callback(id as u64) { + Ok(callback) => callback, + Err(e) => { + throw(&mut env, &e); + return; + } + }; notify_event(&callback, PlaybackEvent::Connecting); let result = (|| { if is_hls_playlist(&url, None) { - return play_hls_stream(id, &url); + return play_hls_stream(id as u64, &url); } let mut response = request_stream(&url, true)?; @@ -967,7 +1118,7 @@ pub fn player_play_radio(id: u64, url: String) -> Result<(), RodioError> { let mut final_url = url.clone(); if is_hls_playlist(&final_url, content_type.as_deref()) { - return play_hls_stream(id, &final_url); + return play_hls_stream(id as u64, &final_url); } if is_playlist(&url, content_type.as_deref()) { @@ -975,26 +1126,24 @@ pub fn player_play_radio(id: u64, url: String) -> Result<(), RodioError> { let stream_url = resolve_playlist(&url, &body) .ok_or_else(|| RodioError::Playlist("playlist did not contain a stream url".to_string()))?; if is_hls_playlist(&stream_url, None) { - return play_hls_stream(id, &stream_url); + return play_hls_stream(id as u64, &stream_url); } response = request_stream(&stream_url, true)?; content_type = response_content_type(&response); final_url = stream_url; if is_hls_playlist(&final_url, content_type.as_deref()) { - return play_hls_stream(id, &final_url); + return play_hls_stream(id as u64, &final_url); } } - if let Some(callback) = &callback { - let headers = response.headers(); - for (key, header) in [ - ("icy-name", "icy-name"), - ("icy-description", "icy-description"), - ("icy-genre", "icy-genre"), - ] { - if let Some(value) = header_value(headers, header) { - callback.on_metadata(key.to_string(), value); - } + let headers = response.headers().clone(); + for (key, header) in [ + ("icy-name", "icy-name"), + ("icy-description", "icy-description"), + ("icy-genre", "icy-genre"), + ] { + if let Some(value) = header_value(&headers, header) { + notify_metadata(&callback, key.to_string(), value); } } @@ -1002,7 +1151,7 @@ pub fn player_play_radio(id: u64, url: String) -> Result<(), RodioError> { let reader = StreamReader::new(response, meta_interval, callback.clone()); let decoder = build_stream_decoder(reader, content_type.as_deref(), &final_url)?; let duration = decoder.total_duration(); - with_player_mut(id, |state| { + with_player_mut(id as u64, |state| { reset_sink(state); state.current_duration = duration; state.seekable = false; @@ -1010,77 +1159,139 @@ pub fn player_play_radio(id: u64, url: String) -> Result<(), RodioError> { Ok(()) }) })(); - if let Err(error) = &result { - notify_error(&callback, error); - } else { - notify_event(&callback, PlaybackEvent::Playing); + match &result { + Err(error) => notify_error(&callback, error), + Ok(()) => notify_event(&callback, PlaybackEvent::Playing), + } + if let Err(e) = &result { + throw(&mut env, e); } - result } -#[uniffi::export] -pub fn player_play(id: u64) -> Result<(), RodioError> { - let callback = with_player(id, |state| { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativePlay( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) { + let result = with_player(id as u64, |state| { state.sink.play(); Ok(state.callback.clone()) - })?; - notify_event(&callback, PlaybackEvent::Playing); - Ok(()) + }); + match result { + Ok(callback) => notify_event(&callback, PlaybackEvent::Playing), + Err(e) => throw(&mut env, &e), + } } -#[uniffi::export] -pub fn player_pause(id: u64) -> Result<(), RodioError> { - let callback = with_player(id, |state| { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativePause( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) { + let result = with_player(id as u64, |state| { state.sink.pause(); Ok(state.callback.clone()) - })?; - notify_event(&callback, PlaybackEvent::Paused); - Ok(()) + }); + match result { + Ok(callback) => notify_event(&callback, PlaybackEvent::Paused), + Err(e) => throw(&mut env, &e), + } } -#[uniffi::export] -pub fn player_stop(id: u64) -> Result<(), RodioError> { - let callback = with_player_mut(id, |state| { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeStop( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) { + let result = with_player_mut(id as u64, |state| { state.current_duration = None; state.seekable = false; state.sink.stop(); Ok(state.callback.clone()) - })?; - notify_event(&callback, PlaybackEvent::Stopped); - Ok(()) + }); + match result { + Ok(callback) => notify_event(&callback, PlaybackEvent::Stopped), + Err(e) => throw(&mut env, &e), + } } -#[uniffi::export] -pub fn player_clear(id: u64) -> Result<(), RodioError> { - let callback = with_player_mut(id, |state| { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeClear( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) { + let result = with_player_mut(id as u64, |state| { state.current_duration = None; state.seekable = false; state.sink.clear(); Ok(state.callback.clone()) - })?; - notify_event(&callback, PlaybackEvent::Stopped); - Ok(()) + }); + match result { + Ok(callback) => notify_event(&callback, PlaybackEvent::Stopped), + Err(e) => throw(&mut env, &e), + } } -#[uniffi::export] -pub fn player_is_paused(id: u64) -> Result { - with_player(id, |state| Ok(state.sink.is_paused())) +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeIsPaused( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) -> jboolean { + match with_player(id as u64, |state| Ok(state.sink.is_paused())) { + Ok(true) => JNI_TRUE, + Ok(false) => JNI_FALSE, + Err(e) => { + throw(&mut env, &e); + JNI_FALSE + } + } } -#[uniffi::export] -pub fn player_is_empty(id: u64) -> Result { - with_player(id, |state| Ok(state.sink.empty())) +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeIsEmpty( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) -> jboolean { + match with_player(id as u64, |state| Ok(state.sink.empty())) { + Ok(true) => JNI_TRUE, + Ok(false) => JNI_FALSE, + Err(e) => { + throw(&mut env, &e); + JNI_FALSE + } + } } -#[uniffi::export] -pub fn player_get_position_ms(id: u64) -> Result { - with_player(id, |state| Ok(duration_to_millis(state.sink.get_pos()))) +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeGetPositionMs( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) -> jlong { + match with_player(id as u64, |state| Ok(duration_to_millis(state.sink.get_pos()))) { + Ok(ms) => ms as jlong, + Err(e) => { + throw(&mut env, &e); + 0 + } + } } -#[uniffi::export] -pub fn player_seek_position_ms(id: u64, position_ms: u64) -> Result<(), RodioError> { - let target = Duration::from_millis(position_ms); - with_player_mut(id, |state| { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeSeekPositionMs( + mut env: JNIEnv, + _class: JClass, + id: jlong, + position_ms: jlong, +) { + let target = Duration::from_millis(position_ms.max(0) as u64); + let result = with_player_mut(id as u64, |state| { let duration = state .current_duration .ok_or_else(|| RodioError::Seek("duration unknown or not seekable".to_string()))?; @@ -1090,28 +1301,119 @@ pub fn player_seek_position_ms(id: u64, position_ms: u64) -> Result<(), RodioErr } state.sink.try_seek(clamped)?; Ok(()) - }) + }); + if let Err(e) = result { + throw(&mut env, &e); + } } -#[uniffi::export] -pub fn player_get_duration_ms(id: u64) -> Result, RodioError> { - with_player(id, |state| Ok(state.current_duration.map(duration_to_millis))) +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeGetDurationMs( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) -> jlong { + // `-1` signals "no duration" (None) to Kotlin. + match with_player(id as u64, |state| Ok(state.current_duration.map(duration_to_millis))) { + Ok(Some(ms)) => ms as jlong, + Ok(None) => -1, + Err(e) => { + throw(&mut env, &e); + -1 + } + } } -#[uniffi::export] -pub fn player_is_seekable(id: u64) -> Result { - with_player(id, |state| Ok(state.seekable)) +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeIsSeekable( + mut env: JNIEnv, + _class: JClass, + id: jlong, +) -> jboolean { + match with_player(id as u64, |state| Ok(state.seekable)) { + Ok(true) => JNI_TRUE, + Ok(false) => JNI_FALSE, + Err(e) => { + throw(&mut env, &e); + JNI_FALSE + } + } } -#[uniffi::export] -pub fn player_set_volume(id: u64, volume: f32) -> Result<(), RodioError> { +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeSetVolume( + mut env: JNIEnv, + _class: JClass, + id: jlong, + volume: jfloat, +) { + let volume = volume as f32; if volume < 0.0 { - return Err(RodioError::InvalidVolume(volume)); + throw(&mut env, &RodioError::InvalidVolume(volume)); + return; } - with_player(id, |state| { + let result = with_player(id as u64, |state| { state.sink.set_volume(volume); Ok(()) - }) + }); + if let Err(e) = result { + throw(&mut env, &e); + } } -uniffi::setup_scaffolding!(); +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeHttpSetAllowInvalidCerts( + mut env: JNIEnv, + _class: JClass, + allow: jboolean, +) { + let allow = allow != JNI_FALSE; + let result = http_options() + .lock() + .map(|mut guard| { + guard.allow_invalid_certs = allow; + }) + .map_err(|_| RodioError::Internal("http options lock failed".to_string())); + if let Err(e) = result { + throw(&mut env, &e); + } +} + +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeHttpAddRootCertPem( + mut env: JNIEnv, + _class: JClass, + pem: JString, +) { + let pem = match jstring(&mut env, &pem) { + Some(pem) => pem, + None => return, + }; + let result = (|| { + let certs = Certificate::from_pem_bundle(pem.as_bytes())?; + let mut guard = http_options() + .lock() + .map_err(|_| RodioError::Internal("http options lock failed".to_string()))?; + guard.extra_roots.extend(certs); + Ok(()) + })(); + if let Err(e) = result { + throw(&mut env, &e); + } +} + +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_rodio_internal_NativeRodioBridge_nativeHttpClearRootCerts( + mut env: JNIEnv, + _class: JClass, +) { + let result = http_options() + .lock() + .map(|mut guard| { + guard.extra_roots.clear(); + }) + .map_err(|_| RodioError::Internal("http options lock failed".to_string())); + if let Err(e) = result { + throw(&mut env, &e); + } +} diff --git a/rodio/src/main/rust/state.rs b/rodio/src/main/native/src/state.rs similarity index 87% rename from rodio/src/main/rust/state.rs rename to rodio/src/main/native/src/state.rs index 39234d8..2695730 100644 --- a/rodio/src/main/rust/state.rs +++ b/rodio/src/main/native/src/state.rs @@ -6,14 +6,25 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; +use jni::objects::GlobalRef; +use jni::JavaVM; use rodio::{cpal::BufferSize, OutputStream, OutputStreamBuilder, Sink}; use crate::error::RodioError; -use crate::PlaybackCallback; + +/// A Kotlin `NativePlaybackCallbackRaw` instance pinned across threads. +/// +/// The `GlobalRef` keeps the JVM-side callback alive and the `JavaVM` lets us +/// attach the current (possibly non-JVM) thread — rodio invokes callbacks from +/// its internal decoder/audio thread, which is never attached by default. +pub struct CallbackRef { + pub global: GlobalRef, + pub vm: JavaVM, +} pub struct PlayerState { pub sink: Sink, - pub callback: Option>, + pub callback: Option>, pub current_duration: Option, pub seekable: bool, } diff --git a/rodio/src/main/native/windows/build.bat b/rodio/src/main/native/windows/build.bat new file mode 100644 index 0000000..576b1ee --- /dev/null +++ b/rodio/src/main/native/windows/build.bat @@ -0,0 +1,80 @@ +@echo off +REM Builds the Rust JNI bridge (nucleus_rodio) into Windows DLLs and copies +REM them into the per-architecture resource folders consumed by +REM NativeLibraryLoader: +REM src\main\resources\nucleus\native\{win32-x64,win32-aarch64}\nucleus_rodio.dll +REM +REM Prerequisites: +REM - rustup with x86_64-pc-windows-msvc (and aarch64-pc-windows-msvc) targets +REM - Visual Studio 2019/2022 Build Tools +REM Usage: build.bat + +setlocal enabledelayedexpansion + +set "SCRIPT_DIR=%~dp0" +set "NATIVE_DIR=%SCRIPT_DIR%.." +set "RESOURCE_DIR=%NATIVE_DIR%\..\resources\nucleus\native" +set "OUT_DIR_X64=%RESOURCE_DIR%\win32-x64" +set "OUT_DIR_ARM64=%RESOURCE_DIR%\win32-aarch64" + +if not exist "%OUT_DIR_X64%" mkdir "%OUT_DIR_X64%" +if not exist "%OUT_DIR_ARM64%" mkdir "%OUT_DIR_ARM64%" + +where cargo >nul 2>&1 +if errorlevel 1 ( + echo ERROR: cargo not found in PATH. Install rustup from https://rustup.rs/ >&2 + exit /b 1 +) + +REM =========================================================================== +REM 1) Rust crate (nucleus_rodio.dll) — x64 +REM =========================================================================== +echo. +echo === Building nucleus_rodio.dll (x64) === +pushd "%NATIVE_DIR%" +rustup target add x86_64-pc-windows-msvc >nul 2>&1 +cargo build --release --target x86_64-pc-windows-msvc +if errorlevel 1 ( + echo ERROR: cargo build x64 failed >&2 + popd + exit /b 1 +) +copy /Y "target\x86_64-pc-windows-msvc\release\nucleus_rodio.dll" "%OUT_DIR_X64%\nucleus_rodio.dll" >nul +popd + +REM =========================================================================== +REM 2) Rust crate (nucleus_rodio.dll) — ARM64 (optional; needs the ARM64 toolchain) +REM =========================================================================== +echo. +echo === Building nucleus_rodio.dll (ARM64) === +pushd "%NATIVE_DIR%" +rustup target add aarch64-pc-windows-msvc >nul 2>&1 +cargo build --release --target aarch64-pc-windows-msvc +if errorlevel 1 ( + echo WARNING: cargo build ARM64 failed - skipping ARM64. >&2 + set "SKIP_ARM64=1" +) else ( + copy /Y "target\aarch64-pc-windows-msvc\release\nucleus_rodio.dll" "%OUT_DIR_ARM64%\nucleus_rodio.dll" >nul +) +popd + +REM =========================================================================== +REM Clear NativeLibraryLoader cache so fresh DLLs are picked up +REM =========================================================================== +if exist "%USERPROFILE%\.cache\nucleus\native" ( + rmdir /s /q "%USERPROFILE%\.cache\nucleus\native" + echo Cleared NativeLibraryLoader cache: %USERPROFILE%\.cache\nucleus\native +) +if exist "%LOCALAPPDATA%\nucleus\native" ( + rmdir /s /q "%LOCALAPPDATA%\nucleus\native" + echo Cleared NativeLibraryLoader cache: %LOCALAPPDATA%\nucleus\native +) + +echo. +echo Built DLLs: +if exist "%OUT_DIR_X64%\nucleus_rodio.dll" echo %OUT_DIR_X64%\nucleus_rodio.dll +if exist "%OUT_DIR_ARM64%\nucleus_rodio.dll" echo %OUT_DIR_ARM64%\nucleus_rodio.dll + +exit /b 0 + +endlocal \ No newline at end of file diff --git a/rodio/src/test/kotlin/dev/nucleusframework/rodio/RodioBridgeSmokeTest.kt b/rodio/src/test/kotlin/dev/nucleusframework/rodio/RodioBridgeSmokeTest.kt new file mode 100644 index 0000000..37b5d58 --- /dev/null +++ b/rodio/src/test/kotlin/dev/nucleusframework/rodio/RodioBridgeSmokeTest.kt @@ -0,0 +1,78 @@ +package dev.nucleusframework.rodio + +import kotlinx.coroutines.runBlocking +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Smoke test for the hand-written JNI bridge: validates that the native + * library is extracted/loaded by NativeLibraryLoader, that every JNI symbol + * resolves, that `nativeSetCallback` pins the callback via a GlobalRef and + * that rodio dispatches `PlaybackEvent` back across the boundary. + * + * Uses `playSine` so it needs no audio file on disk. Runs on the JVM only — + * the native backend is JVM-only, so this test is skipped on non-JVM targets + * by virtue of living under `jvmMain`-equivalent test sources (kotlin("test")). + */ +class RodioBridgeSmokeTest { + private lateinit var player: RodioPlayer + private val events = mutableListOf() + + @BeforeTest + fun setUp() { + player = RodioPlayer() + player.setCallback( + object : PlaybackCallback { + override fun onEvent(event: PlaybackEvent) { + synchronized(events) { events += event } + } + + override fun onMetadata(key: String, value: String) {} + override fun onError(message: String) {} + }, + ) + } + + @AfterTest + fun tearDown() { + player.close() + } + + @Test + fun sinePlaybackDispatchesPlayingEvent() { + // playSine notifies PLAYING synchronously on the calling thread once + // the source is appended, so the event is recorded before return. + player.playSine(440f, 300L) + // Give the audio thread a moment to start, then assert state. + Thread.sleep(100) + assertTrue(events.contains(PlaybackEvent.PLAYING), "expected PLAYING event, got $events") + assertEquals(false, player.isPaused()) + } + + @Test + fun volumeAndSeekableStateAreReadable() { + player.setVolume(0.5f) + player.playSine(220f, 200L) + Thread.sleep(100) + // A sine source is not seekable and has no advanceable duration beyond + // the take_duration window — just assert no exception and paused=false. + assertEquals(false, player.isPaused()) + assertEquals(false, player.isSeekable()) + } + + @Test + fun urlLoopHlsIsRejectedWithRodioException() { + // hls + loop is unsupported by design — validates error path throws + // RodioException across the JNI boundary instead of aborting. + var threw = false + try { + runBlocking { player.playUrlAsync("https://example.com/stream.m3u8", loop = true) } + } catch (_: RodioException) { + threw = true + } + assertTrue(threw, "expected RodioException for hls looped playback") + } +} \ No newline at end of file diff --git a/rodio/src/test/kotlin/io/github/kdroidfilter/rodio/HttpPlaybackIntegrationTest.kt b/rodio/src/test/kotlin/io/github/kdroidfilter/rodio/HttpPlaybackIntegrationTest.kt deleted file mode 100644 index ff61c87..0000000 --- a/rodio/src/test/kotlin/io/github/kdroidfilter/rodio/HttpPlaybackIntegrationTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -package io.github.kdroidfilter.rodio - -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.selects.select -import kotlinx.coroutines.withTimeoutOrNull -import org.junit.Assume.assumeTrue -import kotlin.test.Test -import kotlin.test.assertTrue -import kotlin.test.fail - -private val HTTP_SOURCES = listOf( - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.aac", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.ac3", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.aiff", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.flac", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.m4a", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.mp3", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.wav", - "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.wma", -) - -private val SUPPORTED_EXTENSIONS = setOf( - "aac", - "aiff", - "aif", - "flac", - "m4a", - "mp3", - "mp4", - "ogg", - "opus", - "wav", -) - -private sealed interface PlaybackOutcome { - data class Playing(val durationMs: Long?, val seekable: Boolean) : PlaybackOutcome - data class Error(val message: String) : PlaybackOutcome - object Timeout : PlaybackOutcome -} - -private class PlaybackObserver : PlaybackCallback { - val events = Channel(Channel.UNLIMITED) - val errors = Channel(Channel.UNLIMITED) - val metadata = Channel>(Channel.UNLIMITED) - - override fun onEvent(event: PlaybackEvent) { - events.trySend(event) - } - - override fun onMetadata(key: String, value: String) { - metadata.trySend(key to value) - } - - override fun onError(message: String) { - errors.trySend(message) - } -} - -class HttpPlaybackIntegrationTest { - @Test - fun playback_and_seek_when_available() = runBlocking { - assumeTrue(shouldRunHttpTests()) - for (url in HTTP_SOURCES) { - val outcome = playOnce(url, performSeek = true) - when (outcome) { - is PlaybackOutcome.Playing -> { - if (outcome.seekable && outcome.durationMs != null && outcome.durationMs > 0) { - assertTrue(true, "Seek verified for $url") - } - } - is PlaybackOutcome.Error -> assertTrue( - outcome.message.isNotBlank(), - "Expected an error message for $url", - ) - PlaybackOutcome.Timeout -> fail("Timeout while waiting for playback start for $url") - } - } - } - - @Test - fun metadata_callback_collects_without_crashing() = runBlocking { - assumeTrue(shouldRunHttpTests()) - val source = HTTP_SOURCES.first() - val observer = PlaybackObserver() - val player = RodioPlayer() - player.setCallback(observer) - try { - player.playUrlAsync(source, loop = false) - when (val outcome = observer.awaitOutcome(player, timeoutMs = 15_000)) { - is PlaybackOutcome.Playing -> { - // Drain any metadata events; these static files are expected to emit none. - val metadataPairs = observer.metadata.drain() - assertTrue(metadataPairs.isEmpty(), "Expected no metadata for $source but got $metadataPairs") - } - is PlaybackOutcome.Error -> fail("Playback error for $source: ${outcome.message}") - PlaybackOutcome.Timeout -> fail("Timeout while waiting for playback start for $source") - } - } finally { - player.stop() - player.clearCallback() - player.close() - } - } - - private suspend fun playOnce(url: String, performSeek: Boolean): PlaybackOutcome { - if (!isLikelySupported(url)) { - return PlaybackOutcome.Error("format not supported for test; skipped playback") - } - val observer = PlaybackObserver() - val player = RodioPlayer() - player.setCallback(observer) - try { - val startResult = runCatching { player.playUrlAsync(url, loop = false) } - if (startResult.isFailure) { - return PlaybackOutcome.Error(startResult.exceptionOrNull()?.message ?: "playUrl failed") - } - val outcome = observer.awaitOutcome(player, timeoutMs = 20_000) - if (outcome is PlaybackOutcome.Playing && performSeek && outcome.seekable && outcome.durationMs != null) { - val duration = outcome.durationMs - val target = (duration / 2).coerceAtLeast(1_000L) - val seekResult = runCatching { player.seekToMs(target) } - if (seekResult.isFailure) { - return PlaybackOutcome.Error("seek failed for $url: ${seekResult.exceptionOrNull()?.message}") - } - delay(750) - val position = player.getPositionMs() - val tolerance = 2_000L - if (position !in (target - tolerance)..(target + tolerance)) { - return PlaybackOutcome.Error("seek position for $url should be near $target ms but was $position ms") - } - } - return outcome - } finally { - player.stop() - player.clearCallback() - player.close() - } - } -} - -private fun isLikelySupported(url: String): Boolean { - val extension = url.substringAfterLast('.', missingDelimiterValue = "").lowercase() - return extension in SUPPORTED_EXTENSIONS -} - -private fun shouldRunHttpTests(): Boolean = - System.getenv("RUN_HTTP_PLAYBACK_TESTS") == "1" - -private suspend fun PlaybackObserver.awaitOutcome( - player: RodioPlayer, - timeoutMs: Long, -): PlaybackOutcome { - val result = withTimeoutOrNull(timeoutMs) { - while (true) { - val outcome = select { - events.onReceive { event -> - when (event) { - PlaybackEvent.PLAYING -> PlaybackOutcome.Playing(player.getDurationMs(), player.isSeekable()) - PlaybackEvent.STOPPED -> PlaybackOutcome.Error("Playback stopped unexpectedly") - else -> null - } - } - errors.onReceive { message -> - PlaybackOutcome.Error(message) - } - } - if (outcome != null) return@withTimeoutOrNull outcome - } - null - } - return result ?: PlaybackOutcome.Timeout -} - -private fun Channel.drain(): List { - val items = mutableListOf() - while (true) { - val result = tryReceive().getOrNull() ?: break - items += result - } - return items -} diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index 5fd1a11..99fa56b 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -18,7 +18,6 @@ kotlin { implementation(compose.material) implementation(libs.kotlinx.coroutines.core) implementation(project(":rodio")) - implementation(project(":souvlaki")) } jvmMain.dependencies { diff --git a/sample/composeApp/src/jvmMain/kotlin/sample/app/App.kt b/sample/composeApp/src/jvmMain/kotlin/sample/app/App.kt index 7929fe3..e22896a 100644 --- a/sample/composeApp/src/jvmMain/kotlin/sample/app/App.kt +++ b/sample/composeApp/src/jvmMain/kotlin/sample/app/App.kt @@ -36,22 +36,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import io.github.kdroidfilter.rodio.RodioPlayer -import io.github.kdroidfilter.rodio.native.PlaybackCallback -import io.github.kdroidfilter.rodio.native.PlaybackEvent -import io.github.kdroidfilter.souvlaki.MediaControls -import io.github.kdroidfilter.souvlaki.MediaControlCallback -import io.github.kdroidfilter.souvlaki.MediaControlEventData -import io.github.kdroidfilter.souvlaki.MediaControlEventType -import io.github.kdroidfilter.souvlaki.MediaMetadata -import io.github.kdroidfilter.souvlaki.PlaybackStatus +import dev.nucleusframework.rodio.PlaybackCallback +import dev.nucleusframework.rodio.PlaybackEvent +import dev.nucleusframework.rodio.RodioPlayer import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.awt.FileDialog import java.awt.Frame import java.io.File -private enum class SourceTab { File, Stream, MediaControls } +private enum class SourceTab { File, Stream } @Composable fun App() { @@ -67,15 +61,6 @@ fun App() { var seekable by remember { mutableStateOf(false) } var volume by remember { mutableStateOf(1f) } - // Souvlaki Media Controls state - var mediaControlsInstance by remember { mutableStateOf(null) } - var mediaControlsEnabled by remember { mutableStateOf(false) } - var lastMediaEvent by remember { mutableStateOf(null) } - var trackTitle by remember { mutableStateOf("Sample Track") } - var trackArtist by remember { mutableStateOf("Sample Artist") } - var trackAlbum by remember { mutableStateOf("Sample Album") } - var currentPlaybackStatus by remember { mutableStateOf(PlaybackStatus.STOPPED) } - val accentColor = Color(0xFF2563EB) val tabBackground = Color(0xFFEFF3FB) @@ -104,13 +89,6 @@ fun App() { } } - // Cleanup media controls on dispose - DisposableEffect(Unit) { - onDispose { - mediaControlsInstance?.close() - } - } - LaunchedEffect(player) { while (true) { positionMs = player.getPositionMs() @@ -133,7 +111,6 @@ fun App() { val hasSource = when (activeTab) { SourceTab.File -> filePath.isNotBlank() SourceTab.Stream -> streamUrl.isNotBlank() - SourceTab.MediaControls -> true } Box( @@ -182,7 +159,6 @@ fun App() { text = when (tab) { SourceTab.File -> "File" SourceTab.Stream -> "Stream" - SourceTab.MediaControls -> "Media Controls" }, style = TextStyle(color = if (selected) accentColor else Color(0xFF6B7280)), ) @@ -227,182 +203,6 @@ fun App() { .padding(10.dp), ) } - SourceTab.MediaControls -> { - BasicText("Souvlaki Media Controls Test", style = TextStyle(color = Color.Black)) - Spacer(modifier = Modifier.height(12.dp)) - - // Enable/Disable controls - Row(verticalAlignment = Alignment.CenterVertically) { - Button( - onClick = { - if (mediaControlsEnabled) { - mediaControlsInstance?.close() - mediaControlsInstance = null - mediaControlsEnabled = false - lastMediaEvent = null - } else { - // Launch in IO dispatcher to avoid blocking UI on macOS - scope.launch(kotlinx.coroutines.Dispatchers.IO) { - runCatching { - val controls = MediaControls.create( - dbusName = "rodiokt_sample", - displayName = "RodioKt Sample" - ) - controls.attach { event -> - scope.launch { - lastMediaEvent = when (event.eventType) { - MediaControlEventType.PLAY -> "Play" - MediaControlEventType.PAUSE -> "Pause" - MediaControlEventType.TOGGLE -> "Toggle" - MediaControlEventType.NEXT -> "Next" - MediaControlEventType.PREVIOUS -> "Previous" - MediaControlEventType.STOP -> "Stop" - MediaControlEventType.SEEK -> "Seek ${if (event.seekForward == true) "Forward" else "Backward"}" - MediaControlEventType.SEEK_BY -> "SeekBy ${event.seekOffsetSecs}s" - MediaControlEventType.SET_POSITION -> "SetPosition ${event.positionSecs}s" - MediaControlEventType.SET_VOLUME -> "SetVolume ${event.volume}" - MediaControlEventType.OPEN_URI -> "OpenUri ${event.uri}" - MediaControlEventType.RAISE -> "Raise" - MediaControlEventType.QUIT -> "Quit" - } - } - } - // Set initial metadata and playback status - controls.setMetadata( - title = trackTitle, - artist = trackArtist, - album = trackAlbum, - durationSecs = 180.0 - ) - controls.setPlayback(PlaybackStatus.PAUSED) - currentPlaybackStatus = PlaybackStatus.PAUSED - mediaControlsInstance = controls - mediaControlsEnabled = true - }.onFailure { - println("Failed to create media controls: ${it.message}") - } - } - } - }, - colors = ButtonDefaults.buttonColors( - backgroundColor = if (mediaControlsEnabled) Color(0xFF10B981) else accentColor - ), - ) { - BasicText(if (mediaControlsEnabled) "Disable Controls" else "Enable Controls") - } - } - Spacer(modifier = Modifier.height(12.dp)) - - if (mediaControlsEnabled) { - // Track metadata inputs - BasicText("Track Title", style = TextStyle(color = Color.Gray)) - BasicTextField( - value = trackTitle, - onValueChange = { trackTitle = it }, - singleLine = true, - textStyle = TextStyle(color = Color.Black), - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFFF2F2F2)) - .padding(10.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - - BasicText("Artist", style = TextStyle(color = Color.Gray)) - BasicTextField( - value = trackArtist, - onValueChange = { trackArtist = it }, - singleLine = true, - textStyle = TextStyle(color = Color.Black), - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFFF2F2F2)) - .padding(10.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - - BasicText("Album", style = TextStyle(color = Color.Gray)) - BasicTextField( - value = trackAlbum, - onValueChange = { trackAlbum = it }, - singleLine = true, - textStyle = TextStyle(color = Color.Black), - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFFF2F2F2)) - .padding(10.dp), - ) - Spacer(modifier = Modifier.height(12.dp)) - - // Update metadata button - Button( - onClick = { - mediaControlsInstance?.setMetadata( - MediaMetadata( - title = trackTitle.takeIf { it.isNotBlank() }, - artist = trackArtist.takeIf { it.isNotBlank() }, - album = trackAlbum.takeIf { it.isNotBlank() }, - durationSecs = 180.0 - ) - ) - }, - ) { - BasicText("Update Metadata") - } - Spacer(modifier = Modifier.height(12.dp)) - - // Playback status buttons - BasicText("Set Playback Status:", style = TextStyle(color = Color.Gray)) - Spacer(modifier = Modifier.height(8.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button( - onClick = { - currentPlaybackStatus = PlaybackStatus.PLAYING - mediaControlsInstance?.setPlayback(PlaybackStatus.PLAYING) - }, - colors = ButtonDefaults.buttonColors( - backgroundColor = if (currentPlaybackStatus == PlaybackStatus.PLAYING) Color(0xFF10B981) else Color(0xFFEDEDED) - ), - ) { - BasicText("Playing") - } - Button( - onClick = { - currentPlaybackStatus = PlaybackStatus.PAUSED - mediaControlsInstance?.setPlayback(PlaybackStatus.PAUSED) - }, - colors = ButtonDefaults.buttonColors( - backgroundColor = if (currentPlaybackStatus == PlaybackStatus.PAUSED) Color(0xFFF59E0B) else Color(0xFFEDEDED) - ), - ) { - BasicText("Paused") - } - Button( - onClick = { - currentPlaybackStatus = PlaybackStatus.STOPPED - mediaControlsInstance?.setPlayback(PlaybackStatus.STOPPED) - }, - colors = ButtonDefaults.buttonColors( - backgroundColor = if (currentPlaybackStatus == PlaybackStatus.STOPPED) Color(0xFFEF4444) else Color(0xFFEDEDED) - ), - ) { - BasicText("Stopped") - } - } - Spacer(modifier = Modifier.height(16.dp)) - - // Last received event - BasicText( - "Last Event: ${lastMediaEvent ?: "None"}", - style = TextStyle(color = Color.DarkGray) - ) - } else { - BasicText( - "Click 'Enable Controls' to start media controls.\nOn Linux, you can use playerctl or your DE's media keys to test.", - style = TextStyle(color = Color.Gray) - ) - } - } } Spacer(modifier = Modifier.height(16.dp)) @@ -489,9 +289,6 @@ fun App() { .onFailure { println("Stream error: ${it.message}") } } } - SourceTab.MediaControls -> { - // MediaControls tab doesn't use rodio player - } } }, enabled = hasSource, @@ -530,4 +327,4 @@ private fun formatTime(ms: Long): String { val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 return "%d:%02d".format(minutes, seconds) -} +} \ No newline at end of file diff --git a/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt b/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt index e0d8e08..417c53e 100644 --- a/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt +++ b/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt @@ -2,7 +2,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -import io.github.kdroidfilter.souvlaki.MediaControlsConfig import sample.app.App import java.awt.Dimension @@ -13,8 +12,6 @@ fun main() = application { onCloseRequest = ::exitApplication, ) { window.minimumSize = Dimension(350, 600) - // Initialize MediaControls with the window (required on Windows) - MediaControlsConfig.init(window) App() } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3df1254..4f38821 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,11 +3,11 @@ rootProject.name = "RodioKt" pluginManagement { repositories { google { - content { - includeGroupByRegex("com\\.android.*") - includeGroupByRegex("com\\.google.*") - includeGroupByRegex("androidx.*") - includeGroupByRegex("android.*") + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + includeGroupByRegex("android.*") } } gradlePluginPortal() @@ -17,18 +17,17 @@ pluginManagement { dependencyResolutionManagement { repositories { + mavenLocal() google { - content { - includeGroupByRegex("com\\.android.*") - includeGroupByRegex("com\\.google.*") - includeGroupByRegex("androidx.*") - includeGroupByRegex("android.*") + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + includeGroupByRegex("android.*") } } mavenCentral() } } include(":rodio") -include(":souvlaki") -include(":sample:composeApp") - +include(":sample:composeApp") \ No newline at end of file diff --git a/souvlaki/Cargo.lock b/souvlaki/Cargo.lock deleted file mode 100644 index 1a3ee86..0000000 --- a/souvlaki/Cargo.lock +++ /dev/null @@ -1,2045 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "askama" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" -dependencies = [ - "askama_derive", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" -dependencies = [ - "askama_parser", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash", - "serde", - "serde_derive", - "syn 2.0.114", -] - -[[package]] -name = "askama_parser" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow 0.7.14", -] - -[[package]] -name = "async-broadcast" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" -dependencies = [ - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.3.0", - "futures-lite 2.6.1", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "blocking", - "futures-lite 1.13.0", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.6.1", - "parking", - "polling 3.11.0", - "rustix 1.1.3", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.44", - "windows-sys 0.48.0", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io 2.6.0", - "async-lock 3.4.2", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.3", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite 2.6.1", - "piper", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cocoa" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "dbus" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "dbus-crossroads" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" -dependencies = [ - "dbus", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand 2.3.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "goblin" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" -dependencies = [ - "log", - "plain", - "scroll", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "libc" -version = "0.2.179" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand 2.3.0", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[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 0.2.16", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "scroll" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "souvlaki" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" -dependencies = [ - "base64", - "block", - "cocoa", - "core-graphics", - "dbus", - "dbus-crossroads", - "dispatch", - "objc", - "pollster", - "thiserror 1.0.69", - "windows", - "zbus", - "zvariant", -] - -[[package]] -name = "souvlaki-kt" -version = "0.1.0" -dependencies = [ - "souvlaki", - "thiserror 2.0.17", - "uniffi", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand 2.3.0", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "smawk", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset 0.9.1", - "tempfile", - "winapi", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "uniffi" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" -dependencies = [ - "anyhow", - "cargo_metadata", - "uniffi_bindgen", - "uniffi_core", - "uniffi_macros", - "uniffi_pipeline", -] - -[[package]] -name = "uniffi_bindgen" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" -dependencies = [ - "anyhow", - "askama", - "camino", - "cargo_metadata", - "fs-err", - "glob", - "goblin", - "heck", - "indexmap", - "once_cell", - "serde", - "tempfile", - "textwrap", - "toml", - "uniffi_internal_macros", - "uniffi_meta", - "uniffi_pipeline", - "uniffi_udl", -] - -[[package]] -name = "uniffi_core" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" -dependencies = [ - "anyhow", - "bytes", - "once_cell", - "static_assertions", -] - -[[package]] -name = "uniffi_internal_macros" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" -dependencies = [ - "anyhow", - "indexmap", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "uniffi_macros" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" -dependencies = [ - "camino", - "fs-err", - "once_cell", - "proc-macro2", - "quote", - "serde", - "syn 2.0.114", - "toml", - "uniffi_meta", -] - -[[package]] -name = "uniffi_meta" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" -dependencies = [ - "anyhow", - "siphasher", - "uniffi_internal_macros", - "uniffi_pipeline", -] - -[[package]] -name = "uniffi_pipeline" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "tempfile", - "uniffi_internal_macros", -] - -[[package]] -name = "uniffi_udl" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" -dependencies = [ - "anyhow", - "textwrap", - "uniffi_meta", - "weedle2", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "weedle2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" -dependencies = [ - "nom", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "zbus" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "byteorder", - "derivative", - "enumflags2", - "event-listener 2.5.3", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "once_cell", - "ordered-stream", - "rand", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "winapi", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zmij" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" - -[[package]] -name = "zvariant" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] diff --git a/souvlaki/Cargo.toml b/souvlaki/Cargo.toml deleted file mode 100644 index 7ba9fd1..0000000 --- a/souvlaki/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "souvlaki-kt" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -name = "souvlaki_kt" -crate-type = ["cdylib", "staticlib"] -path = "src/main/rust/lib.rs" - -[dependencies] -thiserror = "2.0.11" -uniffi = "0.29.4" - -[target.'cfg(target_os = "linux")'.dependencies] -souvlaki = { version = "0.8.3", default-features = false, features = ["use_zbus"] } - -[target.'cfg(target_os = "macos")'.dependencies] -souvlaki = "0.8.3" - -[target.'cfg(target_os = "windows")'.dependencies] -souvlaki = "0.8.3" - -[profile.release] -opt-level = "z" -lto = "fat" -codegen-units = 1 -panic = "abort" -strip = "debuginfo" -incremental = false - -[profile.dev] -panic = "abort" diff --git a/souvlaki/build.gradle.kts b/souvlaki/build.gradle.kts deleted file mode 100644 index d5049a0..0000000 --- a/souvlaki/build.gradle.kts +++ /dev/null @@ -1,171 +0,0 @@ -import com.vanniktech.maven.publish.JavaLibrary -import com.vanniktech.maven.publish.JavadocJar -import gobley.gradle.GobleyHost -import gobley.gradle.Variant -import gobley.gradle.cargo.dsl.jvm -import gobley.gradle.cargo.tasks.CargoBuildTask -import gobley.gradle.cargo.tasks.FindDynamicLibrariesTask -import gobley.gradle.cargo.tasks.RustUpTargetAddTask -import org.gradle.api.Project -import org.gradle.api.publish.maven.MavenPublication -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import java.io.File - -fun rustLibraryName(triple: String): String = when { - triple.contains("windows") -> "souvlaki_kt.dll" - triple.contains("darwin") || triple.contains("apple") -> "libsouvlaki_kt.dylib" - else -> "libsouvlaki_kt.so" -} - -fun Project.prebuiltRustLibrary(triple: String): File = - layout.projectDirectory.dir("target/$triple/release").file(rustLibraryName(triple)).asFile - -// Only these targets are built in CI - skip others to avoid cross-compilation errors -val supportedTargets = setOf( - "aarch64-apple-darwin", - "x86_64-apple-darwin", - "x86_64-unknown-linux-gnu", - "aarch64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", - "aarch64-pc-windows-msvc" -) - -plugins { - alias(libs.plugins.kotlinJvm) - alias(libs.plugins.kotlinAtomicfu) - alias(libs.plugins.gobleyCargo) - alias(libs.plugins.gobleyRust) - alias(libs.plugins.gobleyUniffi) - alias(libs.plugins.maven.publish) -} - -cargo { - jvmVariant.set(Variant.Release) - builds.jvm { - // Only embed for the current host platform; other platforms are packed as resources. - embedRustLibrary = GobleyHost.current.rustTarget == rustTarget - } -} - -rust { - val userHome = System.getProperty("user.home") - val cargoBin = file("$userHome/.cargo/bin") - val rustupToolchainBin = file( - "$userHome/.rustup/toolchains/stable-${GobleyHost.current.rustTarget.rustTriple}/bin", - ) - when { - cargoBin.resolve("rustc").exists() -> toolchainDirectory.set(cargoBin) - rustupToolchainBin.resolve("rustc").exists() -> toolchainDirectory.set(rustupToolchainBin) - } -} - -uniffi { - generateFromLibrary { - build.set(GobleyHost.current.rustTarget) - } -} - -kotlin { - jvmToolchain(17) -} - -sourceSets { - main { - resources.srcDir("src/main/resources") - } -} - -dependencies { - implementation(libs.jna) - implementation(libs.kotlinx.coroutines.core) - testImplementation(kotlin("test")) - testImplementation(libs.kotlinx.coroutines.test) -} - -tasks.withType().configureEach { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) - } -} - -tasks.withType().configureEach { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - -tasks.withType().configureEach { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - -tasks.withType().configureEach { - onlyIf { - val rustTarget = target.orNull ?: return@onlyIf true - val triple = rustTarget.rustTriple - // Skip unsupported targets entirely - if (triple !in supportedTargets) return@onlyIf false - val prebuiltLib = project.prebuiltRustLibrary(triple) - val isHostTarget = GobleyHost.current.rustTarget.rustTriple == triple - !prebuiltLib.exists() || isHostTarget - } -} - -tasks.withType().configureEach { - val rustTarget = rustTarget.orNull ?: return@configureEach - val triple = rustTarget.rustTriple - val prebuiltLib = project.prebuiltRustLibrary(triple) - val isHostTarget = GobleyHost.current.rustTarget.rustTriple == triple - if (prebuiltLib.exists() && !isHostTarget) { - searchPaths.set(listOf(prebuiltLib.parentFile)) - } -} - -tasks.withType().configureEach { - onlyIf { - val rustTarget = rustTarget.orNull ?: return@onlyIf true - val triple = rustTarget.rustTriple - // Skip unsupported targets entirely - if (triple !in supportedTargets) return@onlyIf false - val prebuiltLib = project.prebuiltRustLibrary(triple) - val isHostTarget = GobleyHost.current.rustTarget.rustTriple == triple - !prebuiltLib.exists() || isHostTarget - } -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -mavenPublishing { - configure(JavaLibrary(javadocJar = JavadocJar.Empty(), sourcesJar = true)) - publishToMavenCentral() - if (project.findProperty("signingInMemoryKey") != null) { - signAllPublications() - } - coordinates(artifactId = "souvlaki") - pom { - name.set("SouvlakiKt") - description.set("Kotlin media controls library powered by Rust Souvlaki") - } -} - -// Publish native runtime JARs as additional artifacts -afterEvaluate { - publishing { - publications.withType().configureEach { - if (name == "maven") { - // Add native runtime JARs for each platform - val nativeJars = layout.buildDirectory.dir("libs").get().asFile.listFiles() - ?.filter { it.name.startsWith("souvlaki-") && it.name.endsWith(".jar") && !it.name.contains("sources") && !it.name.contains("javadoc") } - ?: emptyList() - - nativeJars.forEach { jar -> - val classifier = jar.name.removePrefix("souvlaki-").removeSuffix(".jar") - artifact(jar) { - this.classifier = classifier - } - } - } - } - } -} diff --git a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaControls.kt b/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaControls.kt deleted file mode 100644 index 4301afa..0000000 --- a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaControls.kt +++ /dev/null @@ -1,213 +0,0 @@ -package io.github.kdroidfilter.souvlaki - -import io.github.kdroidfilter.souvlaki.native.createMediaControls -import io.github.kdroidfilter.souvlaki.native.createMediaControlsWithHwnd -import io.github.kdroidfilter.souvlaki.native.destroyMediaControls -import io.github.kdroidfilter.souvlaki.native.mediaControlsAttach -import io.github.kdroidfilter.souvlaki.native.mediaControlsDetach -import io.github.kdroidfilter.souvlaki.native.mediaControlsSetMetadata -import io.github.kdroidfilter.souvlaki.native.mediaControlsSetPlayback -import io.github.kdroidfilter.souvlaki.native.mediaControlsSetPlaybackWithProgress - -/** - * Cross-platform media controls for Kotlin/JVM applications. - * - * Provides integration with the operating system's media control interfaces: - * - **Linux**: D-Bus MPRIS - * - **macOS**: MediaPlayer framework (Now Playing) - * - **Windows**: System Media Transport Controls (SMTC) - * - * ## Setup - * - * On Windows, you must initialize the configuration before creating controls: - * - * ```kotlin - * // At app startup (required on Windows, optional on Linux/macOS) - * MediaControlsConfig.init(mainWindow) - * ``` - * - * ## Example Usage - * - * ```kotlin - * val controls = MediaControls.create( - * dbusName = "my_player", - * displayName = "My Player" - * ) - * - * controls.setMetadata(MediaMetadata( - * title = "Song Title", - * artist = "Artist Name", - * album = "Album Name" - * )) - * - * controls.setPlayback(PlaybackStatus.PLAYING) - * - * controls.attach { event -> - * when (event.eventType) { - * MediaControlEventType.PLAY -> player.play() - * MediaControlEventType.PAUSE -> player.pause() - * MediaControlEventType.NEXT -> player.next() - * MediaControlEventType.PREVIOUS -> player.previous() - * else -> {} - * } - * } - * - * // When done - * controls.close() - * ``` - * - * @see MediaControlsConfig - * @see MediaMetadata - * @see PlaybackStatus - */ -class MediaControls private constructor( - private var handle: ULong -) : AutoCloseable { - - private var closed = false - - private fun requireHandle(): ULong { - check(!closed) { "MediaControls is closed" } - return handle - } - - /** - * Attach a callback to receive media control events. - * - * The callback will be invoked when the user interacts with the OS - * media controls (e.g., play/pause buttons, seek slider, media keys). - * - * **macOS note:** On macOS, this should be called from a background thread - * to avoid blocking the UI during initialization. - * - * @param callback The callback to receive events - */ - fun attach(callback: MediaControlCallback) { - mediaControlsAttach(requireHandle(), callback) - } - - /** - * Attach a callback to receive media control events using a lambda. - * - * @param onEvent Lambda invoked when an event is received - */ - fun attach(onEvent: (MediaControlEventData) -> Unit) { - attach(object : MediaControlCallback { - override fun onEvent(event: MediaControlEventData) { - onEvent(event) - } - }) - } - - /** - * Detach the event callback. - * - * After calling this, no more events will be received until [attach] is called again. - */ - fun detach() { - mediaControlsDetach(requireHandle()) - } - - /** - * Set the media metadata displayed in OS media controls. - * - * @param metadata The metadata to display - */ - fun setMetadata(metadata: MediaMetadata) { - mediaControlsSetMetadata( - requireHandle(), - metadata.title, - metadata.album, - metadata.artist, - metadata.coverUrl, - metadata.durationSecs - ) - } - - /** - * Set the media metadata displayed in OS media controls. - * - * @param title Track title - * @param album Album name - * @param artist Artist name - * @param coverUrl URL to album artwork - * @param durationSecs Track duration in seconds - */ - fun setMetadata( - title: String? = null, - album: String? = null, - artist: String? = null, - coverUrl: String? = null, - durationSecs: Double? = null, - ) { - mediaControlsSetMetadata( - requireHandle(), - title, - album, - artist, - coverUrl, - durationSecs - ) - } - - /** - * Set the playback status. - * - * @param status The current playback status - */ - fun setPlayback(status: PlaybackStatus) { - mediaControlsSetPlayback(requireHandle(), status) - } - - /** - * Set the playback status with progress information. - * - * @param status The current playback status - * @param progressSecs Current playback position in seconds - */ - fun setPlayback(status: PlaybackStatus, progressSecs: Double?) { - mediaControlsSetPlaybackWithProgress(requireHandle(), status, progressSecs) - } - - /** - * Close the media controls and release resources. - * - * After calling this, the instance cannot be used anymore. - */ - override fun close() { - if (closed) return - destroyMediaControls(handle) - closed = true - } - - companion object { - /** - * Create media controls. - * - * On Windows, [MediaControlsConfig.init] must be called first with the main window. - * On Linux/macOS, initialization is optional. - * - * @param dbusName D-Bus name for Linux MPRIS (e.g., "my_player"). - * Ignored on macOS and Windows. - * @param displayName Display name shown in media controls UI. - * Ignored on macOS and Windows. - * @return A new MediaControls instance - * @throws IllegalStateException On Windows if [MediaControlsConfig.init] was not called. - */ - fun create( - dbusName: String = "kotlin_media_player", - displayName: String = "Kotlin Media Player" - ): MediaControls { - if (isWindows) { - val hwnd = MediaControlsConfig.hwnd - ?: throw IllegalStateException( - "On Windows, MediaControlsConfig.init(window) must be called before creating MediaControls." - ) - val handle = createMediaControlsWithHwnd(hwnd.toULong()) - return MediaControls(handle) - } - val handle = createMediaControls(dbusName, displayName) - return MediaControls(handle) - } - } -} diff --git a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaControlsConfig.kt b/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaControlsConfig.kt deleted file mode 100644 index d665493..0000000 --- a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaControlsConfig.kt +++ /dev/null @@ -1,107 +0,0 @@ -package io.github.kdroidfilter.souvlaki - -import java.awt.Component - -/** - * Platform configuration for MediaControls initialization. - * - * On Windows, the System Media Transport Controls (SMTC) require a window handle (HWND) - * to function properly. Call [init] once at application startup before using [MediaControls]. - * - * On Linux and macOS, initialization is optional but can be called for consistency. - * - * ## Usage - * - * ```kotlin - * // At application startup (e.g., in main() or window initialization) - * MediaControlsConfig.init(mainWindow) - * - * // Then anywhere in your app - * val controls = MediaControls.create() - * ``` - */ -object MediaControlsConfig { - @Volatile - internal var hwnd: Long? = null - private set - - @Volatile - internal var initialized = false - private set - - /** - * Initialize MediaControls for the current platform. - * - * On Windows, this extracts the HWND from the provided component. - * On Linux/macOS, this marks the config as initialized but doesn't require the component. - * - * @param component The main window/frame component (required on Windows, optional on other platforms) - * @throws IllegalStateException If on Windows and the HWND cannot be extracted - */ - fun init(component: Component) { - if (isWindows) { - hwnd = getWindowHandle(component) - ?: throw IllegalStateException( - "Could not extract native window handle (HWND) from component. " + - "Make sure the window is displayable." - ) - } - initialized = true - } - - /** - * Initialize MediaControls with a raw window handle. - * - * This is useful when you already have the native window handle. - * - * @param windowHandle The native window handle (HWND on Windows) - */ - fun init(windowHandle: Long) { - hwnd = windowHandle - initialized = true - } - - /** - * Reset the configuration. - * - * Useful for testing or when the window changes. - */ - fun reset() { - hwnd = null - initialized = false - } - - /** - * Check if the configuration has been initialized. - */ - val isInitialized: Boolean - get() = initialized - - private fun getWindowHandle(component: Component): Long? { - return try { - // Use JNA's Native.getComponentID for native handle extraction - val nativeClass = Class.forName("com.sun.jna.Native") - val method = nativeClass.getMethod("getComponentID", Component::class.java) - when (val result = method.invoke(null, component)) { - is Long -> result - is Int -> result.toLong() - else -> null - } - } catch (_: Exception) { - // Fallback: try using sun.awt.windows.WComponentPeer (Windows-specific) - tryGetWindowsHandle(component) - } - } - - private fun tryGetWindowsHandle(component: Component): Long? { - return try { - val peer = component.javaClass.getMethod("getPeer").invoke(component) - ?: return null - val hwndField = peer.javaClass.getDeclaredField("hwnd") - hwndField.isAccessible = true - hwndField.getLong(peer) - } catch (_: Exception) { - null - } - } -} diff --git a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaMetadata.kt b/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaMetadata.kt deleted file mode 100644 index d21367f..0000000 --- a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/MediaMetadata.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.kdroidfilter.souvlaki - -/** - * Media metadata for display in OS media controls. - * - * @property title Track title - * @property album Album name - * @property artist Artist name - * @property coverUrl URL to album artwork (may not be supported on all platforms) - * @property durationSecs Track duration in seconds - */ -data class MediaMetadata( - val title: String? = null, - val album: String? = null, - val artist: String? = null, - val coverUrl: String? = null, - val durationSecs: Double? = null, -) diff --git a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/Platform.kt b/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/Platform.kt deleted file mode 100644 index 317ee02..0000000 --- a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/Platform.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.kdroidfilter.souvlaki - -/** Check if the current platform is Windows. */ -internal val isWindows: Boolean = System.getProperty("os.name").lowercase().contains("win") - -/** Check if the current platform is macOS. */ -internal val isMacOS: Boolean = System.getProperty("os.name").lowercase().contains("mac") - -/** Check if the current platform is Linux. */ -internal val isLinux: Boolean = System.getProperty("os.name").lowercase().contains("linux") diff --git a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/Types.kt b/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/Types.kt deleted file mode 100644 index 8e26eb6..0000000 --- a/souvlaki/src/main/kotlin/io/github/kdroidfilter/souvlaki/Types.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.kdroidfilter.souvlaki - -/** - * Re-export types from native bindings for convenient access. - */ - -/** Callback interface for receiving media control events. */ -typealias MediaControlCallback = io.github.kdroidfilter.souvlaki.native.MediaControlCallback - -/** Data class containing event information from media controls. */ -typealias MediaControlEventData = io.github.kdroidfilter.souvlaki.native.MediaControlEventData - -/** Enum representing the type of media control event. */ -typealias MediaControlEventType = io.github.kdroidfilter.souvlaki.native.MediaControlEventType - -/** Enum representing playback status (Playing, Paused, Stopped). */ -typealias PlaybackStatus = io.github.kdroidfilter.souvlaki.native.PlaybackStatus - -/** Exception thrown by souvlaki operations. */ -typealias SouvlakiException = io.github.kdroidfilter.souvlaki.native.SouvlakiException diff --git a/souvlaki/src/main/rust/error.rs b/souvlaki/src/main/rust/error.rs deleted file mode 100644 index 11db5b4..0000000 --- a/souvlaki/src/main/rust/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Error types for Souvlaki Kotlin bindings. - -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum SouvlakiError { - #[error("Media controls not found with id: {0}")] - ControlsNotFound(u64), - - #[error("Platform not supported")] - PlatformNotSupported, - - #[error("Failed to create media controls: {0}")] - Creation(String), - - #[error("Failed to attach event handler: {0}")] - Attach(String), - - #[error("Failed to detach event handler: {0}")] - Detach(String), - - #[error("Failed to set metadata: {0}")] - Metadata(String), - - #[error("Failed to set playback status: {0}")] - PlaybackStatus(String), - - #[error("Internal error: {0}")] - Internal(String), -} - -impl From for SouvlakiError { - fn from(_err: souvlaki::Error) -> Self { - // The Error type varies by platform backend, use a generic message - SouvlakiError::Internal("souvlaki operation failed".to_string()) - } -} diff --git a/souvlaki/src/main/rust/lib.rs b/souvlaki/src/main/rust/lib.rs deleted file mode 100644 index f67ed7d..0000000 --- a/souvlaki/src/main/rust/lib.rs +++ /dev/null @@ -1,486 +0,0 @@ -//! Souvlaki Kotlin bindings via UniFFI. -//! -//! Cross-platform media controls for Kotlin/JVM applications. - -mod error; - -use std::collections::HashMap; -use std::sync::{Arc, Mutex, OnceLock}; - -pub use error::SouvlakiError; -use souvlaki::{MediaControlEvent as SouvlakiEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig, SeekDirection}; - -/// Registry of active media controls instances. -static REGISTRY: OnceLock>> = OnceLock::new(); -static NEXT_ID: OnceLock> = OnceLock::new(); - -fn registry() -> &'static Mutex> { - REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) -} - -fn next_id() -> u64 { - let counter = NEXT_ID.get_or_init(|| Mutex::new(1)); - let mut guard = counter.lock().unwrap(); - let id = *guard; - *guard = guard.wrapping_add(1); - id -} - -struct ControlsState { - controls: MediaControls, - callback: Option>, -} - -/// Event types from media controls. -#[derive(Clone, Copy, Debug, uniffi::Enum)] -pub enum MediaControlEventType { - Play, - Pause, - Toggle, - Next, - Previous, - Stop, - Seek, - SeekBy, - SetPosition, - SetVolume, - OpenUri, - Raise, - Quit, -} - -/// Media control event with associated data. -#[derive(Clone, Debug, uniffi::Record)] -pub struct MediaControlEventData { - pub event_type: MediaControlEventType, - /// For Seek events: forward (true) or backward (false) - pub seek_forward: Option, - /// For SeekBy events: offset in seconds (can be negative) - pub seek_offset_secs: Option, - /// For SetPosition events: position in seconds - pub position_secs: Option, - /// For SetVolume events: volume (0.0 to 1.0) - pub volume: Option, - /// For OpenUri events: the URI - pub uri: Option, -} - -impl MediaControlEventData { - fn from_souvlaki(event: SouvlakiEvent) -> Self { - match event { - SouvlakiEvent::Play => Self { - event_type: MediaControlEventType::Play, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Pause => Self { - event_type: MediaControlEventType::Pause, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Toggle => Self { - event_type: MediaControlEventType::Toggle, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Next => Self { - event_type: MediaControlEventType::Next, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Previous => Self { - event_type: MediaControlEventType::Previous, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Stop => Self { - event_type: MediaControlEventType::Stop, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Seek(direction) => Self { - event_type: MediaControlEventType::Seek, - seek_forward: Some(matches!(direction, SeekDirection::Forward)), - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::SeekBy(direction, duration) => { - let secs = duration.as_secs_f64(); - let offset = match direction { - SeekDirection::Forward => secs, - SeekDirection::Backward => -secs, - }; - Self { - event_type: MediaControlEventType::SeekBy, - seek_forward: Some(matches!(direction, SeekDirection::Forward)), - seek_offset_secs: Some(offset), - position_secs: None, - volume: None, - uri: None, - } - } - SouvlakiEvent::SetPosition(pos) => Self { - event_type: MediaControlEventType::SetPosition, - seek_forward: None, - seek_offset_secs: None, - position_secs: Some(pos.0.as_secs_f64()), - volume: None, - uri: None, - }, - SouvlakiEvent::SetVolume(vol) => Self { - event_type: MediaControlEventType::SetVolume, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: Some(vol), - uri: None, - }, - SouvlakiEvent::OpenUri(uri) => Self { - event_type: MediaControlEventType::OpenUri, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: Some(uri), - }, - SouvlakiEvent::Raise => Self { - event_type: MediaControlEventType::Raise, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - SouvlakiEvent::Quit => Self { - event_type: MediaControlEventType::Quit, - seek_forward: None, - seek_offset_secs: None, - position_secs: None, - volume: None, - uri: None, - }, - } - } -} - -/// Playback status for media controls. -#[derive(Clone, Copy, Debug, uniffi::Enum)] -pub enum PlaybackStatus { - Playing, - Paused, - Stopped, -} - -/// Callback interface for media control events. -#[uniffi::export(callback_interface)] -pub trait MediaControlCallback: Send + Sync { - fn on_event(&self, event: MediaControlEventData); -} - -fn with_controls_mut(id: u64, f: F) -> Result -where - F: FnOnce(&mut ControlsState) -> Result, -{ - let mut guard = registry() - .lock() - .map_err(|_| SouvlakiError::Internal("registry lock failed".to_string()))?; - let state = guard - .get_mut(&id) - .ok_or(SouvlakiError::ControlsNotFound(id))?; - f(state) -} - -/// Create media controls for Linux (D-Bus MPRIS). -/// -/// - `dbus_name`: The D-Bus name for the media player (e.g., "my_player") -/// - `display_name`: The display name shown in the media controls UI -#[cfg(target_os = "linux")] -#[uniffi::export] -pub fn create_media_controls(dbus_name: String, display_name: String) -> Result { - let config = PlatformConfig { - dbus_name: &dbus_name, - display_name: &display_name, - hwnd: None, - }; - - let controls = MediaControls::new(config) - .map_err(|_| SouvlakiError::Creation("failed to create media controls".to_string()))?; - - let id = next_id(); - let state = ControlsState { - controls, - callback: None, - }; - - let mut guard = registry() - .lock() - .map_err(|_| SouvlakiError::Internal("registry lock failed".to_string()))?; - guard.insert(id, state); - - Ok(id) -} - -/// Create media controls for macOS. -/// -/// Note: On macOS, you need to run an event loop for events to be received. -#[cfg(target_os = "macos")] -#[uniffi::export] -pub fn create_media_controls(_dbus_name: String, _display_name: String) -> Result { - let config = PlatformConfig { - dbus_name: "", - display_name: "", - hwnd: None, - }; - - let mut controls = MediaControls::new(config) - .map_err(|_| SouvlakiError::Creation("failed to create media controls".to_string()))?; - - // Set initial playback state for controls to appear in Control Center - let _ = controls.set_playback(MediaPlayback::Playing { progress: None }); - - let id = next_id(); - let state = ControlsState { - controls, - callback: None, - }; - - let mut guard = registry() - .lock() - .map_err(|_| SouvlakiError::Internal("registry lock failed".to_string()))?; - guard.insert(id, state); - - Ok(id) -} - -/// Create media controls for Windows. -/// -/// - `dbus_name`: Ignored on Windows -/// - `display_name`: Ignored on Windows -/// -/// Note: On Windows, HWND handling requires special setup. This creates -/// dummy window-less controls which may have limited functionality. -#[cfg(target_os = "windows")] -#[uniffi::export] -pub fn create_media_controls(_dbus_name: String, _display_name: String) -> Result { - // On Windows, we need an HWND. For now, we'll try with None which - // may work for some scenarios or fail gracefully. - let config = PlatformConfig { - dbus_name: "", - display_name: "", - hwnd: None, - }; - - let controls = MediaControls::new(config) - .map_err(|_| SouvlakiError::Creation("failed to create media controls".to_string()))?; - - let id = next_id(); - let state = ControlsState { - controls, - callback: None, - }; - - let mut guard = registry() - .lock() - .map_err(|_| SouvlakiError::Internal("registry lock failed".to_string()))?; - guard.insert(id, state); - - Ok(id) -} - -/// Create media controls for Windows with a window handle. -/// -/// - `hwnd`: The window handle (HWND) as a raw pointer value -#[cfg(target_os = "windows")] -#[uniffi::export] -pub fn create_media_controls_with_hwnd(hwnd: u64) -> Result { - let hwnd_ptr = hwnd as *mut std::ffi::c_void; - let hwnd_opt = if hwnd_ptr.is_null() { - None - } else { - Some(hwnd_ptr) - }; - - let config = PlatformConfig { - dbus_name: "", - display_name: "", - hwnd: hwnd_opt, - }; - - let controls = MediaControls::new(config) - .map_err(|_| SouvlakiError::Creation("failed to create media controls".to_string()))?; - - let id = next_id(); - let state = ControlsState { - controls, - callback: None, - }; - - let mut guard = registry() - .lock() - .map_err(|_| SouvlakiError::Internal("registry lock failed".to_string()))?; - guard.insert(id, state); - - Ok(id) -} - -/// Stub for non-Windows platforms -#[cfg(not(target_os = "windows"))] -#[uniffi::export] -pub fn create_media_controls_with_hwnd(_hwnd: u64) -> Result { - Err(SouvlakiError::PlatformNotSupported) -} - -/// Destroy media controls and release resources. -#[uniffi::export] -pub fn destroy_media_controls(id: u64) -> Result<(), SouvlakiError> { - let mut guard = registry() - .lock() - .map_err(|_| SouvlakiError::Internal("registry lock failed".to_string()))?; - - guard - .remove(&id) - .ok_or(SouvlakiError::ControlsNotFound(id))?; - - Ok(()) -} - -/// Attach a callback to receive media control events. -/// -/// On macOS, this performs an initialization cycle to ensure all buttons -/// (Previous/Next) are properly enabled in Control Center. This should be -/// called from a background thread to avoid blocking the UI. -#[uniffi::export] -pub fn media_controls_attach( - id: u64, - callback: Box, -) -> Result<(), SouvlakiError> { - let callback_arc: Arc = Arc::from(callback); - let callback_clone = callback_arc.clone(); - - with_controls_mut(id, |state| { - // On macOS, do a dummy attach/detach cycle first to initialize all buttons properly - #[cfg(target_os = "macos")] - { - let _ = state.controls.attach(|_| {}); - let _ = state.controls.detach(); - } - - state.callback = Some(callback_arc); - state - .controls - .attach(move |event| { - let data = MediaControlEventData::from_souvlaki(event); - callback_clone.on_event(data); - }) - .map_err(|_| SouvlakiError::Attach("failed to attach event handler".to_string())) - }) -} - -/// Detach the event callback. -#[uniffi::export] -pub fn media_controls_detach(id: u64) -> Result<(), SouvlakiError> { - with_controls_mut(id, |state| { - state.callback = None; - state - .controls - .detach() - .map_err(|_| SouvlakiError::Detach("failed to detach event handler".to_string())) - }) -} - -/// Set media metadata. -/// -/// All parameters are optional and can be None. -#[uniffi::export] -pub fn media_controls_set_metadata( - id: u64, - title: Option, - album: Option, - artist: Option, - cover_url: Option, - duration_secs: Option, -) -> Result<(), SouvlakiError> { - with_controls_mut(id, |state| { - let duration = duration_secs.map(|s| std::time::Duration::from_secs_f64(s)); - - let metadata = MediaMetadata { - title: title.as_deref(), - album: album.as_deref(), - artist: artist.as_deref(), - cover_url: cover_url.as_deref(), - duration, - }; - - state - .controls - .set_metadata(metadata) - .map_err(|_| SouvlakiError::Metadata("failed to set metadata".to_string())) - }) -} - -/// Set playback status. -#[uniffi::export] -pub fn media_controls_set_playback(id: u64, status: PlaybackStatus) -> Result<(), SouvlakiError> { - with_controls_mut(id, |state| { - let playback = match status { - PlaybackStatus::Playing => MediaPlayback::Playing { progress: None }, - PlaybackStatus::Paused => MediaPlayback::Paused { progress: None }, - PlaybackStatus::Stopped => MediaPlayback::Stopped, - }; - - state - .controls - .set_playback(playback) - .map_err(|_| SouvlakiError::PlaybackStatus("failed to set playback status".to_string())) - }) -} - -/// Set playback status with progress information. -/// -/// - `status`: The playback status -/// - `progress_secs`: Current playback position in seconds (optional) -#[uniffi::export] -pub fn media_controls_set_playback_with_progress( - id: u64, - status: PlaybackStatus, - progress_secs: Option, -) -> Result<(), SouvlakiError> { - with_controls_mut(id, |state| { - let progress = progress_secs.map(|s| { - MediaPosition(std::time::Duration::from_secs_f64(s)) - }); - - let playback = match status { - PlaybackStatus::Playing => MediaPlayback::Playing { progress }, - PlaybackStatus::Paused => MediaPlayback::Paused { progress }, - PlaybackStatus::Stopped => MediaPlayback::Stopped, - }; - - state - .controls - .set_playback(playback) - .map_err(|_| SouvlakiError::PlaybackStatus("failed to set playback status".to_string())) - }) -} - -uniffi::setup_scaffolding!(); diff --git a/souvlaki/uniffi.toml b/souvlaki/uniffi.toml deleted file mode 100644 index e5ed8f3..0000000 --- a/souvlaki/uniffi.toml +++ /dev/null @@ -1,2 +0,0 @@ -package_name = "io.github.kdroidfilter.souvlaki.native" -cdylib_name = "souvlaki_kt"