feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+380
@@ -0,0 +1,380 @@
|
||||
# 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 = "audio-loopback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"wasapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.16.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"napi-derive",
|
||||
"napi-sys",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "2.16.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[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-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[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 = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
||||
|
||||
[[package]]
|
||||
name = "wasapi"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f6b03b82e419f186fcdc06ac6068621bdadc88b89b2612067f1c021ad2c9449"
|
||||
dependencies = [
|
||||
"log",
|
||||
"num-integer",
|
||||
"widestring",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "audio-loopback"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
description = "Native Windows process-loopback audio capture for ChatApp Electron"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# Reduce binary size on release builds and avoid the default-features
|
||||
# panic-handler that bloats node addons. We don't strip — keeping symbols
|
||||
# helps debug crashes from a packaged build.
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
napi = { version = "2", default-features = false, features = ["napi6"] }
|
||||
napi-derive = "2"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# Match the Tauri reference exactly. Cargo.lock there pins 0.15.0; the
|
||||
# 0.15 family exposes AudioClient::new_application_loopback_client(pid,
|
||||
# include_tree) which is the load-bearing API for EXCLUDE_TARGET_PROCESS_TREE.
|
||||
wasapi = "0.15"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2"
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
// napi-rs build helper. Generates the platform-specific binding glue
|
||||
// (e.g. typedefs, init symbols) that `napi build` consumes. Required for
|
||||
// every napi-rs crate; the build will silently produce a half-wired
|
||||
// binding without it.
|
||||
|
||||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
/**
|
||||
* Start a process-loopback capture that excludes the current process
|
||||
* tree. The supplied JS callback is invoked from a background thread
|
||||
* with one argument: a Float32Array of interleaved f32 stereo samples
|
||||
* at 48kHz. Returns a numeric capture id that must be passed to
|
||||
* `stopCapture` when the share ends.
|
||||
*
|
||||
* Always excludes `std::process::id()` (whole-OS-mixer-minus-self).
|
||||
* For "include only this app" use `start_capture_for_pid` instead.
|
||||
*/
|
||||
export declare function startCapture(callback: (...args: any[]) => any): number
|
||||
/**
|
||||
* Start a process-loopback capture that INCLUDES the target PID's
|
||||
* process tree (and only that tree) — the WASAPI
|
||||
* INCLUDE_TARGET_PROCESS_TREE mode. Used for window-shares where we
|
||||
* want only the picked app's audio (Discord parity).
|
||||
*/
|
||||
export declare function startCaptureForPid(pid: number, callback: (...args: any[]) => any): number
|
||||
/**
|
||||
* Resolve the owning process id of a top-level window handle. The
|
||||
* renderer derives `hwnd` from desktopCapturer's `window:<HWND>:0`
|
||||
* source ids and we hand that to `start_capture_for_pid`.
|
||||
*/
|
||||
export declare function resolveWindowPid(hwnd: number): number
|
||||
/**
|
||||
* Tear down the capture for the given id. Safe to call on a missing id
|
||||
* (no-op) so the JS side doesn't have to track whether the stop has
|
||||
* already been issued by the screen-share teardown path.
|
||||
*/
|
||||
export declare function stopCapture(captureId: number): void
|
||||
@@ -0,0 +1,318 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
|
||||
const { platform, arch } = process
|
||||
|
||||
let nativeBinding = null
|
||||
let localFileExisted = false
|
||||
let loadError = null
|
||||
|
||||
function isMusl() {
|
||||
// For Node 10
|
||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||
try {
|
||||
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
||||
return readFileSync(lddPath, 'utf8').includes('musl')
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
const { glibcVersionRuntime } = process.report.getReport().header
|
||||
return !glibcVersionRuntime
|
||||
}
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'audio-loopback.android-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.android-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-android-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(join(__dirname, 'audio-loopback.android-arm-eabi.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.android-arm-eabi.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-android-arm-eabi')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.win32-x64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.win32-x64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-win32-x64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'ia32':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.win32-ia32-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.win32-ia32-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-win32-ia32-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.win32-arm64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.win32-arm64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-win32-arm64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
localFileExisted = existsSync(join(__dirname, 'audio-loopback.darwin-universal.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.darwin-universal.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-darwin-universal')
|
||||
}
|
||||
break
|
||||
} catch {}
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'audio-loopback.darwin-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.darwin-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-darwin-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.darwin-arm64.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.darwin-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-darwin-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'audio-loopback.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-x64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-x64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-x64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-x64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-x64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-x64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-arm64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-arm64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-arm64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-arm64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-arm64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-arm64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-arm-musleabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-arm-musleabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-arm-musleabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'riscv64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-riscv64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-riscv64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-riscv64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-riscv64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-riscv64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-riscv64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 's390x':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'audio-loopback.linux-s390x-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./audio-loopback.linux-s390x-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@chatapp/audio-loopback-native-linux-s390x-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadError) {
|
||||
throw loadError
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { startCapture, startCaptureForPid, resolveWindowPid, stopCapture } = nativeBinding
|
||||
|
||||
module.exports.startCapture = startCapture
|
||||
module.exports.startCaptureForPid = startCaptureForPid
|
||||
module.exports.resolveWindowPid = resolveWindowPid
|
||||
module.exports.stopCapture = stopCapture
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@chatapp/audio-loopback-native",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Native Windows process-loopback audio capture addon (excludes our own PID tree)",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"*.node"
|
||||
],
|
||||
"napi": {
|
||||
"name": "audio-loopback",
|
||||
"triples": {
|
||||
"defaults": false,
|
||||
"additional": [
|
||||
"x86_64-pc-windows-msvc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
"build:debug": "napi build --platform"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
// Native system-audio capture addon for the ChatApp Electron desktop
|
||||
// client. Mirrors the proven Tauri implementation
|
||||
// (apps/desktop/src-tauri/src/screen_audio.rs in the legacy repo).
|
||||
//
|
||||
// Why a native addon at all when Electron already exposes a 'loopback'
|
||||
// audio source through setDisplayMediaRequestHandler? Because Chromium's
|
||||
// loopback captures the entire OS mixer including our own renderer's
|
||||
// playback — peers in a video call hear themselves echoed back when the
|
||||
// sharer ticks "system audio". Windows ships an EXCLUDE_TARGET_PROCESS_TREE
|
||||
// process-loopback mode that captures every render session except the
|
||||
// targeted PID's tree. We pass our own PID so the LiveKit playback never
|
||||
// re-enters the outgoing share.
|
||||
//
|
||||
// Wire format is fixed at 48kHz interleaved f32 stereo. The renderer-
|
||||
// side AudioWorklet (lib/loopbackAudio.ts) assumes that layout and feeds
|
||||
// samples into a MediaStreamDestination so LiveKit publishes a plain
|
||||
// ScreenShareAudio track.
|
||||
//
|
||||
// Windows-only for v1. macOS/Linux stubs return a clear napi::Error so
|
||||
// the renderer can fall through to the existing getUserMedia path.
|
||||
|
||||
#![allow(clippy::needless_return)]
|
||||
|
||||
use napi::bindgen_prelude::{Float32Array, Result};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
};
|
||||
use napi::JsFunction;
|
||||
use napi_derive::napi;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::thread;
|
||||
|
||||
// Output format we always deliver to the frontend. Pinning a single fixed
|
||||
// format means the AudioWorklet never has to renegotiate — it just
|
||||
// assumes interleaved f32 stereo at 48kHz. WASAPI mix format is usually
|
||||
// already this on Windows 10+, so the resample/upmix branch inside
|
||||
// process-loopback's autoconvert is rarely hit.
|
||||
const OUTPUT_SAMPLE_RATE: u32 = 48_000;
|
||||
const OUTPUT_CHANNELS: u16 = 2;
|
||||
|
||||
static NEXT_ID: AtomicU32 = AtomicU32::new(1);
|
||||
static SESSIONS: OnceLock<Mutex<HashMap<u32, Session>>> = OnceLock::new();
|
||||
|
||||
fn sessions() -> &'static Mutex<HashMap<u32, Session>> {
|
||||
SESSIONS.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
struct Session {
|
||||
stop: Arc<AtomicBool>,
|
||||
handle: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// Start a process-loopback capture that excludes the current process
|
||||
/// tree. The supplied JS callback is invoked from a background thread
|
||||
/// with one argument: a Float32Array of interleaved f32 stereo samples
|
||||
/// at 48kHz. Returns a numeric capture id that must be passed to
|
||||
/// `stopCapture` when the share ends.
|
||||
///
|
||||
/// Always excludes `std::process::id()` (whole-OS-mixer-minus-self).
|
||||
/// For "include only this app" use `start_capture_for_pid` instead.
|
||||
#[napi]
|
||||
pub fn start_capture(callback: JsFunction) -> Result<u32> {
|
||||
spawn_capture_session(callback, std::process::id(), false)
|
||||
}
|
||||
|
||||
/// Start a process-loopback capture that INCLUDES the target PID's
|
||||
/// process tree (and only that tree) — the WASAPI
|
||||
/// INCLUDE_TARGET_PROCESS_TREE mode. Used for window-shares where we
|
||||
/// want only the picked app's audio (Discord parity).
|
||||
#[napi]
|
||||
pub fn start_capture_for_pid(pid: u32, callback: JsFunction) -> Result<u32> {
|
||||
spawn_capture_session(callback, pid, true)
|
||||
}
|
||||
|
||||
/// Resolve the owning process id of a top-level window handle. The
|
||||
/// renderer derives `hwnd` from desktopCapturer's `window:<HWND>:0`
|
||||
/// source ids and we hand that to `start_capture_for_pid`.
|
||||
#[napi]
|
||||
pub fn resolve_window_pid(hwnd: u32) -> Result<u32> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Minimal FFI to user32!GetWindowThreadProcessId — pulling in a
|
||||
// full windows crate just for one call would balloon build
|
||||
// times. The function returns the thread id (we ignore it) and
|
||||
// writes the process id through the pointer.
|
||||
#[allow(non_snake_case)]
|
||||
extern "system" {
|
||||
fn GetWindowThreadProcessId(hWnd: usize, lpdwProcessId: *mut u32) -> u32;
|
||||
}
|
||||
let mut pid: u32 = 0;
|
||||
// SAFETY: GetWindowThreadProcessId tolerates an invalid HWND
|
||||
// (returns 0 thread id and leaves *lpdwProcessId untouched). We
|
||||
// detect the failure case by checking for pid == 0 below.
|
||||
let thread_id = unsafe { GetWindowThreadProcessId(hwnd as usize, &mut pid) };
|
||||
if thread_id == 0 || pid == 0 {
|
||||
return Err(napi::Error::from_reason(format!(
|
||||
"GetWindowThreadProcessId({hwnd}) failed — window may have closed"
|
||||
)));
|
||||
}
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = hwnd;
|
||||
Err(napi::Error::from_reason(
|
||||
"resolve_window_pid only supported on Windows",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn spawn_capture_session(
|
||||
callback: JsFunction,
|
||||
pid: u32,
|
||||
include_tree: bool,
|
||||
) -> Result<u32> {
|
||||
// ErrorStrategy::Fatal — the JS callback signature is `(samples)`
|
||||
// not `(err, samples)`, so we don't want napi to inject an
|
||||
// error slot. If anything goes wrong on the Rust side we tear
|
||||
// the session down and stop calling the callback.
|
||||
let tsfn: ThreadsafeFunction<Float32Array, ErrorStrategy::Fatal> =
|
||||
callback.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value]))?;
|
||||
|
||||
let capture_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
|
||||
let handle = thread::Builder::new()
|
||||
.name(format!("audio-loopback-{capture_id}"))
|
||||
.spawn(move || {
|
||||
if let Err(err) = windows_loopback::capture_loop(
|
||||
capture_id,
|
||||
tsfn,
|
||||
stop_clone,
|
||||
pid,
|
||||
include_tree,
|
||||
) {
|
||||
eprintln!("audio-loopback {capture_id}: {err}");
|
||||
}
|
||||
})
|
||||
.map_err(|e| {
|
||||
napi::Error::from_reason(format!("failed to spawn audio thread: {e}"))
|
||||
})?;
|
||||
|
||||
sessions().lock().unwrap().insert(
|
||||
capture_id,
|
||||
Session {
|
||||
stop,
|
||||
handle: Some(handle),
|
||||
},
|
||||
);
|
||||
Ok(capture_id)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn spawn_capture_session(
|
||||
callback: JsFunction,
|
||||
_pid: u32,
|
||||
_include_tree: bool,
|
||||
) -> Result<u32> {
|
||||
let _ = callback;
|
||||
Err(napi::Error::from_reason(
|
||||
"system audio capture only supported on Windows",
|
||||
))
|
||||
}
|
||||
|
||||
/// Tear down the capture for the given id. Safe to call on a missing id
|
||||
/// (no-op) so the JS side doesn't have to track whether the stop has
|
||||
/// already been issued by the screen-share teardown path.
|
||||
#[napi]
|
||||
pub fn stop_capture(capture_id: u32) -> Result<()> {
|
||||
let session = sessions().lock().unwrap().remove(&capture_id);
|
||||
let Some(mut session) = session else {
|
||||
return Ok(());
|
||||
};
|
||||
session.stop.store(true, Ordering::Relaxed);
|
||||
if let Some(handle) = session.handle.take() {
|
||||
// Best-effort join — the capture loop polls `stop` every event
|
||||
// cycle (≤100ms) so this usually returns promptly. If a WASAPI
|
||||
// call is wedged we'd rather drop the handle than hang the JS
|
||||
// teardown path.
|
||||
let _ = handle.join();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Windows loopback implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_loopback {
|
||||
use super::*;
|
||||
use wasapi::{initialize_mta, AudioClient, Direction, SampleType, ShareMode, WaveFormat};
|
||||
|
||||
// 200ms request buffer in 100ns units. Process-loopback clients
|
||||
// ignore the period for shared-mode but the API still requires a
|
||||
// non-zero value — picking 200ms keeps wakeups infrequent enough
|
||||
// that we don't spin the capture thread on idle audio.
|
||||
const REQUESTED_BUFFER_HNS: i64 = 2_000_000;
|
||||
|
||||
pub fn capture_loop(
|
||||
capture_id: u32,
|
||||
tsfn: ThreadsafeFunction<Float32Array, ErrorStrategy::Fatal>,
|
||||
stop: Arc<AtomicBool>,
|
||||
pid: u32,
|
||||
include_tree: bool,
|
||||
) -> std::result::Result<(), String> {
|
||||
// COM must be initialised on every thread that touches WASAPI.
|
||||
// MTA is the right model for a background capture thread —
|
||||
// STA would require message pumping we don't want to add.
|
||||
initialize_mta()
|
||||
.ok()
|
||||
.map_err(|e| format!("initialize_mta: {e:?}"))?;
|
||||
|
||||
// Process-loopback. Two modes — selected by `include_tree`:
|
||||
// false → EXCLUDE_TARGET_PROCESS_TREE: every render session on
|
||||
// the box *except* the given PID's tree. Default for
|
||||
// full-screen shares so LiveKit playback stays out of
|
||||
// the outgoing audio (we pass our own PID).
|
||||
// true → INCLUDE_TARGET_PROCESS_TREE: only the given PID's
|
||||
// tree. Used for window-shares so the captured audio
|
||||
// is exactly the picked app (Discord parity).
|
||||
// The `include_tree` flag is the load-bearing arg to
|
||||
// `new_application_loopback_client`; do not flip without
|
||||
// re-reading the wasapi crate's docs.
|
||||
let mut audio_client =
|
||||
AudioClient::new_application_loopback_client(pid, include_tree)
|
||||
.map_err(|e| format!("new_application_loopback_client: {e:?}"))?;
|
||||
|
||||
// Process-loopback only accepts caller-specified formats —
|
||||
// `get_mixformat` is documented as broken on this client. We
|
||||
// pin the wire format we already deliver downstream: 48kHz,
|
||||
// 32-bit float, stereo. `autoconvert=true` (the trailing `true`
|
||||
// arg to initialize_client) lets WASAPI mix arbitrary session
|
||||
// formats into ours so games at 44.1k or mono notification
|
||||
// sounds don't blow up the capture.
|
||||
let wave_format = WaveFormat::new(
|
||||
32,
|
||||
32,
|
||||
&SampleType::Float,
|
||||
OUTPUT_SAMPLE_RATE as usize,
|
||||
OUTPUT_CHANNELS as usize,
|
||||
None,
|
||||
);
|
||||
|
||||
audio_client
|
||||
.initialize_client(
|
||||
&wave_format,
|
||||
REQUESTED_BUFFER_HNS,
|
||||
&Direction::Capture,
|
||||
&ShareMode::Shared,
|
||||
true,
|
||||
)
|
||||
.map_err(|e| format!("initialize_client (process-loopback): {e:?}"))?;
|
||||
|
||||
let h_event = audio_client
|
||||
.set_get_eventhandle()
|
||||
.map_err(|e| format!("set_get_eventhandle: {e:?}"))?;
|
||||
let capture_client = audio_client
|
||||
.get_audiocaptureclient()
|
||||
.map_err(|e| format!("get_audiocaptureclient: {e:?}"))?;
|
||||
|
||||
audio_client
|
||||
.start_stream()
|
||||
.map_err(|e| format!("start_stream: {e:?}"))?;
|
||||
|
||||
let block_align = wave_format.get_blockalign() as usize;
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
// 100ms timeout lets the loop check the stop flag even when
|
||||
// every excluded session is silent — there's nothing to
|
||||
// render so the event handle never fires.
|
||||
if h_event.wait_for_event(100).is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drain all packets available since the last wake — there
|
||||
// can be several queued if we were preempted.
|
||||
loop {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let frames_available = match capture_client.get_next_nbr_frames() {
|
||||
Ok(Some(n)) if n > 0 => n,
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"audio-loopback {capture_id}: get_next_nbr_frames: {e:?}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let bytes_needed = frames_available as usize * block_align;
|
||||
let mut raw = vec![0u8; bytes_needed];
|
||||
if let Err(e) = capture_client.read_from_device(&mut raw) {
|
||||
eprintln!(
|
||||
"audio-loopback {capture_id}: read_from_device: {e:?}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Reinterpret bytes as f32 little-endian samples. The
|
||||
// buffer is already 48kHz f32 stereo because process-
|
||||
// loopback autoconverted to our requested format. We
|
||||
// copy out into a Vec<f32> so napi can hand ownership
|
||||
// of a JS-owned ArrayBuffer to the renderer.
|
||||
let mut samples = Vec::<f32>::with_capacity(raw.len() / 4);
|
||||
let mut idx = 0;
|
||||
while idx + 4 <= raw.len() {
|
||||
let bytes = [raw[idx], raw[idx + 1], raw[idx + 2], raw[idx + 3]];
|
||||
samples.push(f32::from_le_bytes(bytes));
|
||||
idx += 4;
|
||||
}
|
||||
let _ = (OUTPUT_SAMPLE_RATE, OUTPUT_CHANNELS);
|
||||
|
||||
let payload = Float32Array::new(samples);
|
||||
// NonBlocking: never block the WASAPI capture thread on
|
||||
// a slow JS event loop — at 48kHz stereo a stalled
|
||||
// renderer would otherwise back-pressure the WASAPI
|
||||
// event handle and underrun every other consumer.
|
||||
let status = tsfn.call(payload, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
if status != napi::Status::Ok {
|
||||
// Renderer went away or the threadsafe function was
|
||||
// released — stop cleanly.
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = audio_client.stop_stream();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":2812790340253412174,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\denni\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0 (59807616e 2026-04-14)\nbinary: rustc\ncommit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860\ncommit-date: 2026-04-14\nhost: x86_64-pc-windows-msvc\nrelease: 1.95.0\nLLVM version: 22.1.2\n","stderr":""}},"successes":{}}
|
||||
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
e688a2af0c492af6
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":17257705230225558938,"path":12213129009505393428,"deps":[[1363051979936526615,"memchr",false,10675094612715872077]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\aho-corasick-9f0713aa5a0615e8\\dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
+1
@@ -0,0 +1 @@
|
||||
d4081d3eaff98543
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[]","target":5408242616063297496,"profile":17257705230225558938,"path":13767053534773805487,"deps":[[1035178698636953719,"napi_build",false,13969733624275323243]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\audio-loopback-12f5458902d8391e\\dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
be43ec00f8767e8d
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[2572457258515766174,"build_script_build",false,4865569503500372180]],"local":[{"RerunIfEnvChanged":{"var":"DEBUG_GENERATED_CODE","val":null}},{"RerunIfEnvChanged":{"var":"TYPE_DEF_TMP_PATH","val":"C:\\Users\\denni\\AppData\\Local\\Temp\\audio_loopback-a3c3b6f6.napi_type_def.tmp"}},{"RerunIfEnvChanged":{"var":"CARGO_CFG_NAPI_RS_CLI_VERSION","val":"2.18.4"}},{"RerunIfEnvChanged":{"var":"NAPI_DEBUG_GENERATED_CODE","val":null}},{"RerunIfEnvChanged":{"var":"NAPI_TYPE_DEF_TMP_FOLDER","val":null}},{"RerunIfEnvChanged":{"var":"NAPI_FORCE_BUILD_AUDIO_LOOPBACK","val":null}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
5516adc767f5abec
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[]","target":11760611160289367125,"profile":5353520049763563864,"path":10763286916239946207,"deps":[[2572457258515766174,"build_script_build",false,10195717413952832446],[13045677537521422049,"wasapi",false,17410940550348078650],[13423243174795060362,"napi_derive",false,2979073294737158417],[16099943211762415786,"napi",false,7346211637381484829]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\audio-loopback-6c6fd3fda356bdba\\dep-lib-audio_loopback","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
0ccbb31ddfd4265e
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":17257705230225558938,"path":18420280761196579094,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\autocfg-ef41a80ef2a4f8c4\\dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
3f3c2a22cbdb03dc
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[\"arbitrary\", \"bytemuck\", \"example_generated\", \"serde\", \"serde_core\", \"std\"]","target":7691312148208718491,"profile":16503403049695105087,"path":12664969178542693245,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\bitflags-56bc72c2ee6a11d2\\dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
fc3ac7ee25a03df6
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":13840298032947503755,"profile":17257705230225558938,"path":7789178187138679712,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\cfg-if-2de0fde34c98fcea\\dep-lib-cfg_if","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
c9711a2dd730566c
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[\"rand\", \"random\"]","target":13517390075341535229,"profile":17257705230225558938,"path":13073641468317742375,"deps":[[4341528441765018781,"unicode_segmentation",false,4009714942342982812]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\convert_case-cb245ff7b0d5094c\\dep-lib-convert_case","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
fcff169995187d99
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[\"used_linker\"]","target":16767752466166802488,"profile":17257705230225558938,"path":8223028202004655627,"deps":[[10420560437213941093,"syn",false,667617596471125619],[13111758008314797071,"quote",false,15488645298819577813]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\ctor-c44e8ff1492b4966\\dep-lib-ctor","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
48a82cf2ec9381fd
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[]","target":9378127968640496523,"profile":2301124911398833726,"path":15222717544052245301,"deps":[[6959378045035346538,"windows_link",false,12153921703720002459]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\libloading-c73fed9c19177dcc\\dep-lib-libloading","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
ddda7dfc68960487
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[\"kv\", \"kv_serde\", \"kv_std\", \"kv_sval\", \"kv_unstable\", \"kv_unstable_serde\", \"kv_unstable_std\", \"kv_unstable_sval\", \"max_level_debug\", \"max_level_error\", \"max_level_info\", \"max_level_off\", \"max_level_trace\", \"max_level_warn\", \"release_max_level_debug\", \"release_max_level_error\", \"release_max_level_info\", \"release_max_level_off\", \"release_max_level_trace\", \"release_max_level_warn\", \"serde\", \"serde_core\", \"std\", \"sval\", \"sval_ref\", \"value-bag\"]","target":6550155848337067049,"profile":16503403049695105087,"path":2935339677018414979,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\log-503ba197615b4899\\dep-lib-log","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
4d9f9dbefd8d2594
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"core\", \"default\", \"libc\", \"logging\", \"rustc-dep-of-std\", \"std\", \"use_std\"]","target":11745930252914242013,"profile":17257705230225558938,"path":10267584396946875985,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\memchr-0b6ded161300fb6d\\dep-lib-memchr","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
6bc9c269b776dec1
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[]","declared_features":"[]","target":9388202626367339685,"profile":17257705230225558938,"path":6355682921637151155,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\napi-build-547d402dbe5275e3\\dep-lib-napi_build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
1dd1f6f28ffcf265
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"napi1\", \"napi2\", \"napi3\", \"napi4\", \"napi5\", \"napi6\"]","declared_features":"[\"anyhow\", \"async\", \"chrono\", \"chrono_date\", \"compat-mode\", \"default\", \"deferred_trace\", \"dyn-symbols\", \"encoding_rs\", \"error_anyhow\", \"experimental\", \"full\", \"indexmap\", \"latin1\", \"napi1\", \"napi2\", \"napi3\", \"napi4\", \"napi5\", \"napi6\", \"napi7\", \"napi8\", \"napi9\", \"noop\", \"object_indexmap\", \"serde\", \"serde-json\", \"serde-json-ordered\", \"serde_json\", \"tokio\", \"tokio_fs\", \"tokio_full\", \"tokio_io_std\", \"tokio_io_util\", \"tokio_macros\", \"tokio_net\", \"tokio_process\", \"tokio_rt\", \"tokio_signal\", \"tokio_sync\", \"tokio_test_util\", \"tokio_time\"]","target":6604924358859142166,"profile":16503403049695105087,"path":12768161688960962680,"deps":[[900613073546913600,"napi_sys",false,1702221090865446476],[2571033484697105782,"bitflags",false,15853756778772315199],[5855319743879205494,"once_cell",false,4803485267941743741],[6606131838865521726,"ctor",false,11060023290689421308]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\napi-c71c5da813d1757f\\dep-lib-napi","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
1131a9b460cb5729
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"compat-mode\", \"default\", \"full\", \"strict\", \"type-def\"]","declared_features":"[\"compat-mode\", \"default\", \"full\", \"noop\", \"strict\", \"type-def\"]","target":2065430088197001673,"profile":17257705230225558938,"path":14139299789826547350,"deps":[[4289358735036141001,"proc_macro2",false,4386134274409224043],[5241157436998822951,"napi_derive_backend",false,14640003306665018262],[7667230146095136825,"cfg_if",false,17743514191689890556],[10420560437213941093,"syn",false,667617596471125619],[13111758008314797071,"quote",false,15488645298819577813],[13475460906694513802,"convert_case",false,7806480704826143177]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\napi-derive-8a06c7ca101ebb69\\dep-lib-napi_derive","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
960350fc70bd2bcb
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"regex\", \"semver\", \"strict\", \"type-def\"]","declared_features":"[\"noop\", \"regex\", \"semver\", \"strict\", \"type-def\"]","target":7459870077939534063,"profile":17257705230225558938,"path":18206335121874424249,"deps":[[4289358735036141001,"proc_macro2",false,4386134274409224043],[5855319743879205494,"once_cell",false,3458902346603695405],[9680020106200215617,"semver",false,3158118117038451264],[10420560437213941093,"syn",false,667617596471125619],[13111758008314797071,"quote",false,15488645298819577813],[13475460906694513802,"convert_case",false,7806480704826143177],[17109794424245468765,"regex",false,15429713168089850008]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\napi-derive-backend-b741fd0ea2b72c1a\\dep-lib-napi_derive_backend","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
4c7e363a10819f17
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"napi1\", \"napi2\", \"napi3\", \"napi4\", \"napi5\", \"napi6\"]","declared_features":"[\"dyn-symbols\", \"experimental\", \"libloading\", \"napi1\", \"napi2\", \"napi3\", \"napi4\", \"napi5\", \"napi6\", \"napi7\", \"napi8\", \"napi9\"]","target":7475771664120104103,"profile":16503403049695105087,"path":17503114133641832535,"deps":[[7883780462905440460,"libloading",false,18267044209476020296]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\napi-sys-9a4a05b3b03a98e6\\dep-lib-napi_sys","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
bbcfe6834526535c
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"i128\", \"std\"]","target":7628309033881264685,"profile":16503403049695105087,"path":4800420964197105753,"deps":[[5157631553186200874,"num_traits",false,3909741165336574495]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\num-integer-c4c848204269c844\\dep-lib-num_integer","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
1fba9b0be0304236
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"i128\", \"std\"]","declared_features":"[\"default\", \"i128\", \"libm\", \"std\"]","target":4278088450330190724,"profile":16503403049695105087,"path":7756100633843771872,"deps":[[5157631553186200874,"build_script_build",false,13166776042377898855]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\num-traits-03118305971f36e0\\dep-lib-num_traits","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
+1
@@ -0,0 +1 @@
|
||||
65ebf59182c2d99c
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"i128\", \"std\"]","declared_features":"[\"default\", \"i128\", \"libm\", \"std\"]","target":5408242616063297496,"profile":17257705230225558938,"path":9571220808474951353,"deps":[[13927012481677012980,"autocfg",false,6784343943421348620]],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\num-traits-358a4796e6961756\\dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
67eb0ecb0cc9b9b6
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[5157631553186200874,"build_script_build",false,11302278605913320293]],"local":[{"RerunIfChanged":{"output":"release\\build\\num-traits-63b50a34cdd7574c\\output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
7d5c7db76668a942
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"alloc\", \"default\", \"race\", \"std\"]","declared_features":"[\"alloc\", \"atomic-polyfill\", \"critical-section\", \"default\", \"parking_lot\", \"portable-atomic\", \"race\", \"std\", \"unstable\"]","target":17524666916136250164,"profile":16503403049695105087,"path":1942199980420204124,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\once_cell-58eed7c19356b807\\dep-lib-once_cell","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
2d0d14b25b7d0030
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":8891013984288978370,"features":"[\"alloc\", \"default\", \"race\", \"std\"]","declared_features":"[\"alloc\", \"atomic-polyfill\", \"critical-section\", \"default\", \"parking_lot\", \"portable-atomic\", \"race\", \"std\", \"unstable\"]","target":17524666916136250164,"profile":17257705230225558938,"path":1942199980420204124,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\once_cell-f5f3a7b099532975\\dep-lib-once_cell","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
+1
@@ -0,0 +1 @@
|
||||
49e497e513242101
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user