diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 11219f9a..23c0fed9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ SPlayer-Next 是基于 **Electron + Vue 3 + TypeScript** 的桌面音乐播放 ### 原生模块(`native/`) -- `audio-engine`:FFmpeg 解码 + rodio 播放 + FFT + 封面提取 +- `audio-engine`:`ffmpeg_audio` 解码(静态编 FFmpeg,零环境依赖)+ rodio 播放 + FFT + 封面提取;URL 通过 `HttpRangeSource`(ureq + rustls)包成 Read+Seek 流喂给 ffmpeg_audio - `media-ctrl`:跨平台系统媒体控件(Windows SMTC / Linux MPRIS / macOS MPNowPlaying)+ Discord RPC - `taskbar-lyric`:Windows 专属,把窗口嵌入任务栏 + RegistryWatcher / UiaWatcher / TrayWatcher 四路监听 diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 5478d0d8..00000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: "https://github.com/anthropics/claude-code.git" - plugins: "code-review@claude-code-plugins" - prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}" - # 始终使用中文进行代码审查与评论 - claude_args: '--append-system-prompt "请始终使用简体中文进行代码审查与评论,遵循项目 CLAUDE.md 中的约定。"' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 0fe79752..00000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # 始终使用中文回答,并遵循仓库的代码规范 - claude_args: '--append-system-prompt "请始终使用简体中文回答所有问题、评论与代码注释。遵循项目 CLAUDE.md 中的约定。"' diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 71dc5337..289e7dad 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -21,8 +21,6 @@ on: env: NODE_VERSION: 22.x - BUILDER_REPO: apoint123/ffmpeg-builder - FFMPEG_VERSION_TAG: "8.0.1" jobs: build: @@ -76,57 +74,13 @@ jobs: with: workspaces: "native/audio-engine -> target\nnative/media-ctrl -> target" - # 下载预编译的 FFmpeg 静态库 (Windows) - - name: 下载预编译的 FFmpeg 库 (Windows) - if: runner.os == 'Windows' - run: | - $packageName = "ffmpeg-${{ env.FFMPEG_VERSION_TAG }}-${{ matrix.artifact_name_suffix }}.${{ matrix.package_extension }}" - $downloadUrl = "https://github.com/${{ env.BUILDER_REPO }}/releases/latest/download/$packageName" - Write-Host "Downloading $packageName from $downloadUrl ..." - Invoke-WebRequest -Uri $downloadUrl -OutFile $packageName - - mkdir -p vendor/ffmpeg - Expand-Archive -Path $packageName -DestinationPath vendor/ffmpeg_temp -Force - Move-Item -Path vendor/ffmpeg_temp/include -Destination vendor/ffmpeg/include - Move-Item -Path vendor/ffmpeg_temp/lib -Destination vendor/ffmpeg/lib - Remove-Item -Recurse -Force vendor/ffmpeg_temp - Remove-Item $packageName - - echo "FFMPEG_DIR=${{ github.workspace }}/vendor/ffmpeg" >> $env:GITHUB_ENV - shell: powershell - - # 下载预编译的 FFmpeg 静态库 (macOS/Linux) - - name: 下载预编译的 FFmpeg 库 (macOS/Linux) - if: runner.os != 'Windows' - run: | - PACKAGE_NAME="ffmpeg-${{ env.FFMPEG_VERSION_TAG }}-${{ matrix.artifact_name_suffix }}.${{ matrix.package_extension }}" - DOWNLOAD_URL="https://github.com/${{ env.BUILDER_REPO }}/releases/latest/download/${PACKAGE_NAME}" - echo "Downloading $PACKAGE_NAME from $DOWNLOAD_URL ..." - curl -f -sL $DOWNLOAD_URL -o $PACKAGE_NAME - - mkdir -p vendor/ffmpeg - tar -xzf $PACKAGE_NAME -C vendor/ffmpeg - rm $PACKAGE_NAME - - echo "FFMPEG_DIR=${{ github.workspace }}/vendor/ffmpeg" >> $GITHUB_ENV - shell: bash - - # 设置 FFmpeg 环境变量 - - name: 设置 FFmpeg 环境变量 - run: | - echo "PKG_CONFIG_PATH=${{ github.workspace }}/vendor/ffmpeg/lib/pkgconfig" >> $GITHUB_ENV - if [[ "$RUNNER_OS" != "Windows" ]]; then - echo "CFLAGS=-I${{ github.workspace }}/vendor/ffmpeg/include" >> $GITHUB_ENV - fi - shell: bash - # Linux: 安装系统构建/打包依赖 - name: 安装系统依赖 (Linux) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install --no-install-recommends -y \ - libasound2-dev libdbus-1-dev pkg-config clang nasm \ + libasound2-dev libdbus-1-dev pkg-config clang \ rpm libarchive-tools # 清理旧构建产物 diff --git a/CLAUDE.md b/CLAUDE.md index 9e6081df..4310f7e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,21 +20,7 @@ pnpm build:native # Rust only; add `-- --dev` for debug `SKIP_NATIVE_BUILD=true` skips Rust during dev. -### FFmpeg Setup (first-time native build) - -`audio-engine` static-links FFmpeg. Before first `pnpm dev` / `pnpm build:native`, download FFmpeg static libs (with `include` and `lib`) and set: - -```bash -# macOS / Linux -export FFMPEG_DIR=/path/to/ffmpeg -export PKG_CONFIG_PATH="$FFMPEG_DIR/lib/pkgconfig" -``` - -```powershell -# Windows -$env:FFMPEG_DIR="D:\ffmpeg" -$env:PKG_CONFIG_PATH="$env:FFMPEG_DIR\lib\pkgconfig" -``` +`audio-engine` static-links FFmpeg via the `ffmpeg_audio` crate (vendor zip + cc-built at compile time). Zero environment dependency — no `FFMPEG_DIR` / `PKG_CONFIG_PATH`, no system FFmpeg required. ## Architecture @@ -49,7 +35,7 @@ $env:PKG_CONFIG_PATH="$env:FFMPEG_DIR\lib\pkgconfig" Three `.node` modules in `native/`, built via `scripts/build-native.ts`, lazy-loaded by `electron/main/utils/nativeLoader.ts`. NAPI-RS auto-generates `index.d.ts`, imported via path aliases `@splayer/audio-engine`, `@splayer/media-ctrl`, `@splayer/taskbar-lyric`. -- `audio-engine` — FFmpeg decode + rodio playback + FFT + cover extraction. Pushes events (state/position/ended/outputStalled) via ThreadsafeFunction. Has load_token race protection and `AVIOInterruptCB` for instant stop on blocking IO. +- `audio-engine` — `ffmpeg_audio` decode (static FFmpeg) + rodio playback + FFT + cover extraction. URLs wrapped as `Read + Seek` via `HttpRangeSource` (ureq + rustls) — TLS handled in Rust, cross-platform with no system deps. Pushes events (state/position/ended/outputStalled) via ThreadsafeFunction. Has load_token race protection and a cancel flag (injected into `HttpRangeSource`) for instant stop on blocking IO. - `media-ctrl` — Cross-platform system media controls (Windows SMTC / Linux MPRIS / macOS MPNowPlaying) + Discord RPC. - `taskbar-lyric` — Windows taskbar lyric text rendering with RegistryWatcher / UiaWatcher / TrayWatcher. diff --git a/Cargo.lock b/Cargo.lock index c6dfc2ce..020e8ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,18 +45,58 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -83,6 +123,21 @@ dependencies = [ "slab", ] +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "async-io" version = "2.6.0" @@ -107,25 +162,34 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] +[[package]] +name = "async-object-pool" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c" +dependencies = [ + "async-std", +] + [[package]] name = "async-process" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener", + "event-listener 5.4.1", "futures-lite", "rustix", ] @@ -138,7 +202,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -159,6 +223,34 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-task" version = "4.7.1" @@ -173,7 +265,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -188,7 +280,8 @@ version = "0.1.0" dependencies = [ "anyhow", "cpal", - "ffmpeg-next", + "ffmpeg_audio", + "httpmock", "image", "napi", "napi-build", @@ -202,6 +295,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "ureq", "walkdir", ] @@ -211,6 +305,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -220,7 +337,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -228,7 +345,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn", + "syn 2.0.117", ] [[package]] @@ -240,15 +357,32 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", + "log", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 2.1.2", "shlex", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -276,7 +410,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -452,6 +586,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "ctor" version = "0.8.0" @@ -596,6 +736,27 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "discord-rich-presence" version = "1.1.0" @@ -629,7 +790,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -653,6 +814,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "endi" version = "1.1.1" @@ -677,7 +847,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -696,6 +866,12 @@ dependencies = [ "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 = "5.4.1" @@ -713,7 +889,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener", + "event-listener 5.4.1", "pin-project-lite", ] @@ -733,28 +909,26 @@ dependencies = [ ] [[package]] -name = "ffmpeg-next" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d" +name = "ffmpeg_audio" +version = "0.1.0" +source = "git+https://github.com/SPlayer-Dev/ffmpeg-audio?branch=feat%2Fduration-api#5b7d2c05d7fadd1a530b5c32c5b63e7385d8745c" dependencies = [ - "bitflags 2.11.0", - "ffmpeg-sys-next", + "ffmpeg_audio_sys", "libc", + "thiserror 2.0.18", ] [[package]] -name = "ffmpeg-sys-next" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a314bc0e022a33a99567ed4bd2576bd58ffd8fcff7891c29194cfecc26a62547" +name = "ffmpeg_audio_sys" +version = "0.1.0" +source = "git+https://github.com/SPlayer-Dev/ffmpeg-audio?branch=feat%2Fduration-api#5b7d2c05d7fadd1a530b5c32c5b63e7385d8745c" dependencies = [ "bindgen 0.72.1", "cc", "libc", - "num_cpus", "pkg-config", "vcpkg", + "zip", ] [[package]] @@ -763,6 +937,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.9" @@ -771,8 +951,15 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -857,7 +1044,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -931,6 +1118,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -964,6 +1163,91 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "httpmock" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-std", + "async-trait", + "base64 0.21.7", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1110,6 +1394,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1166,7 +1459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1191,6 +1484,46 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1203,6 +1536,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "libc" version = "0.2.183" @@ -1229,6 +1568,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1255,6 +1603,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "mach2" @@ -1358,7 +1709,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "392ce2be7224867869df37e473f28871ab0ff725c0014f1b196ba56a38aea9a8" dependencies = [ - "async-channel", + "async-channel 2.5.0", "futures-channel", "serde", "trait-variant", @@ -1400,7 +1751,7 @@ dependencies = [ "napi-derive-backend", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1413,7 +1764,7 @@ dependencies = [ "proc-macro2", "quote", "semver", - "syn", + "syn 2.0.117", ] [[package]] @@ -1454,6 +1805,12 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1502,7 +1859,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1523,16 +1880,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_enum" version = "0.7.6" @@ -1552,7 +1899,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1816,12 +2163,43 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[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.5" @@ -1881,6 +2259,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -1888,7 +2272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1960,6 +2344,17 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.12.3" @@ -1989,6 +2384,20 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[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.20.1" @@ -2037,6 +2446,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "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.22" @@ -2091,7 +2535,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2107,6 +2551,16 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -2115,7 +2569,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2160,6 +2614,18 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -2172,6 +2638,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -2194,6 +2670,35 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.117" @@ -2213,7 +2718,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2242,6 +2747,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2268,7 +2784,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2279,7 +2795,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2324,6 +2840,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2346,7 +2871,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -2359,7 +2884,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2392,6 +2917,12 @@ dependencies = [ "winnow 1.0.1", ] +[[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" @@ -2423,7 +2954,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2474,7 +3005,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2487,6 +3018,18 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "uds_windows" version = "1.2.1" @@ -2516,6 +3059,27 @@ 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 = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.8" @@ -2560,6 +3124,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2576,6 +3146,15 @@ dependencies = [ "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" @@ -2642,7 +3221,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2699,6 +3278,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[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-util" version = "0.1.11" @@ -2708,6 +3321,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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.54.0" @@ -2781,7 +3400,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2792,7 +3411,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2847,6 +3466,15 @@ 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.59.0" @@ -3053,7 +3681,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3069,7 +3697,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3136,7 +3764,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3156,7 +3784,7 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-lite", "hex", @@ -3184,7 +3812,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -3218,10 +3846,16 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" @@ -3252,15 +3886,47 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zune-core" version = "0.5.1" @@ -3299,7 +3965,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zvariant_utils", ] @@ -3312,6 +3978,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.117", "winnow 0.7.15", ] diff --git a/native/audio-engine/Cargo.toml b/native/audio-engine/Cargo.toml index 8eb7634f..b90f9c44 100644 --- a/native/audio-engine/Cargo.toml +++ b/native/audio-engine/Cargo.toml @@ -9,12 +9,8 @@ crate-type = ["cdylib"] [dependencies] napi = { version = "3.8", features = ["napi9", "serde-json", "tokio_rt"] } napi-derive = "3.5" -ffmpeg-next = { version = "8", default-features = false, features = [ - "codec", - "format", - "software-resampling", - "static", -] } +ffmpeg_audio = { git = "https://github.com/SPlayer-Dev/ffmpeg-audio", branch = "feat/duration-api", default-features = false } +ureq = { version = "2", default-features = false, features = ["tls"] } rodio = { version = "0.20", default-features = false } cpal = "0.15" parking_lot = "0.12" @@ -40,5 +36,8 @@ tokio = { version = "1", features = ["rt"] } [build-dependencies] napi-build = "2.3" +[dev-dependencies] +httpmock = "0.7" + [lints] workspace = true diff --git a/native/audio-engine/src/decoder.rs b/native/audio-engine/src/decoder.rs index 1ee27213..679607b3 100644 --- a/native/audio-engine/src/decoder.rs +++ b/native/audio-engine/src/decoder.rs @@ -1,202 +1,83 @@ -use std::ffi::c_void; -use std::os::raw::c_int; +use std::fs::File; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread::{self, JoinHandle}; +use std::time::Duration; use anyhow::{Context, Result}; -use ffmpeg_next as ffmpeg; -use ffmpeg_next::ChannelLayout; -use ffmpeg_next::Dictionary; -use tracing::{debug, warn}; +use ffmpeg_audio::{AudioError, AudioReader}; +use tracing::warn; +use crate::http_source; use crate::loudness::LoudnessAnalyzer; use crate::metadata; use crate::shared::{AudioChunk, AudioMetadata, Shared}; -/// 播放输出目标格式 +/// 播放输出目标格式(重采样后送入 rodio) pub const TARGET_SAMPLE_RATE: u32 = 48000; pub const TARGET_CHANNELS: u16 = 2; -/// FFT 分析目标格式:单声道 44100Hz -pub const FFT_SAMPLE_RATE: u32 = 44100; -/// 网络流读取失败最大重试次数(ffmpeg 自带 reconnect_delay_max 已在内部重试, -/// 这里只是兜底防止 reconnect 不生效时立即放弃;次数从 3 降到 1 避免与 ffmpeg 重叠) -const NETWORK_READ_RETRIES: u32 = 1; -/// 重试间隔(毫秒) -const NETWORK_RETRY_DELAY_MS: u64 = 500; -/// HTTP 读写超时(微秒),15s。无此项时 ffmpeg HTTP 默认无限阻塞 -const HTTP_RW_TIMEOUT_US: &str = "15000000"; - - -/// 中断回调上下文:ffmpeg 内部线程会按 opaque 指针解引用,必须比 input_ctx 活得久 -/// 通过 Box 持有;DecoderData 把它放在 input_ctx 之后保证 drop 顺序 -struct InterruptCtx { - flag: Arc, -} - -/// ffmpeg 中断回调函数:返回非零让 ffmpeg 立即 abort 当前 IO(返回 AVERROR_EXIT) -/// 在 ffmpeg 的 IO 线程被周期性调用,必须 reentrant + 极快 -extern "C" fn ffmpeg_interrupt_callback(opaque: *mut c_void) -> c_int { - if opaque.is_null() { - return 0; - } - // SAFETY: opaque 指向的 Box 在 DecoderData 生命周期内有效 - let ctx = unsafe { &*(opaque as *const InterruptCtx) }; - if ctx.flag.load(Ordering::Acquire) { - 1 - } else { - 0 - } -} - -/// 解码会话所需的资源(跨 seek 复用,避免重建 FFmpeg 上下文) +/// 解码会话所需的资源(跨 seek 复用,避免重建 ffmpeg_audio 上下文) pub struct DecoderData { - /// 注意 drop 顺序:input_ctx 必须在 _interrupt_ctx 之前被 drop - /// (字段按声明顺序 drop;input_ctx 关闭时 ffmpeg 不再回调,再释放 ctx 才安全) - input_ctx: ffmpeg::format::context::Input, - decoder: ffmpeg::decoder::Audio, - audio_stream_index: usize, - resampler: Option, - fft_resampler: Option, - /// 中断标志的 Arc(与 _interrupt_ctx 内的同一 Arc);resume_decode 需要 clone 给新 shared - interrupt_flag: Arc, - /// 中断回调上下文(保活用),drop 时机晚于 input_ctx - _interrupt_ctx: Box, + reader: AudioReader, + /// 中断标志:仅网络源持有;本地 File 不会长时间阻塞,没必要绑 + /// 通过 shared.bind_interrupt 注入,外部 stop() 触发后 HttpRangeSource::read 会返回 Interrupted + interrupt_flag: Option>, } impl DecoderData { - /// 在已有上下文上 seek + flush,不重新打开文件 - /// 返回 false 表示 seek 失败,调用方应回退到完整 load + /// 在已有 reader 上 seek,失败时调用方应回退到完整 load pub fn seek(&mut self, position_secs: f64) -> bool { - let ts = (position_secs * ffmpeg::ffi::AV_TIME_BASE as f64) as i64; - // 优先 ..ts(max_ts=ts,只接受 ≤ts 的关键帧),失败再退到无约束 - // 反过来在 mp3/aac 上可能定位到 >ts 的帧导致 seek 后多跳一帧 - let ok = self.input_ctx.seek(ts, ..ts).is_ok() - || self.input_ctx.seek(ts, ..).is_ok(); - if ok { - // 清空解码器内部缓冲区 - // SAFETY: decoder 内部持有有效的 AVCodecContext 指针 - unsafe { - ffmpeg::ffi::avcodec_flush_buffers(self.decoder.as_mut_ptr()); - } - } - ok + self.reader + .seek(Duration::from_secs_f64(position_secs)) + .is_ok() } /// 清除中断标志:seek 路径在 join 旧解码线程后调用一次,避免 stop 信号导致 seek 自爆 pub fn reset_interrupt(&self) { - self.interrupt_flag.store(false, Ordering::Release); + if let Some(ref flag) = self.interrupt_flag { + flag.store(false, Ordering::Release); + } } - /// 拿中断标志的 Arc clone:resume_decode 后需把它绑定到新 shared - pub fn interrupt_handle(&self) -> Arc { - Arc::clone(&self.interrupt_flag) - } -} - -/// 把中断回调装到刚打开的 input 上,返回中断标志 + 上下文 Box -/// 标志由调用方保管 Arc,写 true 即可让 ffmpeg 中断 -fn install_interrupt(input: &mut ffmpeg::format::context::Input) -> (Arc, Box) { - let flag = Arc::new(AtomicBool::new(false)); - let ctx = Box::new(InterruptCtx { - flag: Arc::clone(&flag), - }); - let opaque = ctx.as_ref() as *const InterruptCtx as *mut c_void; - // SAFETY: input.as_mut_ptr() 返回有效的 AVFormatContext 指针; - // opaque 指向的 Box 由 DecoderData 持有,drop 顺序保证早于 input_ctx 释放后才回收 - unsafe { - let raw = input.as_mut_ptr(); - (*raw).interrupt_callback.callback = Some(ffmpeg_interrupt_callback); - (*raw).interrupt_callback.opaque = opaque; + /// 拿中断标志的 Arc clone:resume_decode 后需把它绑到新 shared + pub fn interrupt_handle(&self) -> Option> { + self.interrupt_flag.clone() } - (flag, ctx) } -// SAFETY: DecoderData 的所有字段(FFmpeg C 指针的 Rust wrapper) -// 仅被单一线程独占使用,在 spawn 时 move 进线程,不存在并发访问。 +// SAFETY: AudioReader 内部持有 ffmpeg C 指针,仅被解码线程独占使用,spawn 时 move 进线程 unsafe impl Send for DecoderData {} -/// 启动解码线程,返回音频元数据和线程句柄。 +/// 启动解码线程,返回音频元数据和线程句柄 /// /// 线程结束时返回 `DecoderData`,调用方可通过 `handle.join()` 回收并复用于后续 seek, -/// 避免重建 FFmpeg 上下文。 -/// -/// - `cover_cache_dir`: 封面缓存目录,传 Some 时提取封面并写入缓存 +/// 避免重建 ffmpeg_audio 上下文。 pub fn start_decode( source: &str, shared: Arc, cover_cache_dir: Option<&str>, ) -> Result<(AudioMetadata, JoinHandle)> { - let mut input_ctx = open_input(source)?; - // 装中断回调:之后 stop 即可让阻塞中的 read_frame / seek_file 立即返回 - // 注:open_input 自身的 avformat_open_input 不在保护范围内(回调是 open 后才装的), - // 这一步靠 rw_timeout=15s 兜底 - let (interrupt_flag, interrupt_ctx) = install_interrupt(&mut input_ctx); - shared.bind_interrupt(Arc::clone(&interrupt_flag)); - - let stream = input_ctx - .streams() - .best(ffmpeg::media::Type::Audio) - .context("No audio stream found")?; - let audio_stream_index = stream.index(); - - // 时长:优先从流的 time_base 获取 - let time_base = stream.time_base(); - let stream_duration = stream.duration(); - let duration_secs = if stream_duration > 0 { - stream_duration as f64 * time_base.0 as f64 / time_base.1 as f64 - } else if input_ctx.duration() > 0 { - input_ctx.duration() as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE) - } else { - 0.0 - }; - - let tags = metadata::extract_tags(&input_ctx); + let (reader, interrupt_flag) = open_source(source)?; + if let Some(ref flag) = interrupt_flag { + shared.bind_interrupt(Arc::clone(flag)); + } - // SAFETY: input_ctx 在此作用域内有效,stream 引用自 input_ctx - let stream_info = unsafe { metadata::extract_stream_info(&stream, &input_ctx) }; - let codec = ffmpeg::codec::decoder::find(stream.parameters().id()) - .map(|c| c.name().to_string()) - .unwrap_or_default(); + let info = reader.source_info(); + let duration_secs = reader.duration().map(|d| d.as_secs_f64()).unwrap_or(0.0); + let stream_info = metadata::extract_stream_info(info); + let codec = info.codec_name.clone(); + let raw_metadata = reader.metadata(); + let tags = metadata::extract_tags(&raw_metadata); let cover = - cover_cache_dir.and_then(|dir| metadata::extract_cover_thumbnail(&input_ctx, source, dir)); - let cover_raw = metadata::read_attached_pic(&input_ctx); - let embedded_lyric = metadata::extract_embedded_lyric(&input_ctx); + cover_cache_dir.and_then(|dir| metadata::extract_cover_thumbnail(&reader, source, dir)); + let cover_raw = metadata::read_attached_pic(&reader); + let embedded_lyric = metadata::extract_embedded_lyric(&raw_metadata); let external_lyrics = metadata::find_all_external_lyrics(source); - let replay_gain_db = metadata::extract_replay_gain(&input_ctx); - - let decoder_ctx = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?; - let decoder = decoder_ctx.decoder().audio()?; - - let src_format = decoder.format(); - let src_layout = decoder.channel_layout(); - let src_rate = decoder.rate(); - - // 使用平面 F32 格式输出(与参考实现一致) - let target_format = ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar); - let target_layout = ChannelLayout::default(TARGET_CHANNELS as i32); - - let resampler = create_resampler( - src_format, - src_layout, - src_rate, - target_format, - target_layout, - TARGET_SAMPLE_RATE, - )?; + let replay_gain_db = metadata::extract_replay_gain(&raw_metadata); - let fft_resampler = create_resampler( - src_format, - src_layout, - src_rate, - ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), - ChannelLayout::MONO, - FFT_SAMPLE_RATE, - )?; - - // 设置归一化增益:有 ReplayGain tag 时直接使用,否则由实时分析器动态计算 + // 有 ReplayGain tag 时设固定增益,否则由实时分析器动态计算 if let Some(db) = replay_gain_db { shared.set_normalization_gain(metadata::db_to_linear(db)); } @@ -220,13 +101,8 @@ pub fn start_decode( }; let data = DecoderData { - input_ctx, - decoder, - audio_stream_index, - resampler, - fft_resampler, + reader, interrupt_flag, - _interrupt_ctx: interrupt_ctx, }; let handle = thread::spawn(move || { @@ -243,9 +119,10 @@ pub fn start_decode( } /// 用已有的 DecoderData 继续解码(seek 后复用) -/// 复用 input_ctx 上已经装好的中断回调,把它绑定到新的 shared 上 pub fn resume_decode(data: DecoderData, shared: Arc) -> JoinHandle { - shared.bind_interrupt(data.interrupt_handle()); + if let Some(flag) = data.interrupt_handle() { + shared.bind_interrupt(flag); + } thread::spawn(move || { let mut data = data; let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { @@ -256,66 +133,24 @@ pub fn resume_decode(data: DecoderData, shared: Arc) -> JoinHandle Result { - debug!(source, "打开音频输入"); - let is_network = source.starts_with("http://") || source.starts_with("https://"); - - if is_network { - let mut opts = Dictionary::new(); - opts.set("reconnect", "1"); - opts.set("reconnect_streamed", "1"); - opts.set("reconnect_delay_max", "5"); - // 新增:超时与连接复用,参见 https://ffmpeg.org/ffmpeg-protocols.html - opts.set("rw_timeout", HTTP_RW_TIMEOUT_US); - opts.set("timeout", HTTP_RW_TIMEOUT_US); // 老版本 ffmpeg 用这个名字 - opts.set("multiple_requests", "1"); - opts.set("icy", "0"); - opts.set("user_agent", "SPlayer-Next/1.0"); - ffmpeg::format::input_with_dictionary(source, opts) - .with_context(|| format!("Failed to open: {source}")) +/// 根据 source 协议打开音频:http(s) 走 HttpRangeSource + 拿 cancel flag,其他走本地 File +fn open_source(source: &str) -> Result<(AudioReader, Option>)> { + if http_source::is_network_source(source) { + let http = http_source::HttpRangeSource::new(source)?; + let cancel = http.cancel_handle(); + let reader = AudioReader::new(http, TARGET_SAMPLE_RATE as i32, TARGET_CHANNELS as i32) + .with_context(|| format!("打开网络音频失败: {source}"))?; + Ok((reader, Some(cancel))) } else { - ffmpeg::format::input(source).with_context(|| format!("Failed to open: {source}")) + let file = File::open(source).with_context(|| format!("打开本地文件失败: {source}"))?; + let reader = AudioReader::new(file, TARGET_SAMPLE_RATE as i32, TARGET_CHANNELS as i32) + .with_context(|| format!("打开本地音频失败: {source}"))?; + Ok((reader, None)) } } -/// 仅在格式/声道/采样率不同时创建重采样器 -fn create_resampler( - src_format: ffmpeg::format::Sample, - src_layout: ChannelLayout, - src_rate: u32, - target_format: ffmpeg::format::Sample, - target_layout: ChannelLayout, - target_rate: u32, -) -> Result> { - if src_format != target_format || src_layout != target_layout || src_rate != target_rate { - let resampler = ffmpeg::software::resampling::Context::get( - src_format, - src_layout, - src_rate, - target_format, - target_layout, - target_rate, - )?; - Ok(Some(resampler)) - } else { - Ok(None) - } -} - -/// 核心解码循环 +/// 核心解码循环:从 reader 拉交错 stereo f32,按 chunk 推入 shared fn run_decoding_loop(data: &mut DecoderData, shared: &Shared) { - let mut player_scratch = Vec::new(); - let mut fft_scratch = Vec::new(); - let mut eof_sent = false; - let mut consecutive_read_failures: u32 = 0; - // 响度归一化:有 ReplayGain 标签时用固定增益,否则用实时分析 let has_replay_gain = (shared.normalization_gain() - 1.0).abs() > f32::EPSILON; let mut loudness = LoudnessAnalyzer::new(); @@ -327,224 +162,42 @@ fn run_decoding_loop(data: &mut DecoderData, shared: &Shared) { return; } - let mut decoded = ffmpeg::frame::Audio::empty(); - match data.decoder.receive_frame(&mut decoded) { - Ok(()) => { - consecutive_read_failures = 0; - - // 部分文件不设置 channel_layout,需要填默认值 - if decoded.channel_layout().is_empty() { - let default_layout = ChannelLayout::default(decoded.channels() as i32); - decoded.set_channel_layout(default_layout); - } - - player_scratch.clear(); - fft_scratch.clear(); - - resample_frame(data, &mut decoded, &mut player_scratch, &mut fft_scratch); - - // 音量归一化 - if shared.is_normalization_enabled() { - let gain = if has_replay_gain { - // 有 ReplayGain 标签:用固定增益 - shared.normalization_gain() - } else { - // 无标签:实时分析动态增益 - loudness.process(&player_scratch) - }; - if (gain - 1.0).abs() > f32::EPSILON { - for sample in &mut player_scratch { - *sample *= gain; - } - } - } - - let chunk = AudioChunk { - player_samples: std::mem::take(&mut player_scratch), - fft_samples: std::mem::take(&mut fft_scratch), - }; - - shared.push(chunk); - } - Err(ffmpeg::Error::Eof) => { - // 解码器已 drain 完毕;resampler 内部还可能有 delay 样本,flush 一次再退 - flush_and_push_tail(data, shared); + let samples = match data.reader.receive_frame() { + Ok(Some(s)) => s.to_vec(), + Ok(None) => return, + Err(AudioError::Eof) => return, + Err(e) => { + warn!(error = %e, "解码失败"); + shared.mark_decode_failed(); return; } - Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::ffi::EAGAIN => { - if eof_sent { - // 已发送 EOF,解码器清空完毕,flush resampler 尾巴 - flush_and_push_tail(data, shared); - return; - } + }; - // 读取下一个数据包;Packet::read 能区分真正的 EOF 与读取失败 - let mut packet = ffmpeg::codec::packet::Packet::empty(); - match packet.read(&mut data.input_ctx) { - Ok(()) => { - if packet.stream() == data.audio_stream_index { - consecutive_read_failures = 0; - if data.decoder.send_packet(&packet).is_err() { - return; - } - } - // 非音频包:跳过 - } - Err(ffmpeg::Error::Eof) => { - // 真正的文件结束,刷新解码器 - let _ = data.decoder.send_eof(); - eof_sent = true; - } - Err(err) => { - // 读取失败:网络中断 / URL 失效 - consecutive_read_failures += 1; - warn!(error = %err, retries = consecutive_read_failures, "音频源读取失败"); - if consecutive_read_failures <= NETWORK_READ_RETRIES { - // 网络流可能恢复,等待后重试 - std::thread::sleep(std::time::Duration::from_millis( - NETWORK_RETRY_DELAY_MS, - )); - continue; - } - // 重试耗尽:标记音源失效,仍走 EOF 流程把已缓冲数据放完 - shared.mark_decode_failed(); - let _ = data.decoder.send_eof(); - eof_sent = true; - } - } - } - Err(err) => { - // 其他解码错误,重试后放弃 - consecutive_read_failures += 1; - warn!(error = %err, retries = consecutive_read_failures, "解码错误"); - if consecutive_read_failures <= NETWORK_READ_RETRIES { - std::thread::sleep(std::time::Duration::from_millis(NETWORK_RETRY_DELAY_MS)); - continue; - } - return; - } + if samples.is_empty() { + continue; } - } -} - -/// 对解码帧进行重采样,分别输出播放数据和 FFT 数据 -fn resample_frame( - data: &mut DecoderData, - decoded: &mut ffmpeg::frame::Audio, - player_buf: &mut Vec, - fft_buf: &mut Vec, -) { - // 播放重采样 - if let Some(ref mut resampler) = data.resampler { - if let Some(frame) = try_resample(resampler, decoded) { - interleave_planar_frame(player_buf, &frame, frame.samples()); - } - } else { - interleave_planar_frame(player_buf, decoded, decoded.samples()); - } - - // FFT 重采样(单声道) - if let Some(ref mut resampler) = data.fft_resampler { - if let Some(frame) = try_resample(resampler, decoded) { - let samples = frame.samples(); - if samples > 0 { - fft_buf.extend_from_slice(&frame.plane::(0)[..samples]); - } - } - } else { - let samples = decoded.samples(); - if samples > 0 { - fft_buf.extend_from_slice(&decoded.plane::(0)[..samples]); - } - } -} - -/// 执行重采样,为每次调用创建正确尺寸的输出帧 -fn try_resample( - resampler: &mut ffmpeg::software::resampling::Context, - decoded: &ffmpeg::frame::Audio, -) -> Option { - let output_samples = (decoded.samples() as f64 * resampler.output().rate as f64 - / decoded.rate() as f64) - .ceil() as usize; - let mut output_frame = ffmpeg::frame::Audio::new( - resampler.output().format, - output_samples, - resampler.output().channel_layout, - ); + let mut player_samples = samples; - if resampler.run(decoded, &mut output_frame).is_ok() { - Some(output_frame) - } else { - None - } -} + // FFT 取左声道(48k mono),fft.rs 直接用 TARGET_SAMPLE_RATE 不再单独重采样 + let fft_samples: Vec = player_samples.chunks_exact(2).map(|c| c[0]).collect(); -/// 排空 resampler 内部 delay buffer -/// EOF 时调用:swr_convert(NULL,0) 输出剩余样本,否则末尾 10–50ms 音频会丢失 -fn try_flush_resampler( - resampler: &mut ffmpeg::software::resampling::Context, -) -> Option { - // 4096 samples 是经验值:典型 resampler delay 几百到 2k 个样本,4k 留充足余量 - let mut output_frame = ffmpeg::frame::Audio::new( - resampler.output().format, - 4096, - resampler.output().channel_layout, - ); - if resampler.flush(&mut output_frame).is_ok() && output_frame.samples() > 0 { - Some(output_frame) - } else { - None - } -} - -/// 把 resampler 残留样本组装成最后一个 chunk 推进缓冲区 -/// 仅在解码器返回 EOF 时调用一次 -fn flush_and_push_tail(data: &mut DecoderData, shared: &Shared) { - let mut player_buf = Vec::new(); - let mut fft_buf = Vec::new(); - - if let Some(ref mut resampler) = data.resampler { - if let Some(frame) = try_flush_resampler(resampler) { - interleave_planar_frame(&mut player_buf, &frame, frame.samples()); - } - } - if let Some(ref mut resampler) = data.fft_resampler { - if let Some(frame) = try_flush_resampler(resampler) { - let samples = frame.samples(); - if samples > 0 { - fft_buf.extend_from_slice(&frame.plane::(0)[..samples]); + if shared.is_normalization_enabled() { + let gain = if has_replay_gain { + shared.normalization_gain() + } else { + loudness.process(&player_samples) + }; + if (gain - 1.0).abs() > f32::EPSILON { + for s in &mut player_samples { + *s *= gain; + } } } - } - if !player_buf.is_empty() || !fft_buf.is_empty() { + shared.push(AudioChunk { - player_samples: player_buf, - fft_samples: fft_buf, + player_samples, + fft_samples, }); } } - -/// 将平面格式(L L L... R R R...)转为交错格式(L R L R ...) -fn interleave_planar_frame( - sample_buffer: &mut Vec, - frame: &ffmpeg::frame::Audio, - samples_written: usize, -) { - if samples_written == 0 { - return; - } - let left_plane = &frame.plane::(0)[..samples_written]; - - if frame.channels() >= 2 { - let right_plane = &frame.plane::(1)[..samples_written]; - let interleaved = left_plane - .iter() - .zip(right_plane.iter()) - .flat_map(|(&l, &r)| [l, r]); - sample_buffer.extend(interleaved); - } else { - sample_buffer.extend(left_plane.iter().copied()); - } -} diff --git a/native/audio-engine/src/fft.rs b/native/audio-engine/src/fft.rs index cd9b9776..71c623fb 100644 --- a/native/audio-engine/src/fft.rs +++ b/native/audio-engine/src/fft.rs @@ -30,13 +30,6 @@ struct FftWorkBuffers { output: Vec, } -// SAFETY: FftAnalyzer 需要手动实现 Send+Sync,因为 fft_plan 字段类型为 -// Arc>,trait object 默认不含 Send+Sync bound。 -// 实际上 rustfft::FftPlanner 返回的所有实现都是线程安全的, -// 且 FftAnalyzer 的其余字段(Mutex, Mutex, u32)均为 Send+Sync。 -unsafe impl Send for FftAnalyzer {} -unsafe impl Sync for FftAnalyzer {} - impl FftAnalyzer { pub fn new(sample_rate: u32) -> Self { let mut planner = FftPlanner::::new(); diff --git a/native/audio-engine/src/http_source.rs b/native/audio-engine/src/http_source.rs new file mode 100644 index 00000000..30f544a0 --- /dev/null +++ b/native/audio-engine/src/http_source.rs @@ -0,0 +1,601 @@ +//! 把 HTTP/HTTPS 资源伪装成 Read + Seek 流喂给 ffmpeg_audio +//! +//! 行为对齐 Chrome