diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 26ea45179..f69a4b706 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -79,8 +79,6 @@ RUN apt-get update \ rpm \ unzip \ webp \ - xauth \ - xvfb \ xz-utils \ xdg-utils \ zstd \ @@ -99,8 +97,6 @@ RUN apt-get update \ hyperfine \ iproute2 \ iputils-ping \ - kind \ - kubernetes-client \ lldb \ lsof \ ltrace \ @@ -177,7 +173,6 @@ COPY --from=seaweedfs /usr/bin/weed /usr/local/bin/weed USER ${USERNAME} ENV DOCKER_HOST="unix:///var/run/docker.sock" \ - KUBECONFIG="/workspaces/fluxer/.fluxer/k8s/local-kubeconfig" \ PATH="/home/${USERNAME}/.cargo/bin:${PATH}" \ PNPM_HOME="/home/${USERNAME}/.local/share/pnpm" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 38b77fbad..c04ad8df0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,8 +6,7 @@ "shutdownAction": "stopCompose", "remoteUser": "vscode", "remoteEnv": { - "DOCKER_HOST": "unix:///var/run/docker.sock", - "KUBECONFIG": "/workspaces/fluxer/.fluxer/k8s/local-kubeconfig" + "DOCKER_HOST": "unix:///var/run/docker.sock" }, "runServices": ["workspace", "postgres", "valkey", "nats", "livekit", "meilisearch", "mailpit"], "forwardPorts": [ diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5ab094fab..39436a414 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -16,7 +16,6 @@ services: FLUXER_POSTGRES_HOST: postgres FLUXER_SELF_HOSTED: "true" DOCKER_HOST: unix:///var/run/docker.sock - KUBECONFIG: /workspaces/fluxer/.fluxer/k8s/local-kubeconfig volumes: - ..:/workspaces/fluxer:cached - type: volume diff --git a/.devcontainer/verify.sh b/.devcontainer/verify.sh index 8524d5d77..96c422c12 100755 --- a/.devcontainer/verify.sh +++ b/.devcontainer/verify.sh @@ -75,8 +75,6 @@ else fi stage "rust: app proxy tests" cargo test -p fluxer_app_proxy -stage "integration: font serving" cargo run -q -p fluxer-dev -- font-serving-it - echo echo "---------------------------------------------" echo "${PASSED} stages passed, ${#FAILURES[@]} failed" diff --git a/.dockerignore b/.dockerignore index b1b661c7a..09819319c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,11 +27,6 @@ **/target/ **/test-results.json /fluxer_docs/site/ -/tools/integration/integration-driver/Cargo.lock -/tools/integration/integration-driver/target-*/ -/tools/integration/results/*.json -/tools/integration/results/latest-summary.txt - /fluxer_app/.devserver-cache.json /fluxer_app/pkgs/libfluxcore/ /fluxer_app/src/features/i18n/locales/*/messages.mjs diff --git a/.github/workflows/build-app-proxy.yaml b/.github/workflows/build-app-proxy.yaml index 765713b03..90cd31484 100644 --- a/.github/workflows/build-app-proxy.yaml +++ b/.github/workflows/build-app-proxy.yaml @@ -134,13 +134,6 @@ jobs: cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-app-proxy --step upload_assets - - name: verify bundled faces are CORS-usable from the app origin - run: >- - deploy/scripts/verify-asset-cors.sh - --endpoint https://fluxerstatic.com - --origin https://fluxer.app - --manifest app-dist-output/dist/assets-manifest.txt - build-arm64: name: build app-proxy (arm64) needs: meta diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index efb8791be..ed3111f83 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -234,56 +234,6 @@ jobs: - name: Verify shipped fonts match the lockfile run: python3 tools/fonts/build_fonts.py --verify - font-serving: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 - with: - toolchain: stable - - - name: Install pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 - - - name: Install Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e - with: - node-version: '24' - cache: 'pnpm' - - - name: Cache cargo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: rust-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} - restore-keys: | - rust-${{ runner.os }}- - - - name: Install native dependencies - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - pkg-config \ - build-essential \ - libcurl4-openssl-dev \ - libvips-dev \ - libavfilter-dev \ - libheif-dev \ - libwebp-dev - - - name: Install Node.js dependencies - run: pnpm --filter fluxer_admin --filter fluxer_marketing install - - - name: Run the font serving integration suite - run: cargo run --locked -q -p fluxer-dev -- font-serving-it - ci-scripts: runs-on: ubuntu-24.04 timeout-minutes: 25 diff --git a/Cargo.lock b/Cargo.lock index a9687eea3..d86d4a1ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1926,9 +1926,7 @@ dependencies = [ "anyhow", "axum", "base64", - "chrono", "clap", - "futures-util", "hmac 0.13.0", "hyper 1.10.1", "hyper-util", @@ -1942,9 +1940,7 @@ dependencies = [ "sha2 0.11.0", "tempfile", "tokio", - "tokio-tungstenite", "url", - "urlencoding", ] [[package]] @@ -5549,22 +5545,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "rustls 0.23.40", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -5742,24 +5722,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http 1.4.2", - "httparse", - "log", - "rand 0.9.4", - "rustls 0.23.40", - "rustls-pki-types", - "sha1 0.10.6", - "thiserror", -] - [[package]] name = "twox-hash" version = "2.1.2" diff --git a/deploy/scripts/verify-asset-cors.sh b/deploy/scripts/verify-asset-cors.sh deleted file mode 100755 index 30592cffa..000000000 --- a/deploy/scripts/verify-asset-cors.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -euo pipefail - -ENDPOINT="${ASSET_CORS_ENDPOINT:-https://fluxerstatic.com}" -MANIFEST="${ASSET_MANIFEST:-app-dist-output/dist/assets-manifest.txt}" -ORIGIN="${ASSET_CORS_ORIGIN:-https://fluxer.app}" -SAMPLES="${ASSET_CORS_SAMPLES:-5}" -EXPLICIT_KEYS=() - -usage() { - sed -n '3,22p' "$0" - exit "${1:-2}" -} - -while [ $# -gt 0 ]; do - case "$1" in - --endpoint) - ENDPOINT="${2:?--endpoint needs a URL}" - shift 2 - ;; - --manifest) - MANIFEST="${2:?--manifest needs a path}" - shift 2 - ;; - --origin) - ORIGIN="${2:?--origin needs an origin}" - shift 2 - ;; - --samples) - SAMPLES="${2:?--samples needs a count}" - shift 2 - ;; - --key) - EXPLICIT_KEYS+=("${2:?--key needs an S3 key}") - shift 2 - ;; - -h | --help) usage 0 ;; - *) - echo "verify-asset-cors: unknown argument '$1'" >&2 - usage 2 - ;; - esac -done - -ENDPOINT="${ENDPOINT%/}" - -keys=() -if [ "${#EXPLICIT_KEYS[@]}" -gt 0 ]; then - keys=("${EXPLICIT_KEYS[@]}") -elif [ -f "$MANIFEST" ]; then - while IFS= read -r key; do - keys+=("$key") - done < <(grep -E '\.woff2$' "$MANIFEST" | awk -v n="$SAMPLES" ' - { lines[NR] = $0 } - END { - if (NR == 0) exit - if (n > NR) n = NR - for (i = 0; i < n; i++) print lines[int(i * NR / n) + 1] - }') -else - cat >&2 <<-EOF - verify-asset-cors: no asset manifest at '$MANIFEST' and no --key given. - - The manifest is produced by \`build-app-proxy --step generate_asset_manifest\` and uploaded - by the release workflow as the 'app-proxy-assets-manifest' artifact. Pass --manifest to point - at a downloaded copy, or --key to check a single face by hand. - EOF - exit 2 -fi - -if [ "${#keys[@]}" -eq 0 ]; then - echo "verify-asset-cors: no .woff2 keys to check (manifest '$MANIFEST' has none)" >&2 - exit 2 -fi - -echo "verify-asset-cors: endpoint=$ENDPOINT origin=$ORIGIN keys=${#keys[@]}" - -failures=0 -for key in "${keys[@]}"; do - url="$ENDPOINT/$key" - headers="$(curl --silent --show-error --location --max-time 20 \ - --header "Origin: $ORIGIN" --output /dev/null --dump-header - "$url" 2>&1 || true)" - - status="$(printf '%s\n' "$headers" | awk '/^HTTP\//{code=$2} END{print code+0}')" - header_value() { - printf '%s\n' "$headers" | - tr -d '\r' | - awk -v want="$1" 'index(tolower($0), want ":") == 1 {sub(/^[^:]*:[ \t]*/, ""); v=$0} END{print v}' - } - acao="$(header_value 'access-control-allow-origin')" - ctype="$(header_value 'content-type')" - cache="$(header_value 'cache-control')" - cdn_cache="$(header_value 'cdn-cache-control')" - - problems=() - [ "$status" = "200" ] || problems+=("status=$status (want 200)") - if [ -z "$acao" ]; then - problems+=("no Access-Control-Allow-Origin -- the browser will drop this face") - elif [ "$acao" != "*" ] && [ "$acao" != "$ORIGIN" ]; then - problems+=("Access-Control-Allow-Origin='$acao' (want '*' or '$ORIGIN')") - fi - case "$ctype" in - font/woff2*) ;; - *) problems+=("Content-Type='$ctype' (want font/woff2)") ;; - esac - caching_note="" - case "$cache$cdn_cache" in - *immutable*) ;; - *) caching_note=" (note: no 'immutable' token; cache-control='$cache')" ;; - esac - - if [ "${#problems[@]}" -eq 0 ]; then - echo " OK $key [$status $ctype acao=$acao]$caching_note" - else - failures=$((failures + 1)) - echo " FAIL $key" - for problem in "${problems[@]}"; do - echo " - $problem" - done - fi -done - -if [ "$failures" -gt 0 ]; then - cat >&2 <<-EOF - - verify-asset-cors: $failures of ${#keys[@]} sampled faces are NOT usable from $ORIGIN. - - Most likely cause: the bucket's CORS rule is scoped to the retired 'fonts/' prefix and does - not cover 'assets/'. Every bundled face fails in production while JS and CSS keep working, - and the CDN still answers 200, so no server-side alarm fires. - - Fix the bucket rule so it allows GET on 'assets/*' from the app origin (or bucket-wide), - then re-run this check. - EOF - exit 1 -fi - -echo "verify-asset-cors: all ${#keys[@]} sampled faces are usable from $ORIGIN" diff --git a/fluxer_admin/tests/parity.rs b/fluxer_admin/tests/parity.rs index 2ad396a2a..3b84fa91f 100644 --- a/fluxer_admin/tests/parity.rs +++ b/fluxer_admin/tests/parity.rs @@ -4,22 +4,11 @@ mod parity_support; use parity_support::{ - PARITY_RUN_ENV, PROTECTED_ROUTES_ENV, PUBLIC_ROUTES_ENV, TEST_ACCESS_TOKEN, TEST_ADMIN_SECRET, - TEST_ADMIN_USER_ID, api_fixtures, capture, env_flag, html_normalizer, reference_worktree, - route_list_from_env, servers, + TEST_ACCESS_TOKEN, TEST_ADMIN_SECRET, TEST_ADMIN_USER_ID, api_fixtures, capture, + html_normalizer, rust_server, }; use std::{error::Error, io}; -const DEFAULT_PUBLIC_ROUTES: &[&str] = &["/_health", "/robots.txt", "/static/app.css", "/login"]; -const DEFAULT_PROTECTED_ROUTES: &[&str] = &[ - "/dashboard", - "/users?q=Parity", - "/guilds?q=Parity", - "/guilds/1600000000000000001", - "/reports", - "/reports/1700000000000000001", -]; - #[test] fn html_normalizer_canonicalizes_attribute_order_and_csrf_values() { let left = r#"
Open
"#; @@ -64,7 +53,7 @@ async fn rust_admin_fixture_routes_cover_default_protected_routes() -> Result<() let api_server = api_fixtures::ApiFixtureServer::start_default() .await .map_err(test_error)?; - let rust_admin = servers::start_rust_admin(api_server.base_url()) + let rust_admin = rust_server::start(api_server.base_url()) .await .map_err(test_error)?; let client = capture::capture_client().map_err(test_error)?; @@ -115,68 +104,6 @@ async fn rust_admin_fixture_routes_cover_default_protected_routes() -> Result<() Ok(()) } -#[tokio::test(flavor = "multi_thread")] -#[ignore = "set FLUXER_ADMIN_PARITY_RUN=1 to create/use the TS worktree and run dual-server parity"] -async fn dual_server_static_public_and_protected_routes() -> Result<(), Box> { - if !env_flag(PARITY_RUN_ENV) { - eprintln!("skipping dual-server parity; set {PARITY_RUN_ENV}=1 to run it"); - return Ok(()); - } - let repo_root = repo_root()?; - let api_server = api_fixtures::ApiFixtureServer::start_default() - .await - .map_err(test_error)?; - let worktree = reference_worktree::ensure_reference_worktree(&repo_root).map_err(test_error)?; - reference_worktree::prepare_reference_package(&worktree).map_err(test_error)?; - let ts_port = servers::reserve_local_port().map_err(test_error)?; - let ts_admin = servers::start_ts_admin(&worktree, ts_port, api_server.base_url()) - .await - .map_err(test_error)?; - let rust_admin = servers::start_rust_admin(api_server.base_url()) - .await - .map_err(test_error)?; - let client = capture::capture_client().map_err(test_error)?; - let public_routes = route_list_from_env(PUBLIC_ROUTES_ENV, DEFAULT_PUBLIC_ROUTES); - for route in public_routes { - capture::compare_route( - &client, - &route, - ts_admin.base_url(), - rust_admin.base_url(), - None, - ) - .await - .map_err(test_error)?; - } - let session = fluxer_admin::session::create_session( - TEST_ADMIN_USER_ID, - TEST_ACCESS_TOKEN, - TEST_ADMIN_SECRET, - ); - let session_cookie = format!("{}={session}", fluxer_admin::session::SESSION_COOKIE_NAME); - let protected_routes = route_list_from_env(PROTECTED_ROUTES_ENV, DEFAULT_PROTECTED_ROUTES); - for route in protected_routes { - capture::compare_route( - &client, - &route, - ts_admin.base_url(), - rust_admin.base_url(), - Some(&session_cookie), - ) - .await - .map_err(test_error)?; - } - Ok(()) -} - -fn repo_root() -> Result> { - let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir - .parent() - .map(std::path::Path::to_path_buf) - .ok_or_else(|| test_error("fluxer_admin must have a repository parent".to_owned())) -} - fn test_error(message: String) -> Box { Box::new(io::Error::other(message)) } diff --git a/fluxer_admin/tests/parity/capture.rs b/fluxer_admin/tests/parity/capture.rs index 89fdb5066..56d79ab77 100644 --- a/fluxer_admin/tests/parity/capture.rs +++ b/fluxer_admin/tests/parity/capture.rs @@ -18,24 +18,6 @@ pub fn capture_client() -> Result { .map_err(|error| format!("failed to build capture client: {error}")) } -pub async fn compare_route( - client: &Client, - route: &str, - ts_base_url: &str, - rust_base_url: &str, - cookie: Option<&str>, -) -> Result<(), String> { - let ts = fetch_route(client, ts_base_url, route, cookie).await?; - let rust = fetch_route(client, rust_base_url, route, cookie).await?; - if ts == rust { - return Ok(()); - } - Err(format!( - "parity mismatch for {route}\nTS: {ts:#?}\nRust: {rust:#?}\nfirst body diff: {}", - first_body_diff(&ts.body, &rust.body) - )) -} - pub async fn fetch_route( client: &Client, base_url: &str, @@ -73,25 +55,3 @@ pub async fn fetch_route( body, }) } - -fn first_body_diff(left: &str, right: &str) -> String { - let left_chars = left.chars().collect::>(); - let right_chars = right.chars().collect::>(); - let max_len = left_chars.len().max(right_chars.len()); - for index in 0..max_len { - if left_chars.get(index) != right_chars.get(index) { - let left_preview = preview_from(&left_chars, index); - let right_preview = preview_from(&right_chars, index); - return format!("at char {index}: left `{left_preview}`, right `{right_preview}`"); - } - } - "bodies differ but no character diff was found".to_owned() -} - -fn preview_from(chars: &[char], start: usize) -> String { - chars - .iter() - .skip(start.saturating_sub(20)) - .take(80) - .collect::() -} diff --git a/fluxer_admin/tests/parity/mod.rs b/fluxer_admin/tests/parity/mod.rs index a7ec1e918..267f5362e 100644 --- a/fluxer_admin/tests/parity/mod.rs +++ b/fluxer_admin/tests/parity/mod.rs @@ -3,47 +3,8 @@ pub mod api_fixtures; pub mod capture; pub mod html_normalizer; -pub mod reference_worktree; -pub mod servers; +pub mod rust_server; -use std::env; - -pub const TS_REFERENCE_COMMIT: &str = "4748f2f1e6589c325fca6391d6df3e3c3f6a0345^"; -pub const PARITY_RUN_ENV: &str = "FLUXER_ADMIN_PARITY_RUN"; -pub const PUBLIC_ROUTES_ENV: &str = "FLUXER_ADMIN_PARITY_PUBLIC_ROUTES"; -pub const PROTECTED_ROUTES_ENV: &str = "FLUXER_ADMIN_PARITY_PROTECTED_ROUTES"; -pub const TS_WORKTREE_ENV: &str = "FLUXER_ADMIN_PARITY_TS_WORKTREE"; -pub const TS_WORKTREE_ROOT_ENV: &str = "FLUXER_ADMIN_PARITY_WORKTREE_ROOT"; -pub const SKIP_TS_PREPARE_ENV: &str = "FLUXER_ADMIN_PARITY_SKIP_TS_PREPARE"; pub const TEST_ADMIN_SECRET: &str = "test-admin-secret"; pub const TEST_ADMIN_USER_ID: &str = "1130650140672000000"; pub const TEST_ACCESS_TOKEN: &str = "parity-access-token"; - -pub fn env_flag(name: &str) -> bool { - env::var(name) - .map(|value| { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "on" - ) - }) - .unwrap_or(false) -} - -pub fn route_list_from_env(name: &str, default: &[&str]) -> Vec { - match env::var(name) { - Ok(value) => value - .split(',') - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(|value| { - if value.starts_with('/') { - value.to_owned() - } else { - format!("/{value}") - } - }) - .collect(), - Err(_) => default.iter().map(|route| (*route).to_owned()).collect(), - } -} diff --git a/fluxer_admin/tests/parity/reference_worktree.rs b/fluxer_admin/tests/parity/reference_worktree.rs deleted file mode 100644 index 8ba17ce9a..000000000 --- a/fluxer_admin/tests/parity/reference_worktree.rs +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use super::{ - SKIP_TS_PREPARE_ENV, TS_REFERENCE_COMMIT, TS_WORKTREE_ENV, TS_WORKTREE_ROOT_ENV, env_flag, -}; -use std::{ - env, fs, - path::{Path, PathBuf}, - process::Command, -}; - -pub fn ensure_reference_worktree(repo_root: &Path) -> Result { - let target_commit = git_stdout(repo_root, &["rev-parse", TS_REFERENCE_COMMIT])?; - if let Ok(path) = env::var(TS_WORKTREE_ENV) { - let path = PathBuf::from(path); - validate_worktree(&path, &target_commit)?; - return Ok(path); - } - let root = env::var(TS_WORKTREE_ROOT_ENV) - .map(PathBuf::from) - .unwrap_or_else(|_| repo_root.join("target/parity")); - fs::create_dir_all(&root) - .map_err(|error| format!("failed to create {}: {error}", root.display()))?; - let worktree = root.join("fluxer-admin-ts-ref-4748f2f1-parent"); - if !worktree.exists() { - run_git( - repo_root, - &[ - "worktree", - "add", - "--detach", - path_arg(&worktree).as_str(), - TS_REFERENCE_COMMIT, - ], - )?; - } - validate_worktree(&worktree, &target_commit)?; - Ok(worktree) -} - -pub fn prepare_reference_package(worktree: &Path) -> Result<(), String> { - if env_flag(SKIP_TS_PREPARE_ENV) { - return Ok(()); - } - run_command( - Command::new("pnpm") - .current_dir(worktree) - .args(["install", "--frozen-lockfile"]), - "pnpm install --frozen-lockfile", - )?; - run_command( - Command::new("pnpm") - .current_dir(worktree) - .args(["--filter", "@fluxer/config", "generate"]), - "pnpm --filter @fluxer/config generate", - )?; - run_command( - Command::new("pnpm") - .current_dir(worktree) - .args(["--filter", "fluxer_admin", "build:css"]), - "pnpm --filter fluxer_admin build:css", - ) -} - -fn validate_worktree(worktree: &Path, target_commit: &str) -> Result<(), String> { - if !worktree.join("fluxer_admin/package.json").exists() { - return Err(format!( - "{} does not look like the TS reference worktree", - worktree.display() - )); - } - let head = git_stdout(worktree, &["rev-parse", "HEAD"])?; - if head != target_commit { - return Err(format!( - "{} is at {head}, expected {target_commit}", - worktree.display() - )); - } - Ok(()) -} - -fn run_git(repo: &Path, args: &[&str]) -> Result<(), String> { - run_command(Command::new("git").current_dir(repo).args(args), "git") -} - -fn git_stdout(repo: &Path, args: &[&str]) -> Result { - let output = Command::new("git") - .current_dir(repo) - .args(args) - .output() - .map_err(|error| format!("failed to run git {}: {error}", args.join(" ")))?; - if !output.status.success() { - return Err(format!( - "git {} failed\nstdout:\n{}\nstderr:\n{}", - args.join(" "), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) -} - -fn run_command(command: &mut Command, label: &str) -> Result<(), String> { - let output = command - .output() - .map_err(|error| format!("failed to run {label}: {error}"))?; - if output.status.success() { - return Ok(()); - } - Err(format!( - "{label} failed with status {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )) -} - -fn path_arg(path: &Path) -> String { - path.to_string_lossy().into_owned() -} diff --git a/fluxer_admin/tests/parity/rust_server.rs b/fluxer_admin/tests/parity/rust_server.rs new file mode 100644 index 000000000..142d85f0d --- /dev/null +++ b/fluxer_admin/tests/parity/rust_server.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::TEST_ADMIN_SECRET; +use fluxer_admin::{ + build_router, + config::{AdminConfig, ProxyConfig, RuntimeEnv}, +}; +use std::time::{Duration, Instant}; +use tokio::{net::TcpListener, task::JoinHandle, time::sleep}; + +pub struct RunningRustAdmin { + base_url: String, + handle: JoinHandle<()>, +} + +impl RunningRustAdmin { + pub fn base_url(&self) -> &str { + &self.base_url + } +} + +impl Drop for RunningRustAdmin { + fn drop(&mut self) { + self.handle.abort(); + } +} + +pub async fn start(api_endpoint: &str) -> Result { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .map_err(|error| format!("failed to bind Rust admin server: {error}"))?; + let port = listener + .local_addr() + .map_err(|error| format!("failed to read Rust admin address: {error}"))? + .port(); + let base_url = format!("http://127.0.0.1:{port}"); + let config = admin_config(port, api_endpoint, &base_url); + let router = build_router(config); + let handle = tokio::spawn(async move { + let _ = axum::serve(listener, router).await; + }); + wait_for_health(&base_url).await?; + Ok(RunningRustAdmin { base_url, handle }) +} + +fn admin_config(port: u16, api_endpoint: &str, admin_endpoint: &str) -> AdminConfig { + AdminConfig { + env: RuntimeEnv::Test, + host: "127.0.0.1".to_owned(), + port, + secret_key_base: TEST_ADMIN_SECRET.to_owned(), + base_path: String::new(), + api_endpoint: api_endpoint.to_owned(), + media_endpoint: format!("{api_endpoint}/media"), + static_cdn_endpoint: "https://static.example.test".to_owned(), + admin_endpoint: admin_endpoint.to_owned(), + web_app_endpoint: "http://127.0.0.1:8088".to_owned(), + kv_url: "redis://127.0.0.1:6379/0".to_owned(), + oauth_client_id: "1234567890123456789".to_owned(), + oauth_client_secret: "test-admin-oauth-secret".to_owned(), + oauth_redirect_uri: format!("{admin_endpoint}/oauth2_callback"), + build_version: "parity".to_owned(), + release_channel: "parity".to_owned(), + self_hosted: false, + proxy: ProxyConfig { + trust_client_ip_header: false, + client_ip_header_name: "x-forwarded-for".to_owned(), + }, + } +} + +async fn wait_for_health(base_url: &str) -> Result<(), String> { + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| format!("failed to build health client: {error}"))?; + let deadline = Instant::now() + Duration::from_secs(30); + let url = format!("{}/_health", base_url.trim_end_matches('/')); + let mut last_error = String::new(); + while Instant::now() < deadline { + match client.get(&url).send().await { + Ok(response) if response.status().is_success() => return Ok(()), + Ok(response) => last_error = format!("health returned {}", response.status()), + Err(error) => last_error = error.to_string(), + } + sleep(Duration::from_millis(200)).await; + } + Err(format!("timed out waiting for {url}: {last_error}")) +} diff --git a/fluxer_admin/tests/parity/servers.rs b/fluxer_admin/tests/parity/servers.rs deleted file mode 100644 index 04811cb03..000000000 --- a/fluxer_admin/tests/parity/servers.rs +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use super::TEST_ADMIN_SECRET; -use fluxer_admin::{ - build_router, - config::{AdminConfig, ProxyConfig, RuntimeEnv}, -}; -use std::{ - net::TcpListener as StdTcpListener, - path::Path, - process::{Child, Command, Stdio}, - time::{Duration, Instant}, -}; -use tokio::{net::TcpListener, task::JoinHandle, time::sleep}; - -pub struct RunningRustAdmin { - base_url: String, - handle: JoinHandle<()>, -} - -pub struct RunningTsAdmin { - base_url: String, - child: Child, -} - -impl RunningRustAdmin { - pub fn base_url(&self) -> &str { - &self.base_url - } -} - -impl RunningTsAdmin { - pub fn base_url(&self) -> &str { - &self.base_url - } -} - -impl Drop for RunningRustAdmin { - fn drop(&mut self) { - self.handle.abort(); - } -} - -impl Drop for RunningTsAdmin { - fn drop(&mut self) { - let _ = self.child.kill(); - let _ = self.child.wait(); - } -} - -pub fn reserve_local_port() -> Result { - let listener = StdTcpListener::bind("127.0.0.1:0") - .map_err(|error| format!("failed to reserve local port: {error}"))?; - listener - .local_addr() - .map(|addr| addr.port()) - .map_err(|error| format!("failed to read reserved local port: {error}")) -} - -pub async fn start_rust_admin(api_endpoint: &str) -> Result { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .map_err(|error| format!("failed to bind Rust admin server: {error}"))?; - let port = listener - .local_addr() - .map_err(|error| format!("failed to read Rust admin address: {error}"))? - .port(); - let base_url = format!("http://127.0.0.1:{port}"); - let config = admin_config(port, api_endpoint, &base_url); - let router = build_router(config); - let handle = tokio::spawn(async move { - let _ = axum::serve(listener, router).await; - }); - wait_for_health(&base_url).await?; - Ok(RunningRustAdmin { base_url, handle }) -} - -pub async fn start_ts_admin( - worktree: &Path, - port: u16, - api_endpoint: &str, -) -> Result { - let base_url = format!("http://127.0.0.1:{port}"); - let mut command = Command::new("pnpm"); - command - .current_dir(worktree) - .args(["--filter", "fluxer_admin", "start"]) - .env("BUILD_VERSION", "parity") - .env("RELEASE_CHANNEL", "parity"); - for (key, value) in ts_admin_env(port, api_endpoint, &base_url) { - command.env(key, value); - } - let child = command - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map_err(|error| format!("failed to start TS admin server: {error}"))?; - let mut server = RunningTsAdmin { base_url, child }; - if let Err(error) = wait_for_health(server.base_url()).await { - let _ = server.child.kill(); - let _ = server.child.wait(); - return Err(error); - } - Ok(server) -} - -fn admin_config(port: u16, api_endpoint: &str, admin_endpoint: &str) -> AdminConfig { - AdminConfig { - env: RuntimeEnv::Test, - host: "127.0.0.1".to_owned(), - port, - secret_key_base: TEST_ADMIN_SECRET.to_owned(), - base_path: String::new(), - api_endpoint: api_endpoint.to_owned(), - media_endpoint: format!("{api_endpoint}/media"), - static_cdn_endpoint: "https://static.example.test".to_owned(), - admin_endpoint: admin_endpoint.to_owned(), - web_app_endpoint: "http://127.0.0.1:8088".to_owned(), - kv_url: "redis://127.0.0.1:6379/0".to_owned(), - oauth_client_id: "1234567890123456789".to_owned(), - oauth_client_secret: "test-admin-oauth-secret".to_owned(), - oauth_redirect_uri: format!("{admin_endpoint}/oauth2_callback"), - build_version: "parity".to_owned(), - release_channel: "parity".to_owned(), - self_hosted: false, - proxy: ProxyConfig { - trust_client_ip_header: false, - client_ip_header_name: "x-forwarded-for".to_owned(), - }, - } -} - -fn ts_admin_env( - port: u16, - api_endpoint: &str, - admin_endpoint: &str, -) -> Vec<(&'static str, String)> { - vec![ - ("FLUXER_ENV", "test".to_owned()), - ("NODE_ENV", "production".to_owned()), - ("FLUXER_BASE_DOMAIN", "127.0.0.1".to_owned()), - ("FLUXER_PUBLIC_SCHEME", "http".to_owned()), - ("FLUXER_PUBLIC_PORT", port.to_string()), - ("FLUXER_API_ENDPOINT", api_endpoint.to_owned()), - ("FLUXER_ADMIN_ENDPOINT", admin_endpoint.to_owned()), - ("FLUXER_APP_ENDPOINT", "http://127.0.0.1:8088".to_owned()), - ("FLUXER_MEDIA_ENDPOINT", format!("{api_endpoint}/media")), - ( - "FLUXER_STATIC_CDN_ENDPOINT", - "https://static.example.test".to_owned(), - ), - ("FLUXER_CASSANDRA_HOSTS", "127.0.0.1".to_owned()), - ("FLUXER_CASSANDRA_KEYSPACE", "fluxer_test".to_owned()), - ("FLUXER_CASSANDRA_LOCAL_DC", "datacenter1".to_owned()), - ("FLUXER_CASSANDRA_USERNAME", "cassandra".to_owned()), - ("FLUXER_CASSANDRA_PASSWORD", "cassandra".to_owned()), - ("FLUXER_KV_URL", "redis://127.0.0.1:6379/0".to_owned()), - ("FLUXER_S3_ACCESS_KEY_ID", "test".to_owned()), - ("FLUXER_S3_SECRET_ACCESS_KEY", "test".to_owned()), - ( - "FLUXER_MEDIA_PROXY_SECRET_KEY", - "test-media-secret".to_owned(), - ), - ("FLUXER_ADMIN_PORT", port.to_string()), - ("FLUXER_ADMIN_SECRET_KEY_BASE", TEST_ADMIN_SECRET.to_owned()), - ( - "FLUXER_ADMIN_OAUTH_CLIENT_SECRET", - "test-admin-oauth-secret".to_owned(), - ), - ( - "FLUXER_MARKETING_SECRET_KEY_BASE", - "test-marketing-secret".to_owned(), - ), - ("FLUXER_APP_PROXY_PORT", "8773".to_owned()), - ( - "FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT", - format!("{api_endpoint}/media"), - ), - ( - "FLUXER_GATEWAY_RPC_AUTH_TOKEN", - "test-gateway-rpc-token".to_owned(), - ), - ("FLUXER_GATEWAY_PUSH_ENABLED", "false".to_owned()), - ("FLUXER_SUDO_MODE_SECRET", "test-sudo-secret".to_owned()), - ( - "FLUXER_CONNECTION_INITIATION_SECRET", - "test-connection-secret".to_owned(), - ), - ( - "FLUXER_VAPID_PUBLIC_KEY", - "test-vapid-public-key".to_owned(), - ), - ( - "FLUXER_VAPID_PRIVATE_KEY", - "test-vapid-private-key".to_owned(), - ), - ("FLUXER_VAPID_EMAIL", "test@example.com".to_owned()), - ("FLUXER_EMAIL_ENABLED", "false".to_owned()), - ("FLUXER_LIVEKIT_ENABLED", "false".to_owned()), - ("FLUXER_STRIPE_ENABLED", "true".to_owned()), - ("FLUXER_SEARCH_URL", "http://127.0.0.1:9200".to_owned()), - ("FLUXER_SEARCH_API_KEY", "test".to_owned()), - ("FLUXER_CAPTCHA_ENABLED", "false".to_owned()), - ("FLUXER_CAPTCHA_PROVIDER", "none".to_owned()), - ("FLUXER_SELF_HOSTED", "false".to_owned()), - ("FLUXER_DISCOVERY_ENABLED", "true".to_owned()), - ("FLUXER_DISABLE_RATE_LIMITS", "true".to_owned()), - ("FLUXER_TEST_MODE_ENABLED", "true".to_owned()), - ] -} - -async fn wait_for_health(base_url: &str) -> Result<(), String> { - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .map_err(|error| format!("failed to build health client: {error}"))?; - let deadline = Instant::now() + Duration::from_secs(30); - let url = format!("{}/_health", base_url.trim_end_matches('/')); - let mut last_error = String::new(); - while Instant::now() < deadline { - match client.get(&url).send().await { - Ok(response) if response.status().is_success() => return Ok(()), - Ok(response) => { - last_error = format!("health returned {}", response.status()); - } - Err(error) => { - last_error = error.to_string(); - } - } - sleep(Duration::from_millis(200)).await; - } - Err(format!("timed out waiting for {url}: {last_error}")) -} diff --git a/fluxer_api/src/api/database/PostgresKvQueryExecutor.Integration.test.ts b/fluxer_api/src/api/database/PostgresKvQueryExecutor.Integration.test.ts deleted file mode 100644 index f1aa45829..000000000 --- a/fluxer_api/src/api/database/PostgresKvQueryExecutor.Integration.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {randomUUID} from 'node:crypto'; -import {existsSync} from 'node:fs'; -import {getDefaultPostgresClient, initPostgres, quoteIdentifier, shutdownPostgres} from '@pkgs/postgres/src/Client'; -import cassandra from 'cassandra-driver'; -import {afterAll, beforeAll, describe, expect, test} from 'vitest'; -import {defineTable} from './CassandraTableDsl'; -import {Db} from './CassandraTypes'; -import {ensurePostgresKvSchema, PostgresKvQueryExecutor, pruneExpiredPostgresKvRows} from './PostgresKvQueryExecutor'; - -interface TestRow { - tenant_id: string; - item_id: bigint; - created_at: Date; - payload: Buffer | null; - tags: Set | null; - counts: Map | null; - local_day: cassandra.types.LocalDate | null; - note: string | null; -} - -function postgresHost(): string { - return process.env.FLUXER_POSTGRES_TEST_HOST ?? (existsSync('/.dockerenv') ? 'host.docker.internal' : '127.0.0.1'); -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -const kvTable = `fluxer_kv_test_${randomUUID().replaceAll('-', '_')}`; -const logicalTableName = `pg_kv_test_${randomUUID().replaceAll('-', '_')}`; -const TestRows = defineTable({ - name: logicalTableName, - columns: ['tenant_id', 'item_id', 'created_at', 'payload', 'tags', 'counts', 'local_day', 'note'], - primaryKey: ['tenant_id', 'item_id'], - partitionKey: ['tenant_id'], -}); - -describe('PostgresKvQueryExecutor', () => { - let executor: PostgresKvQueryExecutor; - let initialized = false; - - beforeAll(async () => { - await initPostgres({ - host: postgresHost(), - port: Number(process.env.FLUXER_POSTGRES_PORT ?? 5432), - database: process.env.FLUXER_POSTGRES_DATABASE ?? 'fluxer', - username: process.env.FLUXER_POSTGRES_USERNAME ?? 'fluxer', - password: process.env.FLUXER_POSTGRES_PASSWORD ?? 'fluxer', - kvTable, - maxConnections: 4, - }); - initialized = true; - const client = getDefaultPostgresClient(); - await ensurePostgresKvSchema(client); - executor = new PostgresKvQueryExecutor(client); - }); - - afterAll(async () => { - if (!initialized) return; - try { - await getDefaultPostgresClient().query(`DROP TABLE IF EXISTS ${quoteIdentifier(kvTable)}`); - } finally { - await shutdownPostgres(); - } - }); - - test('round trips typed values and conditional writes through a real Postgres KV table', async () => { - const row: TestRow = { - tenant_id: 'tenant-a', - item_id: 10n, - created_at: new Date('2026-01-02T03:04:05.006Z'), - payload: Buffer.from('hello postgres kv'), - tags: new Set(['alpha', 'beta']), - counts: new Map([['seen', 5n]]), - local_day: cassandra.types.LocalDate.fromString('2026-01-02'), - note: 'first', - }; - - await expect(executor.executeQuery<{['[applied]']: boolean}>(TestRows.insertIfNotExists(row))).resolves.toEqual([ - {'[applied]': true}, - ]); - await expect( - executor.executeQuery<{['[applied]']: boolean}>(TestRows.insertIfNotExists({...row, note: 'second'})), - ).resolves.toEqual([{'[applied]': false}]); - - const fetched = await executor.executeQuery( - TestRows.select({ - where: [TestRows.where.eq('tenant_id'), TestRows.where.eq('item_id')], - limit: 1, - }).bind({tenant_id: 'tenant-a', item_id: 10n}), - ); - - expect(fetched).toHaveLength(1); - expect(fetched[0]!.item_id).toBe(10n); - expect(fetched[0]!.created_at.getTime()).toBe(row.created_at.getTime()); - expect(fetched[0]!.payload?.equals(row.payload!)).toBe(true); - expect([...fetched[0]!.tags!.values()].sort()).toEqual(['alpha', 'beta']); - expect(fetched[0]!.counts!.get('seen')).toBe(5n); - expect(fetched[0]!.local_day?.toString()).toBe('2026-01-02'); - expect(fetched[0]!.note).toBe('first'); - }); - - test('filters, orders, patches, pages, and deletes logical rows', async () => { - await executor.executeBatch( - [ - TestRows.upsertAll({ - tenant_id: 'tenant-b', - item_id: 1n, - created_at: new Date('2026-02-01T00:00:00.000Z'), - payload: null, - tags: null, - counts: null, - local_day: null, - note: 'one', - }), - TestRows.upsertAll({ - tenant_id: 'tenant-b', - item_id: 2n, - created_at: new Date('2026-02-02T00:00:00.000Z'), - payload: null, - tags: null, - counts: null, - local_day: null, - note: 'two', - }), - TestRows.upsertAll({ - tenant_id: 'tenant-b', - item_id: 3n, - created_at: new Date('2026-02-03T00:00:00.000Z'), - payload: null, - tags: null, - counts: null, - local_day: null, - note: 'three', - }), - ].map((query) => ({query: query.cql, params: query.params, meta: query.kvMeta})), - ); - - const firstPage = await executor.executePagedQuery( - TestRows.select({ - where: TestRows.where.eq('tenant_id'), - orderBy: {col: 'item_id', direction: 'DESC'}, - }).bind({tenant_id: 'tenant-b'}), - {pageSize: 2}, - ); - expect(firstPage.rows.map((row) => row.item_id)).toEqual([3n, 2n]); - expect(firstPage.pageState).not.toBeNull(); - - const secondPage = await executor.executePagedQuery( - TestRows.select({ - where: TestRows.where.eq('tenant_id'), - orderBy: {col: 'item_id', direction: 'DESC'}, - }).bind({tenant_id: 'tenant-b'}), - {pageSize: 2, pageState: firstPage.pageState}, - ); - expect(secondPage.rows.map((row) => row.item_id)).toEqual([1n]); - expect(secondPage.pageState).toBeNull(); - - await executor.executeQuery(TestRows.patchByPk({tenant_id: 'tenant-b', item_id: 2n}, {note: Db.clear()})); - const patched = await executor.executeQuery( - TestRows.select({where: [TestRows.where.eq('tenant_id'), TestRows.where.eq('item_id')], limit: 1}).bind({ - tenant_id: 'tenant-b', - item_id: 2n, - }), - ); - expect(patched[0]!.note).toBeNull(); - - await executor.executeQuery(TestRows.delete({where: TestRows.where.eq('tenant_id')}).bind({tenant_id: 'tenant-b'})); - const remaining = await executor.executeQuery( - TestRows.select({where: TestRows.where.eq('tenant_id')}).bind({tenant_id: 'tenant-b'}), - ); - expect(remaining).toEqual([]); - }); - - test('filters expired rows and prunes them physically', async () => { - const client = getDefaultPostgresClient(); - await executor.executeQuery( - TestRows.upsertAllWithTtl( - { - tenant_id: 'tenant-expired', - item_id: 1n, - created_at: new Date('2026-03-01T00:00:00.000Z'), - payload: null, - tags: null, - counts: null, - local_day: null, - note: 'temporary', - }, - 1, - ), - ); - - await sleep(1100); - - const visibleRows = await executor.executeQuery( - TestRows.select({where: TestRows.where.eq('tenant_id')}).bind({tenant_id: 'tenant-expired'}), - ); - expect(visibleRows).toEqual([]); - - const physicalBefore = await client.query<{count: string}>( - `SELECT count(*)::text AS count FROM ${quoteIdentifier(kvTable)} WHERE table_name = $1 AND row_data ->> 'tenant_id' = $2`, - [logicalTableName, 'tenant-expired'], - ); - expect(Number(physicalBefore.rows[0]!.count)).toBe(1); - - const pruned = await pruneExpiredPostgresKvRows(client, 10); - expect(pruned).toBeGreaterThanOrEqual(1); - - const physicalAfter = await client.query<{count: string}>( - `SELECT count(*)::text AS count FROM ${quoteIdentifier(kvTable)} WHERE table_name = $1 AND row_data ->> 'tenant_id' = $2`, - [logicalTableName, 'tenant-expired'], - ); - expect(Number(physicalAfter.rows[0]!.count)).toBe(0); - }); - - test('patching a logically expired row makes it visible again without keeping stale TTL', async () => { - await executor.executeQuery( - TestRows.upsertAllWithTtl( - { - tenant_id: 'tenant-resurrect', - item_id: 1n, - created_at: new Date('2026-04-01T00:00:00.000Z'), - payload: null, - tags: null, - counts: null, - local_day: null, - note: 'temporary', - }, - 1, - ), - ); - - await sleep(1100); - await executor.executeQuery( - TestRows.patchByPk({tenant_id: 'tenant-resurrect', item_id: 1n}, {note: Db.set('resurrected')}), - ); - - const rows = await executor.executeQuery( - TestRows.select({ - where: [TestRows.where.eq('tenant_id'), TestRows.where.eq('item_id')], - limit: 1, - }).bind({tenant_id: 'tenant-resurrect', item_id: 1n}), - ); - expect(rows).toHaveLength(1); - expect(rows[0]!.note).toBe('resurrected'); - }); -}); diff --git a/fluxer_api/src/api/test/fake-ncmec/Bin.ts b/fluxer_api/src/api/test/fake-ncmec/Bin.ts deleted file mode 100644 index 5e87d2ee8..000000000 --- a/fluxer_api/src/api/test/fake-ncmec/Bin.ts +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import http from 'node:http'; -import type {AddressInfo} from 'node:net'; -import {type FakeNcmecFailure, FakeNcmecServer} from './FakeNcmecServer'; - -const host = process.env.FAKE_NCMEC_HOST ?? '127.0.0.1'; -const port = readPort('FAKE_NCMEC_PORT', 9090); -const controlHost = process.env.FAKE_NCMEC_CONTROL_HOST ?? '127.0.0.1'; -const controlPort = readPort('FAKE_NCMEC_CONTROL_PORT', port + 1); -const publicHost = process.env.FAKE_NCMEC_PUBLIC_HOST ?? (host === '0.0.0.0' ? '' : host); -const username = process.env.FAKE_NCMEC_USERNAME ?? 'usr123'; -const password = process.env.FAKE_NCMEC_PASSWORD ?? 'pswd123'; - -if (host === controlHost && port === controlPort) { - throw new Error('FAKE_NCMEC_CONTROL_PORT must differ from FAKE_NCMEC_PORT when both servers bind the same host'); -} - -const fake = new FakeNcmecServer({username, password}); - -function log(...parts: Array): void { - const ts = new Date().toISOString(); - console.log(`[fake-ncmec ${ts}]`, ...parts); -} - -await fake.start({ - host, - port, - onRequest: (entry) => { - log( - `${entry.remoteAddress ?? '?'} ${entry.method} ${entry.path} -> ${entry.statusCode} ${entry.durationMs}ms (${entry.requestId})`, - ); - }, -}); - -const controlServer = http.createServer(async (req, res) => { - const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`); - log(`control ${req.method} ${url.pathname}`); - if (req.method === 'GET' && url.pathname === '/_state') { - res.writeHead(200, {'content-type': 'application/json'}); - res.end(JSON.stringify(fake.getReports(), null, 2)); - return; - } - if (req.method === 'GET' && url.pathname.startsWith('/_report/')) { - const reportId = decodeURIComponent(url.pathname.slice('/_report/'.length)); - const report = fake.getReport(reportId); - if (!report) { - res.writeHead(404, {'content-type': 'application/json'}); - res.end(JSON.stringify({error: `Unknown report ${reportId}`})); - return; - } - res.writeHead(200, {'content-type': 'application/json'}); - res.end(JSON.stringify(report, null, 2)); - return; - } - if (req.method === 'POST' && url.pathname === '/_reset') { - fake.reset(); - res.writeHead(204); - res.end(); - return; - } - if (req.method === 'POST' && url.pathname === '/_fail') { - const mode = (url.searchParams.get('mode') ?? 'none') as FakeNcmecFailure; - if (!VALID_FAILURE_MODES.has(mode)) { - res.writeHead(400, {'content-type': 'application/json'}); - res.end(JSON.stringify({error: `Invalid failure mode ${mode}`})); - return; - } - fake.setFailure(mode); - res.writeHead(200, {'content-type': 'application/json'}); - res.end(JSON.stringify({failure: mode})); - return; - } - res.writeHead(404); - res.end('Not found. Try GET /_state, POST /_reset, POST /_fail?mode=...'); -}); - -await new Promise((resolve) => controlServer.listen(controlPort, controlHost, () => resolve())); - -const address = controlServer.address() as AddressInfo; - -log(`NCMEC-compatible fake server ready`); - -log(` NCMEC bind → http://${fake.host}:${fake.port}/ispws`); - -log(` NCMEC base URL → http://${publicHost}:${fake.port}/ispws`); - -log(` control endpoint → http://${address.address}:${address.port}/_state`); - -log(` credentials → ${username}:${password}`); - -if (publicHost.startsWith('<')) { - log(' set FAKE_NCMEC_PUBLIC_HOST to the hostname or IP your prod API can reach'); -} - -log(' point prod Config.ncmec.baseUrl at the NCMEC base URL above'); - -function shutdown(signal: string): void { - log(`received ${signal}, shutting down`); - Promise.allSettled([fake.stop(), new Promise((resolve) => controlServer.close(() => resolve()))]).then(() => - process.exit(0), - ); -} - -process.on('SIGINT', () => shutdown('SIGINT')); - -process.on('SIGTERM', () => shutdown('SIGTERM')); - -const VALID_FAILURE_MODES = new Set([ - 'none', - 'submit', - 'upload', - 'fileinfo', - 'finish', - 'retract', - 'unauthorized', -]); - -function readPort(name: string, fallback: number): number { - const raw = process.env[name]; - if (!raw) return fallback; - const value = Number.parseInt(raw, 10); - if (!Number.isInteger(value) || value < 0 || value > 65535) { - throw new Error(`${name} must be an integer between 0 and 65535`); - } - return value; -} diff --git a/fluxer_api/src/api/test/fake-ncmec/FakeNcmecServer.ts b/fluxer_api/src/api/test/fake-ncmec/FakeNcmecServer.ts deleted file mode 100644 index 4f214c54d..000000000 --- a/fluxer_api/src/api/test/fake-ncmec/FakeNcmecServer.ts +++ /dev/null @@ -1,430 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import crypto from 'node:crypto'; -import http from 'node:http'; -import type {AddressInfo} from 'node:net'; -import {Readable} from 'node:stream'; -import {XMLParser, XMLValidator} from 'fast-xml-parser'; - -export type FakeNcmecFailure = 'none' | 'submit' | 'upload' | 'fileinfo' | 'finish' | 'retract' | 'unauthorized'; - -export interface FakeNcmecFileRecord { - fileId: string; - filename: string; - size: number; - md5: string; - submittedDetails: boolean; - detailsXml: string | null; -} - -export interface FakeNcmecReportRecord { - reportId: string; - reportXml: string; - files: Array; - finished: boolean; - retracted: boolean; - authUsername: string; -} - -interface FakeNcmecServerOptions { - username: string; - password: string; -} - -interface FakeNcmecServerStartOptions { - host?: string; - port?: number; - onRequest?: (entry: FakeNcmecRequestLogEntry) => void; -} - -interface FakeNcmecRequestLogEntry { - method: string; - path: string; - statusCode: number; - requestId: string; - durationMs: number; - remoteAddress: string | null; -} - -const xmlParser = new XMLParser({ignoreAttributes: false, parseTagValue: false, trimValues: true}); - -export class FakeNcmecServer { - private server: http.Server | null = null; - private listenPort = 0; - private listenHost = '127.0.0.1'; - private onRequest: ((entry: FakeNcmecRequestLogEntry) => void) | null = null; - private reportCounter = 1000000; - private fileCounter = 1; - private requestCounter = 1; - private readonly reports = new Map(); - private failureMode: FakeNcmecFailure = 'none'; - - constructor(private readonly options: FakeNcmecServerOptions) {} - - async start(startOptions: FakeNcmecServerStartOptions = {}): Promise { - if (this.server) { - throw new Error('FakeNcmecServer already started'); - } - this.listenHost = startOptions.host ?? '127.0.0.1'; - this.onRequest = startOptions.onRequest ?? null; - this.server = http.createServer((req, res) => this.handle(req, res)); - await new Promise((resolve) => this.server!.listen(startOptions.port ?? 0, this.listenHost, () => resolve())); - this.listenPort = (this.server!.address() as AddressInfo).port; - } - - async stop(): Promise { - if (!this.server) return; - await new Promise((resolve, reject) => { - this.server!.close((error) => (error ? reject(error) : resolve())); - }); - this.server = null; - this.listenPort = 0; - this.onRequest = null; - } - - get baseUrl(): string { - if (!this.server) throw new Error('FakeNcmecServer not started'); - return `http://${this.listenHost}:${this.listenPort}/ispws`; - } - - get host(): string { - if (!this.server) throw new Error('FakeNcmecServer not started'); - return this.listenHost; - } - - get port(): number { - if (!this.server) throw new Error('FakeNcmecServer not started'); - return this.listenPort; - } - - reset(): void { - this.reports.clear(); - this.reportCounter = 1000000; - this.fileCounter = 1; - this.requestCounter = 1; - this.failureMode = 'none'; - } - - setFailure(mode: FakeNcmecFailure): void { - this.failureMode = mode; - } - - getReports(): Array { - return [...this.reports.values()]; - } - - getReport(reportId: string): FakeNcmecReportRecord | null { - return this.reports.get(reportId) ?? null; - } - - private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise { - try { - const url = new URL(req.url ?? '/', 'http://127.0.0.1'); - const pathname = url.pathname.replace(/^\/ispws/, '') || '/'; - const requestId = `req-${this.requestCounter++}`; - const startedAt = Date.now(); - res.setHeader('Request-ID', requestId); - res.on('finish', () => { - this.onRequest?.({ - method: req.method ?? 'GET', - path: pathname, - statusCode: res.statusCode, - requestId, - durationMs: Date.now() - startedAt, - remoteAddress: req.socket.remoteAddress ?? null, - }); - }); - if (this.failureMode === 'unauthorized') { - return this.sendStatus(res, 401, ''); - } - if (!this.checkBasicAuth(req)) { - return this.sendStatus(res, 401, ''); - } - const injectedHeader = (req.headers['x-test-fail'] ?? '').toString() as FakeNcmecFailure; - const effectiveFailure: FakeNcmecFailure = injectedHeader || this.failureMode; - if (req.method === 'GET' && pathname === '/status') { - const remoteIp = req.socket.remoteAddress ?? 'unknown'; - return this.sendXml( - res, - 200, - ok( - `Remote User : ${this.options.username}, Remote Ip : ${remoteIp}`, - ), - ); - } - if (req.method === 'GET' && pathname === '/xsd') { - return this.sendText(res, 200, ''); - } - if (req.method !== 'POST') { - return this.sendStatus(res, 405, ''); - } - const request = await this.toWebRequest(req, url); - switch (pathname) { - case '/submit': - return this.handleSubmit(request, res, effectiveFailure); - case '/upload': - return this.handleUpload(request, res, effectiveFailure); - case '/fileinfo': - return this.handleFileInfo(request, res, effectiveFailure); - case '/finish': - return this.handleFinish(request, res, effectiveFailure); - case '/retract': - return this.handleRetract(request, res, effectiveFailure); - default: - return this.sendStatus(res, 404, ''); - } - } catch (error) { - this.sendStatus(res, 500, String(error)); - } - } - - private async handleSubmit(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise { - if (failure === 'submit') { - return this.sendXml(res, 200, errorXml(1100, 'Save failed')); - } - if (!isXmlContentType(request)) { - return this.sendXml(res, 200, errorXml(4000, 'Invalid request')); - } - const body = await request.text(); - if (XMLValidator.validate(body) !== true) { - return this.sendXml(res, 200, errorXml(4110, 'Malformed XML submittal')); - } - const report = parseReportDocument(body); - if (!hasRequiredReportFields(report)) { - return this.sendXml(res, 200, errorXml(4100, 'Validation failed')); - } - const reportId = String(this.reportCounter++); - this.reports.set(reportId, { - reportId, - reportXml: body, - files: [], - finished: false, - retracted: false, - authUsername: this.options.username, - }); - return this.sendXml(res, 200, ok(`${reportId}`)); - } - - private async handleUpload(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise { - if (failure === 'upload') { - return this.sendXml(res, 200, errorXml(1111, 'File upload failed')); - } - const form = await request.formData(); - const reportId = String(form.get('id') ?? ''); - const file = form.get('file'); - const record = this.reports.get(reportId); - if (!reportId || !record) { - return this.sendXml(res, 200, errorXml(5001, 'Report does not exist')); - } - if (record.retracted) { - return this.sendXml(res, 200, errorXml(5101, 'Report already retracted')); - } - if (record.finished) { - return this.sendXml(res, 200, errorXml(5102, 'Report already finished')); - } - if (!(file instanceof Blob)) { - return this.sendXml(res, 200, errorXml(4200, 'Malformed file submittal')); - } - const buffer = Buffer.from(await file.arrayBuffer()); - const md5 = crypto.createHash('md5').update(buffer).digest('hex'); - const fileId = `file-${this.fileCounter++}`; - record.files.push({ - fileId, - filename: (file as File).name || 'upload.bin', - size: buffer.byteLength, - md5, - submittedDetails: false, - detailsXml: null, - }); - return this.sendXml(res, 200, ok(`${reportId}${fileId}${md5}`)); - } - - private async handleFileInfo(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise { - if (failure === 'fileinfo') { - return this.sendXml(res, 200, errorXml(1300, 'Update failed')); - } - if (!isXmlContentType(request)) { - return this.sendXml(res, 200, errorXml(4000, 'Invalid request')); - } - const body = await request.text(); - if (XMLValidator.validate(body) !== true) { - return this.sendXml(res, 200, errorXml(4110, 'Malformed XML submittal')); - } - const fileDetails = parseFileDetailsDocument(body); - if (!hasFileDetailsFields(fileDetails)) { - return this.sendXml(res, 200, errorXml(4100, 'Validation failed')); - } - const reportId = nonBlankString(fileDetails?.reportId); - const fileId = nonBlankString(fileDetails?.fileId); - const record = reportId ? this.reports.get(reportId) : null; - if (!record) { - return this.sendXml(res, 200, errorXml(5001, 'Report does not exist')); - } - if (record.retracted) { - return this.sendXml(res, 200, errorXml(5101, 'Report already retracted')); - } - if (record.finished) { - return this.sendXml(res, 200, errorXml(5102, 'Report already finished')); - } - const file = record.files.find((f) => f.fileId === fileId); - if (!file) { - return this.sendXml(res, 200, errorXml(5002, 'File does not exist')); - } - file.submittedDetails = true; - file.detailsXml = body; - return this.sendXml(res, 200, ok(`${reportId}`)); - } - - private async handleFinish(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise { - if (failure === 'finish') { - return this.sendXml(res, 200, errorXml(1300, 'Update failed')); - } - const form = await request.formData(); - const reportId = String(form.get('id') ?? ''); - const record = this.reports.get(reportId); - if (!record) { - return this.sendXml(res, 200, errorXml(5001, 'Report does not exist')); - } - if (record.retracted) { - return this.sendXml(res, 200, errorXml(5101, 'Report already retracted')); - } - if (record.finished) { - return this.sendXml(res, 200, errorXml(5102, 'Report already finished')); - } - record.finished = true; - const files = record.files.map((f) => ` ${f.fileId}`).join('\n'); - const body = ` - - 0 - ${reportId} - -${files} - -`; - return this.sendXml(res, 200, body); - } - - private async handleRetract(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise { - if (failure === 'retract') { - return this.sendXml(res, 200, errorXml(1300, 'Update failed')); - } - const form = await request.formData(); - const reportId = String(form.get('id') ?? ''); - const record = this.reports.get(reportId); - if (!record) { - return this.sendXml(res, 200, errorXml(5001, 'Report does not exist')); - } - if (record.retracted) { - return this.sendXml(res, 200, errorXml(5101, 'Report already retracted')); - } - if (record.finished) { - return this.sendXml(res, 200, errorXml(5102, 'Report already finished')); - } - record.retracted = true; - return this.sendXml(res, 200, ok(`${reportId}`)); - } - - private checkBasicAuth(req: http.IncomingMessage): boolean { - const header = req.headers.authorization ?? ''; - if (!header.toLowerCase().startsWith('basic ')) return false; - const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8'); - const idx = decoded.indexOf(':'); - if (idx === -1) return false; - const user = decoded.slice(0, idx); - const pass = decoded.slice(idx + 1); - return user === this.options.username && pass === this.options.password; - } - - private async toWebRequest(req: http.IncomingMessage, url: URL): Promise { - const headers = new Headers(); - for (const [k, v] of Object.entries(req.headers)) { - if (v === undefined) continue; - if (Array.isArray(v)) headers.set(k, v.join(',')); - else headers.set(k, String(v)); - } - const method = req.method ?? 'GET'; - const body = method === 'GET' || method === 'HEAD' ? undefined : (Readable.toWeb(req) as ReadableStream); - return new Request(url, {method, headers, body, duplex: 'half'} as RequestInit & { - duplex: 'half'; - }); - } - - private sendXml(res: http.ServerResponse, status: number, body: string): void { - res.writeHead(status, {'content-type': 'application/xml; charset=utf-8'}); - res.end(body); - } - - private sendText(res: http.ServerResponse, status: number, body: string): void { - res.writeHead(status, {'content-type': 'text/plain; charset=utf-8'}); - res.end(body); - } - - private sendStatus(res: http.ServerResponse, status: number, body: string): void { - res.writeHead(status); - res.end(body); - } -} - -function ok(extra: string): string { - return ` - - 0 - Success - ${extra} -`; -} - -function errorXml(code: number, description: string): string { - return ` - - ${code} - ${description} -`; -} - -function isXmlContentType(request: Request): boolean { - return (request.headers.get('content-type') ?? '').toLowerCase().includes('text/xml'); -} - -function parseReportDocument(xml: string): Record | null { - const parsed = xmlParser.parse(xml) as Record; - return objectField(parsed, 'report'); -} - -function parseFileDetailsDocument(xml: string): Record | null { - const parsed = xmlParser.parse(xml) as Record; - return objectField(parsed, 'fileDetails'); -} - -function hasRequiredReportFields(report: Record | null): boolean { - const incidentSummary = objectField(report, 'incidentSummary'); - const reporter = objectField(report, 'reporter'); - const reportingPerson = objectField(reporter, 'reportingPerson'); - return Boolean( - nonBlankString(incidentSummary?.incidentType) && - nonBlankString(incidentSummary?.incidentDateTime) && - nonBlankString(reportingPerson?.email), - ); -} - -function hasFileDetailsFields(fileDetails: Record | null): boolean { - return Boolean(nonBlankString(fileDetails?.reportId) && nonBlankString(fileDetails?.fileId)); -} - -function objectField(value: unknown, key: string): Record | null { - if (!value || typeof value !== 'object' || Array.isArray(value)) return null; - const field = (value as Record)[key]; - if (!field || typeof field !== 'object' || Array.isArray(field)) return null; - return field as Record; -} - -function nonBlankString(value: unknown): string | null { - if (typeof value === 'string') { - const trimmed = value.trim(); - return trimmed || null; - } - if (typeof value === 'number' || typeof value === 'bigint') { - return String(value); - } - return null; -} diff --git a/fluxer_api/src/api/test/msw/handlers/NcmecHandlers.ts b/fluxer_api/src/api/test/msw/handlers/NcmecHandlers.ts index 5423a5616..4ee020bdd 100644 --- a/fluxer_api/src/api/test/msw/handlers/NcmecHandlers.ts +++ b/fluxer_api/src/api/test/msw/handlers/NcmecHandlers.ts @@ -3,7 +3,26 @@ import crypto from 'node:crypto'; import {XMLParser, XMLValidator} from 'fast-xml-parser'; import {HttpResponse, http, type RequestHandler} from 'msw'; -import type {FakeNcmecFailure, FakeNcmecFileRecord, FakeNcmecReportRecord} from '../../fake-ncmec/FakeNcmecServer'; + +type FakeNcmecFailure = 'none' | 'submit' | 'upload' | 'fileinfo' | 'finish' | 'retract' | 'unauthorized'; + +interface FakeNcmecFileRecord { + fileId: string; + filename: string; + size: number; + md5: string; + submittedDetails: boolean; + detailsXml: string | null; +} + +interface FakeNcmecReportRecord { + reportId: string; + reportXml: string; + files: Array; + finished: boolean; + retracted: boolean; + authUsername: string; +} const BASE_URL = 'https://ncmec.test/ispws'; const USERNAME = 'usr123'; diff --git a/fluxer_desktop/electron-builder.config.cjs b/fluxer_desktop/electron-builder.config.cjs index 75d5c6efd..7acca26f8 100644 --- a/fluxer_desktop/electron-builder.config.cjs +++ b/fluxer_desktop/electron-builder.config.cjs @@ -200,11 +200,13 @@ const nativeRuntimeFilePatterns = [ 'node_modules/@fluxer/linux-input-hook/*.node', 'node_modules/@fluxer/platform-info/package.json', 'node_modules/@fluxer/platform-info/index.js', + 'node_modules/@fluxer/platform-info/pure.cjs', 'node_modules/@fluxer/platform-info/loader-diagnostics.cjs', 'node_modules/@fluxer/platform-info/*.node', 'node_modules/@fluxer/webauthn/package.json', 'node_modules/@fluxer/webauthn/index.js', 'node_modules/@fluxer/webauthn/index.d.ts', + 'node_modules/@fluxer/webauthn/pure.cjs', 'node_modules/@fluxer/webauthn/loader-diagnostics.cjs', 'node_modules/@fluxer/webauthn/*.node', 'node_modules/@fluxer/webauthn/*.so*', @@ -213,6 +215,7 @@ const nativeRuntimeFilePatterns = [ 'node_modules/@fluxer/webrtc-sender/index.d.ts', 'node_modules/@fluxer/webrtc-sender/*.node', 'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/loader-diagnostics.cjs', + 'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/pure.cjs', 'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node', 'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.node', 'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.dll', diff --git a/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/livekit-harness.json b/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/livekit-harness.json deleted file mode 100644 index 8875f4c53..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/livekit-harness.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "status": "skip", - "startedAt": "2026-06-05T15:40:30.428Z", - "endedAt": "2026-06-05T15:40:30.428Z", - "platform": "linux", - "arch": "arm64", - "config": { - "url": "ws://localhost:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-38884-16b7fb75-22a2-4914-8e2d-255451c5b7e9", - "required": false, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAvDriftMs": 0, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/workspaces/fluxer/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/livekit-harness.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null - }, - "checks": [], - "skippedReason": "LiveKit is unreachable at ws://localhost:7880 (connect ECONNREFUSED 127.0.0.1:7880)", - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": false, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "nvenc_not_compiled", - "detail": "CUDA headers were not found at build time; rebuild with CUDA_HOME pointing to a CUDA toolkit that contains include/cuda.h" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": null, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "avDriftMs": null, - "droppedVideoFrameCallbacks": 0, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/summary.json b/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/summary.json deleted file mode 100644 index 9e0b8071a..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z/summary.json +++ /dev/null @@ -1,208 +0,0 @@ -{ - "status": "pass", - "mode": "smoke", - "strict": false, - "platform": "linux", - "arch": "arm64", - "hostname": "850d0711b42a", - "startedAt": "2026-06-05T15:40:10.774Z", - "endedAt": "2026-06-05T15:40:45.392Z", - "reportDir": "/workspaces/fluxer/fluxer_desktop/native-media-reports/2026-06-05T15-40-10-773Z", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "linux-screen-capture-build", - "command": "pnpm --dir native/linux-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-screen-capture-tests", - "command": "pnpm --dir native/linux-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-audio-capture-build", - "command": "pnpm --dir native/linux-audio-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-audio-capture-tests", - "command": "pnpm --dir native/linux-audio-capture test", - "category": "platform-native", - "cwd": "." - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:10.775Z", - "endedAt": "2026-06-05T15:40:10.863Z", - "durationMs": 88, - "stdoutTail": "▶ NativeScreenCapture DMA-BUF metadata validation\n ✔ accepts valid DMA-BUF metadata and pads native plane arrays (1.747886ms)\n ✔ keeps valid handles when optional device UUID metadata is absent or malformed (0.452503ms)\n ✔ rejects invalid plane counts, FDs, strides, and format metadata (0.616004ms)\n✔ NativeScreenCapture DMA-BUF metadata validation (3.258061ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU frames (2.165721ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.671504ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.583295ms)\n✔ NativeVoiceEngine screen frame routing (3.909315ms)\n▶ WebrtcScreenPublish native frame routing\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU buffers (1.627218ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.594711ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.41371ms)\n✔ WebrtcScreenPublish native frame routing (3.18327ms)\nℹ tests 9\nℹ suites 3\nℹ pass 9\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 69.874336\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:10.863Z", - "endedAt": "2026-06-05T15:40:13.457Z", - "durationMs": 2594, - "stdoutTail": "native/mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\nnative/windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> fluxer_desktop@0.0.0 typecheck /workspaces/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:13.457Z", - "endedAt": "2026-06-05T15:40:25.076Z", - "durationMs": 11619, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/webrtc-sender@0.0.0 build /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> node scripts/build-rust.mjs\n\n[buildRustNodeAddon] cwd=/workspaces/fluxer/fluxer_desktop/native/webrtc-sender\ncargo build --release --target aarch64-unknown-linux-gnu --manifest-path Cargo.toml --features camera-native\n", - "stderrTail": "warning: webrtc-sys@0.3.33: cuda.h not found; building without hardware accelerated video codec support for NVidia GPUs\nwarning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: variable does not need to be mutable\n --> vendor/libwebrtc/src/native/desktop_capturer.rs:66:13\n |\n66 | let mut sys_handle = sys_dc::ffi::DesktopCapturerOptions {\n | ----^^^^^^^^^^\n | |\n | help: remove this `mut`\n |\n = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 4 warnings (run `cargo fix --lib -p libwebrtc` to apply 3 suggestions)\n Compiling fluxer_webrtc_sender v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender)\n Finished `release` profile [optimized] target(s) in 9.11s\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:25.076Z", - "endedAt": "2026-06-05T15:40:27.641Z", - "durationMs": 2565, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/webrtc-sender@0.0.0 test /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo test --features publisher,camera-native && node --test index.test.mjs\n\n\nrunning 89 tests\ntest audio::tests::clamp_volume_holds_the_contract_range ... ok\ntest audio::tests::clamp_volume_maps_non_finite_to_unity ... ok\ntest audio::tests::input_devices_json_array ... ok\ntest audio::tests::output_devices_json_array ... ok\ntest audio::tests::processing_options_preserve_defaults_unless_overridden ... ok\ntest audio::tests::output_device_json_locks_the_contract_shape ... ok\ntest audio::tests::shape_input_marks_index_zero_default_and_drops_empty_ids ... ok\ntest audio::tests::is_muted_only_at_zero ... ok\ntest audio::tests::shape_marks_index_zero_default_and_drops_empty_ids ... ok\ntest camera::tests::from_opts_applies_defaults ... ok\ntest camera::tests::from_opts_forces_even_dims_and_honours_overrides ... ok\ntest camera::tests::from_opts_parses_device_index ... ok\ntest camera::tests::from_opts_preserves_string_device_ids ... ok\ntest camera::tests::from_opts_rejects_degenerate_dims_and_fps ... ok\ntest config::tests::canonical_codec_name_accepts_all_five_case_insensitively ... ok\ntest config::tests::canonical_codec_name_rejects_empty_and_unknown ... ok\ntest config::tests::rejects_bad_url ... ok\ntest config::tests::rejects_empty_token ... ok\ntest config::tests::rejects_odd_or_oob_dimensions ... ok\ntest config::tests::state_gates_frames_and_connect ... ok\ntest engine::tests::parse_codec_agrees_with_pure_canonicaliser ... ok\ntest config::tests::valid_config_passes ... ok\ntest engine::tests::parse_codec_maps_all_five_including_h265 ... ok\ntest engine::tests::parse_codec_rejects_empty_and_unknown ... ok\ntest engine::tests::parse_track_source_accepts_renderer_and_livekit_spellings ... ok\ntest engine::tests::parse_video_quality_accepts_subscription_quality_levels ... ok\ntest engine::tests::pcm16_audio_frame_rejects_malformed_buffers ... ok\ntest engine::tests::pcm16_audio_frame_shapes_interleaved_stereo_samples ... ok\ntest engine::tests::tight_i420_len_matches_plane_geometry ... ok\ntest engine::tests::video_frame_meta_json_matches_contract ... ok\ntest events::tests::empty_object_and_array ... ok\ntest events::tests::escapes_quote_backslash_and_control_chars ... ok\ntest events::tests::json_object_preserves_order_and_mixes_str_and_raw ... ok\ntest events::tests::json_u8_array_serializes_bytes_as_numbers ... ok\ntest events::tests::leaves_plain_ascii_and_unicode_untouched ... ok\ntest events::tests::raw_array_preserves_prebuilt_json_objects ... ok\ntest hardware_encoder::tests::reports_available_nvenc_when_compiled_and_runtime_ready ... ok\ntest hardware_encoder::tests::reports_driver_sdk_incompatibility_before_encode_session ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_compiled ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_probe_is_absent ... ok\ntest hardware_encoder::tests::reports_not_compiled_before_runtime_missing ... ok\ntest hardware_encoder::tests::reports_no_supported_codecs_when_guid_query_finds_no_fluxer_codecs ... ok\ntest hardware_encoder::tests::reports_h264_only_when_hevc_guid_is_absent ... ok\ntest hardware_encoder::tests::reports_no_cuda_devices_using_obs_style_reason ... ok\ntest hardware_encoder::tests::reports_unsupported_platforms_clearly ... ok\ntest publisher::tests::i420_plane_copy_fills_libwebrtc_buffer ... ok\ntest stats::tests::bitrate_computes_kbps_from_byte_delta ... ok\ntest stats::tests::bitrate_first_sample_is_zero ... ok\ntest publisher::tests::parse_codec_maps_known_names ... ok\ntest stats::tests::bitrate_half_second_doubles_rate ... ok\ntest stats::tests::bitrate_rejects_backwards_time_and_bytes ... ok\ntest stats::tests::empty_stats_serialise_to_null_rtt_and_empty_arrays ... ok\ntest stats::tests::full_stats_serialise_to_exact_contract_shape ... ok\ntest stats::tests::sanitize_kbps_drops_nan_inf_negative_and_rounds ... ok\ntest stats::tests::sanitize_audio_level_clamps ... ok\ntest stats::tests::video_inbound_omits_audio_level_audio_omits_fps ... ok\ntest stats::tests::unit_conversions_seconds_to_ms ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_negative_fds ... ok\ntest texture_source::tests::dxgi_format_gate_accepts_8bit_rgba_bgra_only ... ok\ntest texture_source::tests::drm_format_gate_accepts_obs_vkcapture_texture_formats ... ok\ntest texture_source::tests::error_strings_are_stable ... ok\ntest texture_source::tests::probe_is_available_only_for_explicit_hardware_codecs ... ok\ntest texture_source::tests::should_attempt_dmabuf_is_sdk_gated_after_validation_capability ... ok\ntest texture_source::tests::should_attempt_falls_back_when_capability_unavailable ... ok\ntest texture_source::tests::should_attempt_validates_frame_when_capability_available ... ok\ntest texture_source::tests::validate_accepts_a_clean_bgra_texture ... ok\ntest texture_source::tests::validate_dmabuf_accepts_supported_formats_with_optional_uuid ... ok\ntest texture_source::tests::validate_dmabuf_rejects_invalid_planes ... ok\ntest texture_source::tests::validate_precedence_handle_before_dims_before_format ... ok\ntest texture_source::tests::validate_rejects_odd_zero_and_oversized_dims ... ok\ntest texture_source::tests::validate_rejects_unsupported_format ... ok\ntest texture_source::tests::validate_rejects_zero_handle_as_no_texture ... ok\ntest yuv::tests::bgra_black_white_grey_levels ... ok\ntest yuv::tests::bgra_golden_2x2_solid_colour_exact_bytes ... ok\ntest yuv::tests::bgra_golden_strided_2x2_skips_row_padding ... ok\ntest yuv::tests::bgra_primaries_have_expected_chroma_signs ... ok\ntest yuv::tests::bgra_plane_sizes ... ok\ntest yuv::tests::copy_nv12_planes_preserves_nv12_layout ... ok\ntest yuv::tests::copy_nv12_planes_rejects_short_buffers ... ok\ntest yuv::tests::copy_nv12_planes_respects_destination_stride ... ok\ntest yuv::tests::nv12_4x4_deinterleaves_two_chroma_columns ... ok\ntest yuv::tests::nv12_golden_4x2_packed_to_i420 ... ok\ntest yuv::tests::nv12_packed_2x2_deinterleaves ... ok\ntest yuv::tests::nv12_respects_row_padding ... ok\ntest yuv::tests::nv12_short_buffer_is_rejected ... ok\ntest yuv::tests::rejects_odd_or_tiny_dimensions ... ok\ntest yuv::tests::rgb_golden_2x2_solid_colour_matches_bgra_path ... ok\ntest yuv::tests::rgb_plane_sizes_and_levels ... ok\ntest yuv::tests::rgb_rejects_odd_dims_and_short_buffer ... ok\n\ntest result: ok. 89 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 6 tests\ntest outbound_covers_the_single_identity_publish_path ... ok\ntest e2ee_state_is_ok_on_the_wire ... ok\ntest outbound_fixtures_match_contract ... ok\ntest inbound_covers_subscribe_and_lifecycle ... ok\ntest video_frame_meta_matches_contract ... ok\ntest inbound_fixtures_match_contract ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ webrtc-sender loader wrapper\n ✔ resolves the native addon filename for every desktop OS/arch target (0.414128ms)\n ✔ rejects unsupported platform/architecture pairs explicitly (0.194543ms)\n ✔ returns a native hardware encoder capability when the binding exports one (0.19071ms)\n ✔ delegates dropped video callback metrics through the VoiceEngine wrapper (0.08925ms)\n ✔ delegates shared texture handles through the WebrtcPublisher wrapper (0.244294ms)\n ✔ treats shared texture publishing as unavailable for older publisher bindings (0.05975ms)\n ✔ delegates dma-buf frames through the WebrtcPublisher wrapper (0.130917ms)\n ✔ delegates shared texture handles through the VoiceEngine wrapper (0.093ms)\n ✔ treats shared texture publishing as unavailable for older voice bindings (0.074ms)\n ✔ treats dma-buf publishing as unavailable for older native bindings (0.109542ms)\n ✔ defaults dropped video callback metrics to zero for older native bindings (0.072001ms)\n ✔ delegates remote track subscription updates through the VoiceEngine wrapper (0.090292ms)\n ✔ ignores remote track subscription updates for older native bindings (0.054792ms)\n ✔ delegates data packets through the VoiceEngine wrapper (0.129459ms)\n ✔ rejects invalid data packet payloads in the wrapper (0.139584ms)\n ✔ returns an unavailable hardware encoder capability without a binding (0.063959ms)\n✔ webrtc-sender loader wrapper (3.186227ms)\nℹ tests 16\nℹ suites 1\nℹ pass 16\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 39.277319\n", - "stderrTail": "warning: webrtc-sys@0.3.33: cuda.h not found; building without hardware accelerated video codec support for NVidia GPUs\nwarning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: variable does not need to be mutable\n --> vendor/libwebrtc/src/native/desktop_capturer.rs:66:13\n |\n66 | let mut sys_handle = sys_dc::ffi::DesktopCapturerOptions {\n | ----^^^^^^^^^^\n | |\n | help: remove this `mut`\n |\n = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 4 warnings (run `cargo fix --lib -p libwebrtc` to apply 3 suggestions)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 0.15s\n Running unittests src/lib.rs (target/debug/deps/fluxer_webrtc_sender-96ed47cbef11f6a0)\n Running tests/event_contract.rs (target/debug/deps/event_contract-c09db034b80d5f00)\n Doc-tests fluxer_webrtc_sender\n" - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:27.641Z", - "endedAt": "2026-06-05T15:40:30.440Z", - "durationMs": 2799, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/webrtc-sender@0.0.0 test:livekit /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> node scripts/livekit-harness.mjs\n\n[livekit-harness] SKIP: LiveKit is unreachable at ws://localhost:7880 (connect ECONNREFUSED 127.0.0.1:7880). Set FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED=1 to fail instead.\n", - "stderrTail": "" - }, - { - "name": "linux-screen-capture-build", - "command": "pnpm --dir native/linux-screen-capture build", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:30.440Z", - "endedAt": "2026-06-05T15:40:32.883Z", - "durationMs": 2443, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/linux-screen-capture@0.0.0 build /workspaces/fluxer/fluxer_desktop/native/linux-screen-capture\n> node scripts/build-rust.mjs\n\n[buildRustNodeAddon] cwd=/workspaces/fluxer/fluxer_desktop/native/linux-screen-capture\ncargo build --release --target aarch64-unknown-linux-gnu --manifest-path Cargo.toml\n", - "stderrTail": " Finished `release` profile [optimized] target(s) in 0.13s\n" - }, - { - "name": "linux-screen-capture-tests", - "command": "pnpm --dir native/linux-screen-capture test", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:32.883Z", - "endedAt": "2026-06-05T15:40:37.620Z", - "durationMs": 4737, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/linux-screen-capture@0.0.0 test /workspaces/fluxer/fluxer_desktop/native/linux-screen-capture\n> cargo test --manifest-path Cargo.toml && node --test index.test.mjs\n\n\nrunning 42 tests\ntest game_capture::tests::auto_import_policy_walks_obs_fallback_ladder ... ok\ntest game_capture::tests::converts_abgr10_row_to_bgra ... ok\ntest game_capture::tests::diagnostics_can_pin_host_mapped_capture_policy ... ok\ntest game_capture::tests::converts_fp16_rgba_row_to_bgra_srgb ... ok\ntest game_capture::tests::diagnostics_modifier_uses_obs_invalid_name ... ok\ntest game_capture::tests::control_message_encodes_obs_import_modes ... ok\ntest game_capture::tests::host_mapped_import_rejects_invalid_stride_and_offset_with_diagnostics ... ok\ntest game_capture::tests::host_mapped_import_rejects_multiplane_descriptors_with_diagnostic ... ok\ntest game_capture::tests::diagnostics_default_to_obs_fallback_capture_policy ... ok\ntest game_capture::tests::identifies_rgba_memory_formats_that_need_swizzle ... ok\ntest game_capture::tests::host_mapped_import_rejects_unsupported_formats_with_supported_list ... ok\ntest game_capture::tests::lifecycle_messages_update_diagnostics_snapshot ... ok\ntest game_capture::tests::host_mapped_import_rejects_invalid_fd_with_diagnostic ... ok\ntest game_capture::tests::host_mapped_import_reads_cpu_nv12_and_preserves_source_dmabuf_metadata ... ok\ntest game_capture::tests::import_mode_env_parses_nvidia_debug_shortcuts ... ok\ntest game_capture::tests::non_host_mapped_import_reports_fallback_requirement ... ok\ntest game_capture::tests::mapped_texture_exports_dmabuf_metadata_for_native_encoder ... ok\ntest napi_surface_linux::tests::parse_source_id_roundtrip ... ok\ntest napi_surface_linux::tests::stream_dimensions_for_node_uses_selected_portal_stream ... ok\ntest nv12_packing::tests::bgra_to_nv12_rejects_short_stride ... ok\ntest game_capture::tests::parses_obs_vkcapture_texture_layout ... ok\ntest napi_surface_linux::tests::parse_source_id_rejects_malformed ... ok\ntest game_capture::tests::parses_client_name_as_c_string ... ok\ntest napi_surface_linux::tests::source_kind_for_type_maps_bits ... ok\ntest nv12_packing::tests::bgra_to_nv12_solid_black ... ok\ntest nv12_packing::tests::pack_nv12_rejects_short_source ... ok\ntest nv12_packing::tests::pack_nv12_rejects_undersized_stride ... ok\ntest nv12_packing::tests::pack_nv12_rejects_wrong_dst_size ... ok\ntest nv12_packing::tests::pack_nv12_strips_row_padding ... ok\ntest nv12_packing::tests::pack_nv12_zero_padding_is_identity ... ok\ntest nv12_packing::tests::packed_size_matches_yuv420_layout ... ok\ntest nv12_packing::tests::packed_size_rejects_odd_height ... ok\ntest nv12_packing::tests::packed_size_rejects_zero_dims ... ok\ntest portal::tests::cursor_mode_constants_match_portal_spec ... ok\ntest portal::tests::cursor_mode_matches_treats_missing_key_as_honoured ... ok\ntest portal::tests::mint_token_is_distinct_and_prefixed ... ok\ntest portal::tests::normalize_desktop_entry_app_id_accepts_fluxer_ids ... ok\ntest portal::tests::normalize_desktop_entry_app_id_rejects_paths_and_empty_values ... ok\ntest portal::tests::request_path_sanitizes_unique_bus_name ... ok\ntest portal::tests::session_path_sanitizes_unique_bus_name ... ok\ntest portal::tests::source_type_mask_combines_monitor_and_window ... ok\ntest nv12_packing::tests::bgra_to_nv12_solid_white_is_in_range ... ok\n\ntest result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ linux-screen-capture game capture launch environment\n ✔ enables OBS Vulkan capture and names the client (0.838797ms)\n ✔ prefers bundled hook assets and can force PRIME/NVIDIA launch variables (0.434836ms)\n ✔ does not duplicate launch path entries or Vulkan layer names (0.278376ms)\n ✔ can force a specific NVIDIA Vulkan ICD for hybrid GPU systems (0.627129ms)\n ✔ keeps obs-vkcapture runtime assets and license notes in the package surface (0.78638ms)\n✔ linux-screen-capture game capture launch environment (3.608563ms)\nℹ tests 5\nℹ suites 1\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 58.707644\n", - "stderrTail": " Compiling fluxer_linux_screen_capture v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/linux-screen-capture)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 2.44s\n Running unittests src/lib.rs (target/debug/deps/fluxer_linux_screen_capture-0784dd97ce1aa9bb)\n Doc-tests fluxer_linux_screen_capture\n" - }, - { - "name": "linux-audio-capture-build", - "command": "pnpm --dir native/linux-audio-capture build", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:37.620Z", - "endedAt": "2026-06-05T15:40:41.518Z", - "durationMs": 3898, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/linux-audio-capture@0.0.0 build /workspaces/fluxer/fluxer_desktop/native/linux-audio-capture\n> node scripts/build-rust.mjs\n\n[buildRustNodeAddon] cwd=/workspaces/fluxer/fluxer_desktop/native/linux-audio-capture\ncargo build --release --target aarch64-unknown-linux-gnu --manifest-path Cargo.toml\n", - "stderrTail": " Compiling fluxer_linux_audio_capture v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/linux-audio-capture)\n Finished `release` profile [optimized] target(s) in 1.29s\n" - }, - { - "name": "linux-audio-capture-tests", - "command": "pnpm --dir native/linux-audio-capture test", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-05T15:40:41.518Z", - "endedAt": "2026-06-05T15:40:45.392Z", - "durationMs": 3874, - "stdoutTail": "../mac-app-audio |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-sysctl |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../mac-tcc |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../macos-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"darwin\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"linux\",\"cpu\":\"arm64\",\"libc\":\"glibc\"})\n\n> @fluxer/linux-audio-capture@0.0.0 test /workspaces/fluxer/fluxer_desktop/native/linux-audio-capture\n> cargo test --manifest-path Cargo.toml\n\n\nrunning 69 tests\ntest audio_contract::tests::sample_duration_conversion_uses_whole_audio_frames ... ok\ntest audio_contract::tests::whole_frame_sample_count_trims_incomplete_channel_frames ... ok\ntest audio_contract::tests::routing_parser_caps_are_intentionally_finite ... ok\ntest audio_contract::tests::direct_append_slice_keeps_only_complete_stereo_samples_within_queue_cap ... ok\ntest audio_contract::tests::direct_read_count_is_bounded_and_stereo_aligned ... ok\ntest audio_contract::tests::direct_read_count_uses_stable_twenty_ms_chunks ... ok\ntest audio_contract::tests::direct_capture_latency_matches_stable_screen_share_buffer ... ok\ntest direct_buffer::tests::format_changes_clear_queued_samples ... ok\ntest env::tests::hint_str_matches_audio_backend_js_contract ... ok\ntest direct_buffer::tests::read_emits_stable_twenty_ms_chunks_with_continuous_timestamps ... ok\ntest env::tests::none_when_no_audio_hint_present ... ok\ntest env::tests::pipewire_wins_when_both_present ... ok\ntest env::tests::prefers_pipewire_when_pipewire_runtime_dir_set ... ok\ntest env::tests::pulse_server_falls_through_to_pulseaudio_when_no_pipewire_hint ... ok\ntest env::tests::xdg_runtime_dir_alone_is_enough_for_pipewire_hint ... ok\ntest pipewire_bridge::tests::direct_chunk_payload_range_respects_pipewire_chunk_offset ... ok\ntest pipewire_bridge::tests::direct_stream_props_capture_private_sink_monitor ... ok\ntest pipewire_bridge::tests::direct_audio_info_advertises_stereo_fl_fr ... ok\ntest pipewire_bridge::tests::is_routable_media_class_matches_audio_node_classes ... ok\ntest pipewire_bridge::tests::link_props_carry_per_port_routing ... ok\ntest pipewire_bridge::tests::private_sink_props_advertise_hidden_audio_sink ... ok\ntest pipewire_bridge::tests::virtual_sink_props_match_legacy_contract ... ok\ntest pipewire_bridge::tests::next_direct_sink_name_is_unique_per_call ... ok\ntest pulse_bridge::tests::pulse_direct_read_size_matches_capture_latency ... ok\ntest pipewire_bridge::tests::default_sink_target_id_uses_object_serial_for_matching_node_name ... ok\ntest pipewire_bridge::tests::inventory_does_not_inherit_client_object_serial_as_node_target ... ok\ntest pipewire_bridge::tests::inventory_falls_back_to_pipewire_security_pid ... ok\ntest pipewire_bridge::tests::inventory_enriches_nodes_with_owning_client_identity ... ok\ntest pipewire_bridge::tests::inventory_keeps_node_properties_authoritative_over_client_props ... ok\ntest pipewire_bridge::tests::matching_pinned_capture_nodes_only_matches_record_stream_inputs ... ok\ntest pipewire_bridge::tests::pick_node_ports_applies_same_fallback_to_private_capture_inputs ... ok\ntest pipewire_bridge::tests::pick_source_output_ports_falls_back_to_first_two_jack_style_ports ... ok\ntest pipewire_bridge::tests::pick_source_output_ports_fans_mono_to_both_inputs ... ok\ntest direct_buffer::tests::overflow_drops_from_front_and_advances_timestamp ... ok\ntest pipewire_bridge::tests::pick_source_output_ports_prefers_stereo_pair ... ok\ntest pipewire_bridge::tests::pick_source_output_ports_returns_none_when_only_one_side_present ... ok\ntest pipewire_bridge::tests::pick_source_output_ports_treats_blank_channel_as_mono ... ok\ntest pulse_bridge::tests::build_input_props_synthesises_media_class_and_target_object ... ok\ntest pulse_bridge::tests::extract_target_pid_prefers_include_then_pin ... ok\ntest pipewire_bridge::tests::pick_source_output_ports_returns_none_when_no_outputs ... ok\ntest routing::tests::empty_rules_route_nothing_and_sink_id_is_excluded ... ok\ntest pulse_bridge::tests::daemon_unreachable_returns_none_from_open ... ok\ntest routing::tests::include_mode_honors_hardware_filtering_and_never_rules ... ok\ntest routing::tests::include_mode_rejects_non_playback_nodes_that_match_the_include_filter ... ok\ntest routing::tests::matches_any_requires_at_least_one_matching_pattern ... ok\ntest routing::tests::missing_keys_and_mismatched_values_do_not_match ... ok\ntest routing::tests::parse_default_sink_name_is_strict_and_tolerant ... ok\ntest routing::tests::routing_refuses_non_playback_media_classes_even_when_included ... ok\ntest routing::tests::self_identity_matches_across_documented_pipewire_keys ... ok\ntest routing::tests::structural_self_identity_wins_over_include_rules ... ok\ntest routing::tests::synthetic_display_pattern_keys_do_not_affect_routing ... ok\ntest routing::tests::system_mode_routes_only_default_playback_streams ... ok\ntest self_identity::tests::parse_ppid_handles_comm_with_spaces_and_parens ... ok\ntest routing::tests::synthetic_target_object_pattern_matches_name_or_serial ... ok\ntest self_identity::tests::parse_ppid_handles_plain_stat ... ok\ntest pipewire_bridge::tests::daemon_unreachable_returns_none_from_open ... ok\ntest pipewire_bridge::tests::integration_smoke_apply_release_cycle_is_safe ... ok\ntest pulse_bridge::tests::direct_capture_open_then_drop_is_safe_without_daemon ... ok\ntest routing::tests::system_mode_accepts_default_sink_object_id_targets ... ok\ntest self_identity::tests::strip_exe_suffix_handles_known_extensions ... ok\ntest routing::tests::system_mode_accepts_untargeted_and_node_target_streams ... ok\ntest pipewire_bridge::tests::direct_open_returns_none_or_cleans_up ... ok\ntest self_identity::tests::parse_ppid_returns_none_on_malformed_input ... ok\ntest self_identity::tests::populate_self_identity_records_own_pid ... ok\ntest pulse_bridge::tests::direct_capture_start_then_drop_without_daemon_is_clean ... ok\ntest pulse_bridge::tests::per_pid_sink_args_format_is_stable ... ok\ntest routing::tests::empty_pattern_matches_any_candidate ... ok\ntest pulse_bridge::tests::integration_smoke_apply_release_cycle_does_not_deadlock ... ok\ntest pulse_bridge::tests::push_f32_chunk_respects_queue_cap_and_self_consistent_after_drain ... ok\n\ntest result: ok. 69 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n", - "stderrTail": " Compiling fluxer_linux_audio_capture v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/linux-audio-capture)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 1.32s\n Running unittests src/lib.rs (target/debug/deps/fluxer_linux_audio_capture-d5f10b7d43827c26)\n[W][15:40:45.313376] pw.conf | [ conf.c: 1182 try_load_conf()] can't load config client.conf: No such file or directory\n[E][15:40:45.313405] pw.conf | [ conf.c: 1215 pw_conf_load_conf_for_context()] can't load config client.conf: No such file or directory\n[W][15:40:45.313191] pw.conf | [ conf.c: 1182 try_load_conf()] can't load config client.conf: No such file or directory\n[E][15:40:45.313441] pw.conf | [ conf.c: 1215 pw_conf_load_conf_for_context()] can't load config client.conf: No such file or directory\n[W][15:40:45.313613] pw.conf | [ conf.c: 1182 try_load_conf()] can't load config client.conf: No such file or directory\n[E][15:40:45.313620] pw.conf | [ conf.c: 1215 pw_conf_load_conf_for_context()] can't load config client.conf: No such file or directory\n Doc-tests fluxer_linux_audio_capture\n" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/2026-06-06T22-10-30Z-codex-host-smoke/summary.json b/fluxer_desktop/native-media-reports/2026-06-06T22-10-30Z-codex-host-smoke/summary.json deleted file mode 100644 index 9ad9b41e2..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-06T22-10-30Z-codex-host-smoke/summary.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "status": "fail", - "mode": "smoke", - "strict": false, - "platform": "darwin", - "arch": "arm64", - "hostname": "Hampuss-Mac-Studio.local", - "startedAt": "2026-06-06T22:10:30.484Z", - "endedAt": "2026-06-06T22:11:23.013Z", - "reportDir": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-06T22-10-30Z-codex-host-smoke", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "mac-screen-capture-build", - "command": "pnpm --dir native/mac-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-screen-capture-tests", - "command": "pnpm --dir native/mac-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-build", - "command": "pnpm --dir native/mac-app-audio build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-rust-tests", - "command": "pnpm --dir native/mac-app-audio test:cargo", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-js-tests", - "command": "'/Users/hampus/Development/fluxer/node_modules/.pnpm/node_modules/.bin/vitest' run", - "category": "platform-native", - "cwd": "native/mac-app-audio" - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:10:30.486Z", - "endedAt": "2026-06-06T22:10:30.704Z", - "durationMs": 218, - "stdoutTail": "▶ NativeScreenCapture DMA-BUF metadata validation\n ✔ accepts valid DMA-BUF metadata and pads native plane arrays (2.191542ms)\n ✔ keeps valid handles when optional device UUID metadata is absent or malformed (0.45425ms)\n ✔ rejects invalid plane counts, FDs, strides, and format metadata (0.478709ms)\n✔ NativeScreenCapture DMA-BUF metadata validation (3.578041ms)\n▶ NativeScreenCapture source identity and capability reporting\n ✔ normalizes display and window sources without changing source ids (0.671125ms)\n ✔ starts display and window captures with exact source id and kind and reports diagnostics (1.393083ms)\n ✔ forwards frames to renderer even when a native frame tap is installed (0.572875ms)\n ✔ reports macOS and Linux display/window capture capabilities from platform backends (1.508583ms)\n✔ NativeScreenCapture source identity and capability reporting (4.293125ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ pushes the initial capture frame after publishing screen share (2.516958ms)\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU frames (0.687709ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.544916ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.468917ms)\n ✔ keeps screen-share audio published when screen video is unpublished (0.553791ms)\n ✔ replaces screen capture taps without releasing screen-share audio or duplicating priority (0.479167ms)\n ✔ forwards watch and unwatch operations with source, enablement, and quality intact (1.627166ms)\n ✔ reports VideoToolbox hardware capability from the native addon (0.5325ms)\n ✔ reports native voice engine capabilities from the native addon (0.4195ms)\n ✔ maps native addon microphone not-connected errors to the typed operation code (0.560208ms)\n✔ NativeVoiceEngine screen frame routing (9.007958ms)\n▶ WebrtcScreenPublish native frame routing\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU buffers (2.107208ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.448834ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.413166ms)\n ✔ replaces an existing publisher before routing frames for the same capture id (0.406ms)\n✔ WebrtcScreenPublish native frame routing (3.859209ms)\nℹ tests 21\nℹ suites 4\nℹ pass 21\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 155.770792\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:10:30.705Z", - "endedAt": "2026-06-06T22:10:32.150Z", - "durationMs": 1445, - "stdoutTail": "native/linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> fluxer_desktop@0.0.0 typecheck /Users/hampus/Development/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:10:32.150Z", - "endedAt": "2026-06-06T22:11:01.715Z", - "durationMs": 29565, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/webrtc-sender@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml --features camera-native\n", - "stderrTail": "warning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\n Compiling fluxer_webrtc_sender v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender)\n Finished `release` profile [optimized] target(s) in 9.86s\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "fail", - "code": 1, - "signal": null, - "startedAt": "2026-06-06T22:11:01.715Z", - "endedAt": "2026-06-06T22:11:23.013Z", - "durationMs": 21298, - "stdoutTail": "e_contract_range ... ok\ntest audio::tests::is_muted_only_at_zero ... ok\ntest audio::tests::processing_options_preserve_defaults_unless_overridden ... ok\ntest audio::tests::output_devices_json_array ... ok\ntest audio::tests::output_device_json_locks_the_contract_shape ... ok\ntest audio::tests::input_devices_json_array ... ok\ntest audio::tests::shape_input_marks_index_zero_default_and_drops_empty_ids ... ok\ntest audio::tests::shape_marks_index_zero_default_and_drops_empty_ids ... ok\ntest camera::live::tests::fallback_format_selection_uses_decoder_supported_non_mjpeg_modes ... ok\ntest camera::live::tests::fallback_format_selection_prefers_nearest_resolution_before_format_priority ... ok\ntest camera::tests::from_opts_applies_defaults ... ok\ntest camera::tests::from_opts_forces_even_dims_and_honours_overrides ... ok\ntest camera::tests::from_opts_parses_device_index ... ok\ntest camera::tests::from_opts_preserves_string_device_ids ... ok\ntest camera::tests::from_opts_rejects_degenerate_dims_and_fps ... ok\ntest config::tests::accepts_dimension_boundaries_and_rejects_zero_fps ... ok\ntest config::tests::canonical_codec_name_accepts_all_five_case_insensitively ... ok\ntest config::tests::canonical_codec_name_rejects_empty_and_unknown ... ok\ntest config::tests::rejects_bad_url ... ok\ntest config::tests::rejects_empty_token ... ok\ntest config::tests::rejects_odd_or_oob_dimensions ... ok\ntest config::tests::rejects_unknown_non_empty_codec_but_allows_empty_default ... ok\ntest config::tests::state_gates_frames_and_connect ... ok\ntest config::tests::valid_config_passes ... ok\ntest engine::tests::adaptive_video_sender_buffers_two_fresh_frames_without_coalescing ... ok\ntest engine::tests::adaptive_video_sender_coalesces_only_when_fresh_queue_overflows ... ok\ntest engine::tests::adaptive_video_sender_drops_stale_queued_frame_without_counting_coalescing ... ok\ntest engine::tests::adaptive_video_sender_stop_clears_pending_and_rejects_new_frames ... ok\ntest engine::tests::bgra_input_validation_requires_even_dims_stride_and_complete_buffer ... ok\ntest engine::tests::nv12_input_validation_requires_complete_luma_and_chroma_planes ... ok\ntest engine::tests::parse_codec_agrees_with_pure_canonicaliser ... ok\ntest engine::tests::parse_codec_maps_all_five_including_h265 ... ok\ntest engine::tests::parse_codec_rejects_empty_and_unknown ... ok\ntest engine::tests::parse_track_source_accepts_renderer_and_livekit_spellings ... ok\ntest engine::tests::parse_video_quality_accepts_subscription_quality_levels ... ok\ntest engine::tests::pcm16_audio_frame_rejects_malformed_buffers ... ok\ntest engine::tests::pcm16_audio_frame_shapes_interleaved_stereo_samples ... ok\ntest engine::tests::session_rtt_falls_back_to_subscriber_and_preserves_existing_rtt ... ok\ntest engine::tests::session_rtt_uses_peer_connection_candidate_pair_without_track_stats ... ok\ntest engine::tests::tight_i420_len_matches_plane_geometry ... ok\ntest engine::tests::video_frame_meta_json_matches_contract ... ok\ntest engine::tests::video_input_validation_rejects_degenerate_odd_and_oversized_frames ... ok\ntest engine::tests::video_timestamp_validation_rejects_values_that_cast_badly_at_napi_boundary ... ok\ntest events::tests::empty_object_and_array ... ok\ntest events::tests::escapes_quote_backslash_and_control_chars ... ok\ntest events::tests::json_object_preserves_order_and_mixes_str_and_raw ... ok\ntest events::tests::json_u8_array_serializes_bytes_as_numbers ... ok\ntest events::tests::leaves_plain_ascii_and_unicode_untouched ... ok\ntest events::tests::raw_array_preserves_prebuilt_json_objects ... ok\ntest hardware_encoder::tests::publish_codec_runtime_gate_accepts_h265_when_reported_available ... ok\ntest hardware_encoder::tests::publish_codec_runtime_gate_only_requires_hardware_for_h265 ... ok\ntest hardware_encoder::tests::reports_available_nvenc_when_compiled_and_runtime_ready ... ok\ntest hardware_encoder::tests::reports_available_videotoolbox_with_h264_and_h265 ... ok\ntest hardware_encoder::tests::reports_driver_sdk_incompatibility_before_encode_session ... ok\ntest hardware_encoder::tests::reports_h264_only_when_hevc_guid_is_absent ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_compiled ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_probe_is_absent ... ok\ntest hardware_encoder::tests::reports_no_cuda_devices_using_obs_style_reason ... ok\ntest hardware_encoder::tests::reports_no_supported_codecs_when_guid_query_finds_no_fluxer_codecs ... ok\ntest hardware_encoder::tests::reports_not_compiled_before_runtime_missing ... ok\ntest hardware_encoder::tests::reports_unsupported_platforms_clearly ... ok\ntest hardware_encoder::tests::reports_videotoolbox_h264_only_when_hevc_session_is_unavailable ... ok\ntest hardware_encoder::tests::reports_videotoolbox_unavailable_with_status_details ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_publish_backend_not_compiled ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_runtime_dll_names_when_missing ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_without_linux_zero_copy_inputs ... ok\ntest publisher::tests::parse_codec_maps_known_names ... ok\ntest publisher::tests::i420_plane_copy_fills_libwebrtc_buffer ... ok\ntest send_control::tests::audio_buffer_growth_caps_at_configured_max_and_reset_clears_pressure ... ok\ntest send_control::tests::audio_buffer_expands_on_gaps_and_shrinks_after_stability ... ok\ntest send_control::tests::audio_buffer_ignores_clock_regression_without_false_rebuffer ... ok\ntest send_control::tests::audio_buffer_max_is_clamped_to_real_time_bounds ... ok\ntest send_control::tests::idle_snapshot_reflects_audio_pressure_without_video_state ... ok\ntest send_control::tests::video_controller_continues_degrading_to_minimum_under_sustained_pressure ... ok\ntest send_control::tests::video_controller_counts_coalesced_replacements_once_for_pressure ... ok\ntest send_control::tests::audio_buffer_ignores_concurrent_stale_pushes_without_moving_last_push_backwards ... ok\ntest send_control::tests::video_controller_degrades_on_encoder_egress_pressure ... ok\ntest send_control::tests::audio_buffer_stress_stays_within_realtime_bounds_under_jitter ... ok\ntest send_control::tests::video_controller_degrades_on_latency_pressure_without_coalescing ... ok\ntest send_control::tests::video_controller_degrades_on_repeated_coalescing_pressure ... ok\ntest send_control::tests::video_controller_degrades_when_disrupted_frame_ratio_exceeds_threshold ... ok\ntest send_control::tests::video_controller_ignores_bounded_one_frame_coalescing_pressure ... ok\ntest send_control::tests::video_controller_ignores_single_encoder_egress_sample ... ok\ntest send_control::tests::video_controller_keeps_requested_fps_when_adaptive_send_is_disabled ... ok\ntest send_control::tests::video_controller_preserves_pressure_reason_while_degraded_but_stable ... ok\ntest send_control::tests::video_controller_recovers_after_stable_windows ... ok\ntest send_control::tests::video_controller_recovers_from_minimum_in_two_stable_steps ... ok\ntest send_control::tests::video_controller_requires_a_full_stable_recovery_window ... ok\ntest send_control::tests::video_controller_waits_until_pressure_window_boundary ... ok\ntest send_control::tests::video_stats_effective_fps_uses_capture_time_bounds_for_out_of_order_records ... ok\ntest send_control::tests::video_stats_snapshot_counts_coalescing_drops_failures_and_effective_fps ... ok\ntest stats::tests::bitrate_computes_kbps_from_byte_delta ... ok\ntest stats::tests::bitrate_first_sample_is_zero ... ok\ntest stats::tests::bitrate_half_second_doubles_rate ... ok\ntest send_control::tests::video_controller_stress_keeps_target_inside_configured_bounds ... ok\ntest stats::tests::bitrate_rejects_backwards_time_and_bytes ... ok\ntest stats::tests::empty_stats_serialise_to_null_rtt_and_empty_arrays ... ok\ntest stats::tests::sanitize_audio_level_clamps ... ok\ntest stats::tests::full_stats_serialise_to_exact_contract_shape ... ok\ntest stats::tests::sanitize_kbps_drops_nan_inf_negative_and_rounds ... ok\ntest stats::tests::send_health_serialises_to_exact_contract_shape ... ok\ntest stats::tests::unit_conversions_seconds_to_ms ... ok\ntest stats::tests::video_inbound_omits_audio_level_audio_omits_fps ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_populates_all_plane_arrays ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_incomplete_native_inputs ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_negative_fds ... ok\ntest texture_source::tests::drm_format_gate_accepts_obs_vkcapture_texture_formats ... ok\ntest texture_source::tests::dxgi_format_gate_accepts_8bit_rgba_bgra_only ... ok\ntest send_control::tests::video_stats_handles_concurrent_recording_without_lost_counts ... ok\ntest texture_source::tests::error_strings_are_stable ... ok\ntest texture_source::tests::probe_is_available_only_for_explicit_hardware_codecs ... ok\ntest texture_source::tests::should_attempt_dmabuf_is_sdk_gated_after_validation_capability ... ok\ntest texture_source::tests::should_attempt_falls_back_when_capability_unavailable ... ok\ntest texture_source::tests::should_attempt_validates_frame_when_capability_available ... ok\ntest texture_source::tests::validate_accepts_a_clean_bgra_texture ... ok\ntest texture_source::tests::validate_dmabuf_accepts_supported_formats_with_optional_uuid ... ok\ntest texture_source::tests::validate_dmabuf_rejects_invalid_planes ... ok\ntest texture_source::tests::validate_precedence_handle_before_dims_before_format ... ok\ntest texture_source::tests::validate_rejects_odd_zero_and_oversized_dims ... ok\ntest texture_source::tests::validate_rejects_unsupported_format ... ok\ntest texture_source::tests::validate_rejects_zero_handle_as_no_texture ... ok\ntest yuv::tests::bgra_black_white_grey_levels ... ok\ntest yuv::tests::bgra_golden_2x2_solid_colour_exact_bytes ... ok\ntest yuv::tests::bgra_golden_strided_2x2_skips_row_padding ... ok\ntest yuv::tests::bgra_plane_sizes ... ok\ntest yuv::tests::bgra_primaries_have_expected_chroma_signs ... ok\ntest yuv::tests::copy_nv12_planes_preserves_nv12_layout ... ok\ntest yuv::tests::copy_nv12_planes_rejects_short_buffers ... ok\ntest yuv::tests::copy_nv12_planes_respects_destination_stride ... ok\ntest yuv::tests::nv12_4x4_deinterleaves_two_chroma_columns ... ok\ntest yuv::tests::nv12_golden_4x2_packed_to_i420 ... ok\ntest yuv::tests::nv12_packed_2x2_deinterleaves ... ok\ntest yuv::tests::nv12_respects_row_padding ... ok\ntest yuv::tests::nv12_short_buffer_is_rejected ... ok\ntest yuv::tests::rejects_odd_or_tiny_dimensions ... ok\ntest yuv::tests::rgb_golden_2x2_solid_colour_matches_bgra_path ... ok\ntest yuv::tests::rgb_plane_sizes_and_levels ... ok\ntest yuv::tests::rgb_rejects_odd_dims_and_short_buffer ... ok\ntest engine::tests::pcm16_audio_frame_rejects_implausible_format_and_overlong_chunks ... ok\n\ntest result: ok. 140 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n\n\nrunning 6 tests\ntest outbound_fixtures_match_contract ... ok\ntest video_frame_meta_matches_contract ... ok\ntest inbound_covers_subscribe_and_lifecycle ... ok\ntest outbound_covers_the_single_identity_publish_path ... ok\ntest e2ee_state_is_ok_on_the_wire ... ok\ntest inbound_fixtures_match_contract ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n✖ index.test.mjs (65.935708ms)\n✖ scripts/livekit-harness.test.mjs (68.75525ms)\nℹ tests 2\nℹ suites 0\nℹ pass 0\nℹ fail 2\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 75.266917\n\n✖ failing tests:\n\ntest at index.test.mjs:1:1\n✖ index.test.mjs (65.935708ms)\n 'test failed'\n\ntest at scripts/livekit-harness.test.mjs:1:1\n✖ scripts/livekit-harness.test.mjs (68.75525ms)\n 'test failed'\n ELIFECYCLE  Test failed. See above for more details.\n WARN  Local package.json exists, but node_modules missing, did you mean to install?\n", - "stderrTail": " Compiling ring v0.17.14\n Compiling fluxer_webrtc_sender v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender)\n Compiling fluxer_voice_core v0.1.0 (/Users/hampus/Development/fluxer/fluxer_voice_core)\n Compiling rustls v0.23.40\n Compiling rustls-webpki v0.103.13\n Compiling tokio-rustls v0.26.4\n Compiling tungstenite v0.29.0\n Compiling hyper-rustls v0.27.9\n Compiling reqwest v0.12.28\n Compiling tokio-tungstenite v0.29.0\n Compiling livekit-api v0.5.0\n Compiling webrtc-sys-build v0.3.18\n Compiling webrtc-sys v0.3.33 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys)\n Compiling libwebrtc v0.3.35 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/vendor/libwebrtc)\nwarning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\n Compiling livekit v0.7.43\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 17.99s\n Running unittests src/lib.rs (target/debug/deps/fluxer_webrtc_sender-4fb413a724225319)\n Running tests/event_contract.rs (target/debug/deps/event_contract-dc15840aabc01f6c)\n Doc-tests fluxer_webrtc_sender\n" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/livekit-harness.json b/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/livekit-harness.json deleted file mode 100644 index 690ca680c..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/livekit-harness.json +++ /dev/null @@ -1,197 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-06T22:15:58.713Z", - "endedAt": "2026-06-06T22:16:28.718Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "serverApiUrl": "http://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-84586-8f492568-efae-42a1-a344-5a0c9599fa8d", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-84586", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-84586", - "subscriberIdentity": "fluxer-native-subscriber-84586", - "required": false, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/livekit-harness.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": false - }, - { - "name": "subscriber video resolution 320x180", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 3", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 1", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 3", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1346:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1773:3)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:2025:18)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:2058:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": null, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/summary.json b/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/summary.json deleted file mode 100644 index fe5200c39..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed/summary.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "status": "fail", - "mode": "smoke", - "strict": false, - "platform": "darwin", - "arch": "arm64", - "hostname": "Hampuss-Mac-Studio.local", - "startedAt": "2026-06-06T22:15:48.881Z", - "endedAt": "2026-06-06T22:16:28.858Z", - "reportDir": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-06T22-15-48Z-codex-host-smoke-signed", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "mac-screen-capture-build", - "command": "pnpm --dir native/mac-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-screen-capture-tests", - "command": "pnpm --dir native/mac-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-build", - "command": "pnpm --dir native/mac-app-audio build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-rust-tests", - "command": "pnpm --dir native/mac-app-audio test:cargo", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-js-tests", - "command": "'/Users/hampus/Development/fluxer/node_modules/.pnpm/node_modules/.bin/vitest' run", - "category": "platform-native", - "cwd": "native/mac-app-audio" - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:15:48.882Z", - "endedAt": "2026-06-06T22:15:49.087Z", - "durationMs": 205, - "stdoutTail": "▶ NativeScreenCapture DMA-BUF metadata validation\n ✔ accepts valid DMA-BUF metadata and pads native plane arrays (2.208167ms)\n ✔ keeps valid handles when optional device UUID metadata is absent or malformed (0.448834ms)\n ✔ rejects invalid plane counts, FDs, strides, and format metadata (0.462542ms)\n✔ NativeScreenCapture DMA-BUF metadata validation (3.565042ms)\n▶ NativeScreenCapture source identity and capability reporting\n ✔ normalizes display and window sources without changing source ids (0.66975ms)\n ✔ starts display and window captures with exact source id and kind and reports diagnostics (1.315458ms)\n ✔ forwards frames to renderer even when a native frame tap is installed (0.511625ms)\n ✔ reports macOS and Linux display/window capture capabilities from platform backends (1.343834ms)\n✔ NativeScreenCapture source identity and capability reporting (3.975166ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ pushes the initial capture frame after publishing screen share (2.103875ms)\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU frames (0.476083ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.5025ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.419958ms)\n ✔ keeps screen-share audio published when screen video is unpublished (0.485458ms)\n ✔ replaces screen capture taps without releasing screen-share audio or duplicating priority (0.930666ms)\n ✔ forwards watch and unwatch operations with source, enablement, and quality intact (0.89425ms)\n ✔ reports VideoToolbox hardware capability from the native addon (0.447916ms)\n ✔ reports native voice engine capabilities from the native addon (0.39275ms)\n ✔ maps native addon microphone not-connected errors to the typed operation code (0.527125ms)\n✔ NativeVoiceEngine screen frame routing (7.720792ms)\n▶ WebrtcScreenPublish native frame routing\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU buffers (1.90625ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.453542ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.402ms)\n ✔ replaces an existing publisher before routing frames for the same capture id (0.400541ms)\n✔ WebrtcScreenPublish native frame routing (3.595417ms)\nℹ tests 21\nℹ suites 4\nℹ pass 21\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 144.171292\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:15:49.088Z", - "endedAt": "2026-06-06T22:15:50.164Z", - "durationMs": 1076, - "stdoutTail": "native/linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> fluxer_desktop@0.0.0 typecheck /Users/hampus/Development/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:15:50.164Z", - "endedAt": "2026-06-06T22:15:54.767Z", - "durationMs": 4603, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/webrtc-sender@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml --features camera-native\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/webrtc-sender.darwin-arm64.node\n", - "stderrTail": "warning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\n Finished `release` profile [optimized] target(s) in 0.17s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/webrtc-sender.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:15:54.767Z", - "endedAt": "2026-06-06T22:15:57.734Z", - "durationMs": 2967, - "stdoutTail": ".. ok\ntest hardware_encoder::tests::publish_codec_runtime_gate_only_requires_hardware_for_h265 ... ok\ntest hardware_encoder::tests::reports_available_nvenc_when_compiled_and_runtime_ready ... ok\ntest hardware_encoder::tests::reports_available_videotoolbox_with_h264_and_h265 ... ok\ntest hardware_encoder::tests::reports_driver_sdk_incompatibility_before_encode_session ... ok\ntest hardware_encoder::tests::reports_h264_only_when_hevc_guid_is_absent ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_compiled ... ok\ntest hardware_encoder::tests::reports_no_cuda_devices_using_obs_style_reason ... ok\ntest hardware_encoder::tests::reports_no_supported_codecs_when_guid_query_finds_no_fluxer_codecs ... ok\ntest hardware_encoder::tests::reports_not_compiled_before_runtime_missing ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_probe_is_absent ... ok\ntest hardware_encoder::tests::reports_unsupported_platforms_clearly ... ok\ntest hardware_encoder::tests::reports_videotoolbox_h264_only_when_hevc_session_is_unavailable ... ok\ntest hardware_encoder::tests::reports_videotoolbox_unavailable_with_status_details ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_publish_backend_not_compiled ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_runtime_dll_names_when_missing ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_without_linux_zero_copy_inputs ... ok\ntest publisher::tests::i420_plane_copy_fills_libwebrtc_buffer ... ok\ntest publisher::tests::parse_codec_maps_known_names ... ok\ntest send_control::tests::audio_buffer_growth_caps_at_configured_max_and_reset_clears_pressure ... ok\ntest send_control::tests::audio_buffer_ignores_clock_regression_without_false_rebuffer ... ok\ntest send_control::tests::audio_buffer_expands_on_gaps_and_shrinks_after_stability ... ok\ntest send_control::tests::audio_buffer_max_is_clamped_to_real_time_bounds ... ok\ntest send_control::tests::idle_snapshot_reflects_audio_pressure_without_video_state ... ok\ntest send_control::tests::video_controller_counts_coalesced_replacements_once_for_pressure ... ok\ntest send_control::tests::video_controller_continues_degrading_to_minimum_under_sustained_pressure ... ok\ntest send_control::tests::audio_buffer_stress_stays_within_realtime_bounds_under_jitter ... ok\ntest send_control::tests::video_controller_degrades_on_encoder_egress_pressure ... ok\ntest send_control::tests::audio_buffer_ignores_concurrent_stale_pushes_without_moving_last_push_backwards ... ok\ntest send_control::tests::video_controller_degrades_on_latency_pressure_without_coalescing ... ok\ntest send_control::tests::video_controller_degrades_on_repeated_coalescing_pressure ... ok\ntest send_control::tests::video_controller_degrades_when_disrupted_frame_ratio_exceeds_threshold ... ok\ntest send_control::tests::video_controller_ignores_bounded_one_frame_coalescing_pressure ... ok\ntest send_control::tests::video_controller_ignores_single_encoder_egress_sample ... ok\ntest send_control::tests::video_controller_keeps_requested_fps_when_adaptive_send_is_disabled ... ok\ntest send_control::tests::video_controller_preserves_pressure_reason_while_degraded_but_stable ... ok\ntest send_control::tests::video_controller_recovers_after_stable_windows ... ok\ntest send_control::tests::video_controller_recovers_from_minimum_in_two_stable_steps ... ok\ntest send_control::tests::video_controller_requires_a_full_stable_recovery_window ... ok\ntest send_control::tests::video_controller_waits_until_pressure_window_boundary ... ok\ntest send_control::tests::video_stats_effective_fps_uses_capture_time_bounds_for_out_of_order_records ... ok\ntest send_control::tests::video_stats_snapshot_counts_coalescing_drops_failures_and_effective_fps ... ok\ntest stats::tests::bitrate_computes_kbps_from_byte_delta ... ok\ntest send_control::tests::video_controller_stress_keeps_target_inside_configured_bounds ... ok\ntest stats::tests::bitrate_first_sample_is_zero ... ok\ntest stats::tests::bitrate_half_second_doubles_rate ... ok\ntest stats::tests::bitrate_rejects_backwards_time_and_bytes ... ok\ntest stats::tests::empty_stats_serialise_to_null_rtt_and_empty_arrays ... ok\ntest stats::tests::sanitize_audio_level_clamps ... ok\ntest send_control::tests::video_stats_handles_concurrent_recording_without_lost_counts ... ok\ntest stats::tests::sanitize_kbps_drops_nan_inf_negative_and_rounds ... ok\ntest stats::tests::full_stats_serialise_to_exact_contract_shape ... ok\ntest stats::tests::send_health_serialises_to_exact_contract_shape ... ok\ntest stats::tests::unit_conversions_seconds_to_ms ... ok\ntest stats::tests::video_inbound_omits_audio_level_audio_omits_fps ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_populates_all_plane_arrays ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_incomplete_native_inputs ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_negative_fds ... ok\ntest texture_source::tests::drm_format_gate_accepts_obs_vkcapture_texture_formats ... ok\ntest texture_source::tests::dxgi_format_gate_accepts_8bit_rgba_bgra_only ... ok\ntest texture_source::tests::error_strings_are_stable ... ok\ntest texture_source::tests::probe_is_available_only_for_explicit_hardware_codecs ... ok\ntest texture_source::tests::should_attempt_dmabuf_is_sdk_gated_after_validation_capability ... ok\ntest texture_source::tests::should_attempt_falls_back_when_capability_unavailable ... ok\ntest texture_source::tests::should_attempt_validates_frame_when_capability_available ... ok\ntest texture_source::tests::validate_accepts_a_clean_bgra_texture ... ok\ntest texture_source::tests::validate_dmabuf_accepts_supported_formats_with_optional_uuid ... ok\ntest texture_source::tests::validate_dmabuf_rejects_invalid_planes ... ok\ntest texture_source::tests::validate_precedence_handle_before_dims_before_format ... ok\ntest texture_source::tests::validate_rejects_odd_zero_and_oversized_dims ... ok\ntest texture_source::tests::validate_rejects_unsupported_format ... ok\ntest texture_source::tests::validate_rejects_zero_handle_as_no_texture ... ok\ntest yuv::tests::bgra_black_white_grey_levels ... ok\ntest yuv::tests::bgra_golden_2x2_solid_colour_exact_bytes ... ok\ntest yuv::tests::bgra_golden_strided_2x2_skips_row_padding ... ok\ntest yuv::tests::bgra_plane_sizes ... ok\ntest yuv::tests::bgra_primaries_have_expected_chroma_signs ... ok\ntest yuv::tests::copy_nv12_planes_preserves_nv12_layout ... ok\ntest yuv::tests::copy_nv12_planes_rejects_short_buffers ... ok\ntest yuv::tests::copy_nv12_planes_respects_destination_stride ... ok\ntest yuv::tests::nv12_4x4_deinterleaves_two_chroma_columns ... ok\ntest yuv::tests::nv12_golden_4x2_packed_to_i420 ... ok\ntest yuv::tests::nv12_packed_2x2_deinterleaves ... ok\ntest yuv::tests::nv12_respects_row_padding ... ok\ntest yuv::tests::nv12_short_buffer_is_rejected ... ok\ntest yuv::tests::rejects_odd_or_tiny_dimensions ... ok\ntest yuv::tests::rgb_golden_2x2_solid_colour_matches_bgra_path ... ok\ntest yuv::tests::rgb_plane_sizes_and_levels ... ok\ntest yuv::tests::rgb_rejects_odd_dims_and_short_buffer ... ok\ntest engine::tests::pcm16_audio_frame_rejects_implausible_format_and_overlong_chunks ... ok\n\ntest result: ok. 140 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 6 tests\ntest video_frame_meta_matches_contract ... ok\ntest inbound_covers_subscribe_and_lifecycle ... ok\ntest outbound_covers_the_single_identity_publish_path ... ok\ntest inbound_fixtures_match_contract ... ok\ntest outbound_fixtures_match_contract ... ok\ntest e2ee_state_is_ok_on_the_wire ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ webrtc-sender loader wrapper\n ✔ resolves the native addon filename for every desktop OS/arch target (1.069958ms)\n ✔ rejects unsupported platform/architecture pairs explicitly (0.887333ms)\n ✔ returns a native hardware encoder capability when the binding exports one (0.8905ms)\n ✔ delegates dropped video callback metrics through the VoiceEngine wrapper (0.151208ms)\n ✔ reports VoiceEngine feature capabilities from the wrapped native prototype (0.499375ms)\n ✔ delegates device microphone publish through the VoiceEngine wrapper (0.171625ms)\n ✔ delegates native camera devices and publish options through the VoiceEngine wrapper (0.164625ms)\n ✔ delegates screen-share simulcast selection through the VoiceEngine wrapper (0.157417ms)\n ✔ delegates connect options through the VoiceEngine wrapper (0.206292ms)\n ✔ delegates shared texture handles through the WebrtcPublisher wrapper (0.201958ms)\n ✔ treats shared texture publishing as unavailable for older publisher bindings (0.089625ms)\n ✔ delegates dma-buf frames through the WebrtcPublisher wrapper (0.266709ms)\n ✔ delegates shared texture handles through the VoiceEngine wrapper (0.126375ms)\n ✔ treats shared texture publishing as unavailable for older voice bindings (0.074041ms)\n ✔ treats dma-buf publishing as unavailable for older native bindings (0.104333ms)\n ✔ defaults dropped video callback metrics to zero for older native bindings (0.05725ms)\n ✔ delegates remote track subscription updates through the VoiceEngine wrapper (0.1255ms)\n ✔ ignores remote track subscription updates for older native bindings (0.072583ms)\n ✔ delegates data packets through the VoiceEngine wrapper (0.183833ms)\n ✔ rejects invalid data packet payloads in the wrapper (0.25775ms)\n ✔ returns an unavailable hardware encoder capability without a binding (0.070875ms)\n✔ webrtc-sender loader wrapper (7.269583ms)\n▶ livekit harness setup matrix\n ✔ maps all supported codec spellings to expected mime types (1.39ms)\n ✔ parses codec lists with fallback and rejects empty effective lists (1.192416ms)\n ✔ default non-strict config keeps the fast local live setup small (1.466167ms)\n ✔ strict config expands to multi-codec secondary-publisher scenarios (1.057417ms)\n ✔ stress knobs parse packet loss, resolution stability, audio gap, and video pattern (0.65175ms)\n ✔ stress knobs reject impossible send pacing configurations (3.341208ms)\n ✔ explicit secondary codec expectations override rotating defaults (0.570125ms)\n ✔ secondary screen codec aliases are accepted and conflicting aliases are rejected (0.955083ms)\n ✔ external token setup derives identities, requires matching tokens, and redacts reports (1.394917ms)\n ✔ report sanitization strips URL credentials, query strings, and fragments (0.676625ms)\n ✔ jwt subject and TCP target parsing catch malformed live setups early (0.34125ms)\n ✔ admin token has the room-scoped grant needed for server publishing validation (0.463875ms)\n ✔ scenario room names are stable and bounded for generated-token suites (0.110917ms)\n ✔ strict report checks fail when the strict window has too few samples to measure gaps (90.970125ms)\n ✔ strict report checks use only samples inside the measured window (6.760833ms)\n ✔ strict packet loss gate uses loss deltas inside the strict window (12.074166ms)\n ✔ server publishing checks cover primary and secondary screen, audio, and camera tracks (0.618458ms)\n ✔ server publishing checks fail closed on missing server tracks (0.280125ms)\n ✔ server publishing checks reject wrong participant identity (0.2235ms)\n ✔ server publishing checks reject wrong kind, source, and screen mime (0.254208ms)\n ✔ server publishing checks reject muted or missing mute state on expected tracks (0.213625ms)\n ✔ server publishing checks handle protobuf numeric enums without accepting data tracks as video (0.195584ms)\n ✔ server publishing checks fail closed on missing secondary, audio, camera, and screen-audio tracks (0.208791ms)\n ✔ strict server publishing validation does not pass open on Twirp errors (0.247834ms)\n✔ livekit harness setup matrix (127.168458ms)\nℹ tests 45\nℹ suites 2\nℹ pass 45\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 522.131583\n", - "stderrTail": "warning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 0.16s\n Running unittests src/lib.rs (target/debug/deps/fluxer_webrtc_sender-4fb413a724225319)\n Running tests/event_contract.rs (target/debug/deps/event_contract-dc15840aabc01f6c)\n Doc-tests fluxer_webrtc_sender\n" - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "status": "fail", - "code": 1, - "signal": null, - "startedAt": "2026-06-06T22:15:57.734Z", - "endedAt": "2026-06-06T22:16:28.858Z", - "durationMs": 31124, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/webrtc-sender@0.0.0 test:livekit /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> node scripts/livekit-harness.mjs\n\n[livekit-harness] connecting to ws://127.0.0.1:7880 room=fluxer-webrtc-harness-84586-8f492568-efae-42a1-a344-5a0c9599fa8d\n ELIFECYCLE  Command failed with exit code 1.\n WARN  Local package.json exists, but node_modules missing, did you mean to install?\n", - "stderrTail": "[livekit-harness] FAIL: Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1346:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1773:3)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:2025:18)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:2058:19\n" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/2026-06-06T22-17-15Z-codex-livekit-proxy.json b/fluxer_desktop/native-media-reports/2026-06-06T22-17-15Z-codex-livekit-proxy.json deleted file mode 100644 index 45b967bf8..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-06T22-17-15Z-codex-livekit-proxy.json +++ /dev/null @@ -1,274 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-06T22:17:16.198Z", - "endedAt": "2026-06-06T22:17:18.067Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:8088/livekit", - "serverApiUrl": "http://127.0.0.1:8088", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-84689-a15db5c1-00f9-4cc5-8b1a-e68759b5e380", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-84689", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-84689", - "subscriberIdentity": "fluxer-native-subscriber-84689", - "required": false, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-06T22-17-15Z-codex-livekit-proxy.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 320x180", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 13, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 13, - "videoBytes": 1123200, - "receivedFps": 0, - "requiredFps": null, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 390, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 5, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780784237058, - "stoppedAtMs": null, - "framesPushed": 16, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 69, - "lastPushAtMs": 1780784238060, - "elapsedMs": 1018, - "producedFps": 15.72 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": { - "participantSid": "PA_NicBnT5mxvvm", - "trackSid": "TR_VSrvFY4wSg7eLt", - "width": 320, - "height": 180, - "timestampUs": 335522961000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSrvFY4wSg7eLt", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 0, - "packetsLost": 0, - "fps": 15, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 14.6, - "framesProduced": 14, - "framesAccepted": 14, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 14, - "captureFailures": 0, - "maxQueueAgeMs": 26, - "maxPushLatencyMs": 26, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 14, - "outgoingVideoFramesAccepted": 14, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 14, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.56, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 26, - "outgoingVideoMaxPushLatencyMs": 26, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 2, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_NicBnT5mxvvm", - "trackSid": "TR_VSrvFY4wSg7eLt", - "kind": "video", - "bitrateKbps": 0, - "packetsLost": 5, - "jitterMs": 4, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/livekit-harness.json b/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/livekit-harness.json deleted file mode 100644 index 88d097cd3..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/livekit-harness.json +++ /dev/null @@ -1,321 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-06T22:17:32.639Z", - "endedAt": "2026-06-06T22:17:34.704Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:8088/livekit", - "serverApiUrl": "http://127.0.0.1:8088", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-84848-43854e34-d3a9-4e33-bf45-b7ef3611083a", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-84848", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-84848", - "subscriberIdentity": "fluxer-native-subscriber-84848", - "required": false, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/livekit-harness.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 320x180", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 3", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 1", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 3", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 72, - "inboundVideoFrames": 13, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 13, - "videoBytes": 1123200, - "receivedFps": 0, - "requiredFps": null, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 184, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 0, - "maxObservedPacketLoss": 3, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780784253698, - "stoppedAtMs": null, - "framesPushed": 16, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 68, - "lastPushAtMs": 1780784254699, - "elapsedMs": 1014, - "producedFps": 15.78 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": { - "participantSid": "PA_HwsqmPnPxvGD", - "trackSid": "TR_VS8RxgcaoR5kbz", - "width": 320, - "height": 180, - "timestampUs": 335539594000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VS8RxgcaoR5kbz", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 0, - "packetsLost": 0, - "fps": 16, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 14.6, - "framesProduced": 6, - "framesAccepted": 6, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 6, - "captureFailures": 0, - "maxQueueAgeMs": 8, - "maxPushLatencyMs": 8, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMBPL59S6qUeEf", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 0, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 6, - "outgoingVideoFramesAccepted": 6, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 6, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.62, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 8, - "outgoingVideoMaxPushLatencyMs": 8, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 23, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_HwsqmPnPxvGD", - "trackSid": "TR_AMBPL59S6qUeEf", - "kind": "audio", - "bitrateKbps": 0, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.315, - "codec": "audio/opus" - }, - { - "participantSid": "PA_HwsqmPnPxvGD", - "trackSid": "TR_VS8RxgcaoR5kbz", - "kind": "video", - "bitrateKbps": 0, - "packetsLost": 3, - "jitterMs": 4, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/summary.json b/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/summary.json deleted file mode 100644 index 3cfe7b4d1..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy/summary.json +++ /dev/null @@ -1,227 +0,0 @@ -{ - "status": "pass", - "mode": "smoke", - "strict": false, - "platform": "darwin", - "arch": "arm64", - "hostname": "Hampuss-Mac-Studio.local", - "startedAt": "2026-06-06T22:17:25.424Z", - "endedAt": "2026-06-06T22:17:42.978Z", - "reportDir": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-06T22-17-25Z-codex-host-smoke-proxy", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "mac-screen-capture-build", - "command": "pnpm --dir native/mac-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-screen-capture-tests", - "command": "pnpm --dir native/mac-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-build", - "command": "pnpm --dir native/mac-app-audio build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-rust-tests", - "command": "pnpm --dir native/mac-app-audio test:cargo", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-js-tests", - "command": "'/Users/hampus/Development/fluxer/node_modules/.pnpm/node_modules/.bin/vitest' run", - "category": "platform-native", - "cwd": "native/mac-app-audio" - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/WebrtcScreenPublish.test.mjs src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:25.425Z", - "endedAt": "2026-06-06T22:17:25.608Z", - "durationMs": 183, - "stdoutTail": "▶ NativeScreenCapture DMA-BUF metadata validation\n ✔ accepts valid DMA-BUF metadata and pads native plane arrays (2.167459ms)\n ✔ keeps valid handles when optional device UUID metadata is absent or malformed (0.439666ms)\n ✔ rejects invalid plane counts, FDs, strides, and format metadata (0.458916ms)\n✔ NativeScreenCapture DMA-BUF metadata validation (3.515292ms)\n▶ NativeScreenCapture source identity and capability reporting\n ✔ normalizes display and window sources without changing source ids (0.661875ms)\n ✔ starts display and window captures with exact source id and kind and reports diagnostics (1.226708ms)\n ✔ forwards frames to renderer even when a native frame tap is installed (0.477666ms)\n ✔ reports macOS and Linux display/window capture capabilities from platform backends (1.341208ms)\n✔ NativeScreenCapture source identity and capability reporting (3.829875ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ pushes the initial capture frame after publishing screen share (2.384041ms)\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU frames (0.56125ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.49475ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.461792ms)\n ✔ keeps screen-share audio published when screen video is unpublished (0.516583ms)\n ✔ replaces screen capture taps without releasing screen-share audio or duplicating priority (1.04175ms)\n ✔ forwards watch and unwatch operations with source, enablement, and quality intact (0.860792ms)\n ✔ reports VideoToolbox hardware capability from the native addon (0.46825ms)\n ✔ reports native voice engine capabilities from the native addon (0.402ms)\n ✔ maps native addon microphone not-connected errors to the typed operation code (0.542958ms)\n✔ NativeVoiceEngine screen frame routing (8.310334ms)\n▶ WebrtcScreenPublish native frame routing\n ✔ prefers Windows shared texture handles over DMA-BUF and CPU buffers (2.040083ms)\n ✔ uses Linux DMA-BUF handles before CPU conversion (0.431291ms)\n ✔ falls back to CPU frames when GPU paths are unavailable or rejected (0.405292ms)\n ✔ replaces an existing publisher before routing frames for the same capture id (0.398542ms)\n✔ WebrtcScreenPublish native frame routing (3.742875ms)\nℹ tests 21\nℹ suites 4\nℹ pass 21\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 120.183542\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:25.609Z", - "endedAt": "2026-06-06T22:17:26.719Z", - "durationMs": 1110, - "stdoutTail": "native/linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\nnative/windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> fluxer_desktop@0.0.0 typecheck /Users/hampus/Development/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:26.719Z", - "endedAt": "2026-06-06T22:17:28.890Z", - "durationMs": 2171, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/webrtc-sender@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml --features camera-native\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/webrtc-sender.darwin-arm64.node\n", - "stderrTail": "warning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\n Finished `release` profile [optimized] target(s) in 0.16s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/webrtc-sender.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:28.890Z", - "endedAt": "2026-06-06T22:17:31.695Z", - "durationMs": 2805, - "stdoutTail": "eported_available ... ok\ntest hardware_encoder::tests::publish_codec_runtime_gate_only_requires_hardware_for_h265 ... ok\ntest hardware_encoder::tests::reports_available_nvenc_when_compiled_and_runtime_ready ... ok\ntest hardware_encoder::tests::reports_available_videotoolbox_with_h264_and_h265 ... ok\ntest hardware_encoder::tests::reports_driver_sdk_incompatibility_before_encode_session ... ok\ntest hardware_encoder::tests::reports_h264_only_when_hevc_guid_is_absent ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_compiled ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_probe_is_absent ... ok\ntest hardware_encoder::tests::reports_no_cuda_devices_using_obs_style_reason ... ok\ntest hardware_encoder::tests::reports_no_supported_codecs_when_guid_query_finds_no_fluxer_codecs ... ok\ntest hardware_encoder::tests::reports_not_compiled_before_runtime_missing ... ok\ntest hardware_encoder::tests::reports_unsupported_platforms_clearly ... ok\ntest hardware_encoder::tests::reports_videotoolbox_h264_only_when_hevc_session_is_unavailable ... ok\ntest hardware_encoder::tests::reports_videotoolbox_unavailable_with_status_details ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_publish_backend_not_compiled ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_runtime_dll_names_when_missing ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_without_linux_zero_copy_inputs ... ok\ntest publisher::tests::i420_plane_copy_fills_libwebrtc_buffer ... ok\ntest publisher::tests::parse_codec_maps_known_names ... ok\ntest send_control::tests::audio_buffer_growth_caps_at_configured_max_and_reset_clears_pressure ... ok\ntest send_control::tests::audio_buffer_ignores_clock_regression_without_false_rebuffer ... ok\ntest send_control::tests::audio_buffer_expands_on_gaps_and_shrinks_after_stability ... ok\ntest send_control::tests::audio_buffer_max_is_clamped_to_real_time_bounds ... ok\ntest send_control::tests::video_controller_continues_degrading_to_minimum_under_sustained_pressure ... ok\ntest send_control::tests::idle_snapshot_reflects_audio_pressure_without_video_state ... ok\ntest send_control::tests::video_controller_counts_coalesced_replacements_once_for_pressure ... ok\ntest send_control::tests::audio_buffer_stress_stays_within_realtime_bounds_under_jitter ... ok\ntest send_control::tests::audio_buffer_ignores_concurrent_stale_pushes_without_moving_last_push_backwards ... ok\ntest send_control::tests::video_controller_degrades_on_encoder_egress_pressure ... ok\ntest send_control::tests::video_controller_degrades_on_latency_pressure_without_coalescing ... ok\ntest send_control::tests::video_controller_degrades_on_repeated_coalescing_pressure ... ok\ntest send_control::tests::video_controller_degrades_when_disrupted_frame_ratio_exceeds_threshold ... ok\ntest send_control::tests::video_controller_ignores_bounded_one_frame_coalescing_pressure ... ok\ntest send_control::tests::video_controller_ignores_single_encoder_egress_sample ... ok\ntest send_control::tests::video_controller_keeps_requested_fps_when_adaptive_send_is_disabled ... ok\ntest send_control::tests::video_controller_preserves_pressure_reason_while_degraded_but_stable ... ok\ntest send_control::tests::video_controller_recovers_after_stable_windows ... ok\ntest send_control::tests::video_controller_recovers_from_minimum_in_two_stable_steps ... ok\ntest send_control::tests::video_controller_requires_a_full_stable_recovery_window ... ok\ntest send_control::tests::video_controller_waits_until_pressure_window_boundary ... ok\ntest send_control::tests::video_stats_effective_fps_uses_capture_time_bounds_for_out_of_order_records ... ok\ntest send_control::tests::video_stats_snapshot_counts_coalescing_drops_failures_and_effective_fps ... ok\ntest stats::tests::bitrate_computes_kbps_from_byte_delta ... ok\ntest stats::tests::bitrate_first_sample_is_zero ... ok\ntest send_control::tests::video_controller_stress_keeps_target_inside_configured_bounds ... ok\ntest stats::tests::bitrate_half_second_doubles_rate ... ok\ntest stats::tests::bitrate_rejects_backwards_time_and_bytes ... ok\ntest stats::tests::empty_stats_serialise_to_null_rtt_and_empty_arrays ... ok\ntest stats::tests::sanitize_audio_level_clamps ... ok\ntest stats::tests::sanitize_kbps_drops_nan_inf_negative_and_rounds ... ok\ntest stats::tests::full_stats_serialise_to_exact_contract_shape ... ok\ntest stats::tests::send_health_serialises_to_exact_contract_shape ... ok\ntest stats::tests::unit_conversions_seconds_to_ms ... ok\ntest send_control::tests::video_stats_handles_concurrent_recording_without_lost_counts ... ok\ntest stats::tests::video_inbound_omits_audio_level_audio_omits_fps ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_populates_all_plane_arrays ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_incomplete_native_inputs ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_negative_fds ... ok\ntest texture_source::tests::drm_format_gate_accepts_obs_vkcapture_texture_formats ... ok\ntest texture_source::tests::dxgi_format_gate_accepts_8bit_rgba_bgra_only ... ok\ntest texture_source::tests::error_strings_are_stable ... ok\ntest texture_source::tests::probe_is_available_only_for_explicit_hardware_codecs ... ok\ntest texture_source::tests::should_attempt_dmabuf_is_sdk_gated_after_validation_capability ... ok\ntest texture_source::tests::should_attempt_falls_back_when_capability_unavailable ... ok\ntest texture_source::tests::should_attempt_validates_frame_when_capability_available ... ok\ntest texture_source::tests::validate_accepts_a_clean_bgra_texture ... ok\ntest texture_source::tests::validate_dmabuf_accepts_supported_formats_with_optional_uuid ... ok\ntest texture_source::tests::validate_dmabuf_rejects_invalid_planes ... ok\ntest texture_source::tests::validate_precedence_handle_before_dims_before_format ... ok\ntest texture_source::tests::validate_rejects_odd_zero_and_oversized_dims ... ok\ntest texture_source::tests::validate_rejects_unsupported_format ... ok\ntest texture_source::tests::validate_rejects_zero_handle_as_no_texture ... ok\ntest yuv::tests::bgra_black_white_grey_levels ... ok\ntest yuv::tests::bgra_golden_2x2_solid_colour_exact_bytes ... ok\ntest yuv::tests::bgra_golden_strided_2x2_skips_row_padding ... ok\ntest yuv::tests::bgra_plane_sizes ... ok\ntest yuv::tests::bgra_primaries_have_expected_chroma_signs ... ok\ntest yuv::tests::copy_nv12_planes_preserves_nv12_layout ... ok\ntest yuv::tests::copy_nv12_planes_rejects_short_buffers ... ok\ntest yuv::tests::copy_nv12_planes_respects_destination_stride ... ok\ntest yuv::tests::nv12_4x4_deinterleaves_two_chroma_columns ... ok\ntest yuv::tests::nv12_golden_4x2_packed_to_i420 ... ok\ntest yuv::tests::nv12_packed_2x2_deinterleaves ... ok\ntest yuv::tests::nv12_respects_row_padding ... ok\ntest yuv::tests::nv12_short_buffer_is_rejected ... ok\ntest yuv::tests::rejects_odd_or_tiny_dimensions ... ok\ntest yuv::tests::rgb_golden_2x2_solid_colour_matches_bgra_path ... ok\ntest yuv::tests::rgb_plane_sizes_and_levels ... ok\ntest yuv::tests::rgb_rejects_odd_dims_and_short_buffer ... ok\ntest engine::tests::pcm16_audio_frame_rejects_implausible_format_and_overlong_chunks ... ok\n\ntest result: ok. 140 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 6 tests\ntest video_frame_meta_matches_contract ... ok\ntest inbound_covers_subscribe_and_lifecycle ... ok\ntest outbound_covers_the_single_identity_publish_path ... ok\ntest outbound_fixtures_match_contract ... ok\ntest inbound_fixtures_match_contract ... ok\ntest e2ee_state_is_ok_on_the_wire ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ webrtc-sender loader wrapper\n ✔ resolves the native addon filename for every desktop OS/arch target (0.937291ms)\n ✔ rejects unsupported platform/architecture pairs explicitly (0.3075ms)\n ✔ returns a native hardware encoder capability when the binding exports one (0.9935ms)\n ✔ delegates dropped video callback metrics through the VoiceEngine wrapper (0.163167ms)\n ✔ reports VoiceEngine feature capabilities from the wrapped native prototype (0.5065ms)\n ✔ delegates device microphone publish through the VoiceEngine wrapper (0.174917ms)\n ✔ delegates native camera devices and publish options through the VoiceEngine wrapper (0.172875ms)\n ✔ delegates screen-share simulcast selection through the VoiceEngine wrapper (0.163833ms)\n ✔ delegates connect options through the VoiceEngine wrapper (0.211375ms)\n ✔ delegates shared texture handles through the WebrtcPublisher wrapper (0.22075ms)\n ✔ treats shared texture publishing as unavailable for older publisher bindings (0.095625ms)\n ✔ delegates dma-buf frames through the WebrtcPublisher wrapper (0.276959ms)\n ✔ delegates shared texture handles through the VoiceEngine wrapper (0.132ms)\n ✔ treats shared texture publishing as unavailable for older voice bindings (0.073083ms)\n ✔ treats dma-buf publishing as unavailable for older native bindings (0.110125ms)\n ✔ defaults dropped video callback metrics to zero for older native bindings (0.0585ms)\n ✔ delegates remote track subscription updates through the VoiceEngine wrapper (0.136583ms)\n ✔ ignores remote track subscription updates for older native bindings (0.088709ms)\n ✔ delegates data packets through the VoiceEngine wrapper (0.179834ms)\n ✔ rejects invalid data packet payloads in the wrapper (0.238083ms)\n ✔ returns an unavailable hardware encoder capability without a binding (0.072ms)\n✔ webrtc-sender loader wrapper (7.268042ms)\n▶ livekit harness setup matrix\n ✔ maps all supported codec spellings to expected mime types (1.146667ms)\n ✔ parses codec lists with fallback and rejects empty effective lists (1.1805ms)\n ✔ default non-strict config keeps the fast local live setup small (1.487ms)\n ✔ strict config expands to multi-codec secondary-publisher scenarios (1.038875ms)\n ✔ stress knobs parse packet loss, resolution stability, audio gap, and video pattern (0.635208ms)\n ✔ stress knobs reject impossible send pacing configurations (3.125583ms)\n ✔ explicit secondary codec expectations override rotating defaults (0.484584ms)\n ✔ secondary screen codec aliases are accepted and conflicting aliases are rejected (0.924208ms)\n ✔ external token setup derives identities, requires matching tokens, and redacts reports (1.383958ms)\n ✔ report sanitization strips URL credentials, query strings, and fragments (0.6515ms)\n ✔ jwt subject and TCP target parsing catch malformed live setups early (0.336458ms)\n ✔ admin token has the room-scoped grant needed for server publishing validation (0.468333ms)\n ✔ scenario room names are stable and bounded for generated-token suites (0.1065ms)\n ✔ strict report checks fail when the strict window has too few samples to measure gaps (89.199292ms)\n ✔ strict report checks use only samples inside the measured window (5.439292ms)\n ✔ strict packet loss gate uses loss deltas inside the strict window (10.6855ms)\n ✔ server publishing checks cover primary and secondary screen, audio, and camera tracks (0.5675ms)\n ✔ server publishing checks fail closed on missing server tracks (0.275125ms)\n ✔ server publishing checks reject wrong participant identity (0.2045ms)\n ✔ server publishing checks reject wrong kind, source, and screen mime (0.258042ms)\n ✔ server publishing checks reject muted or missing mute state on expected tracks (0.200416ms)\n ✔ server publishing checks handle protobuf numeric enums without accepting data tracks as video (0.194ms)\n ✔ server publishing checks fail closed on missing secondary, audio, camera, and screen-audio tracks (0.203708ms)\n ✔ strict server publishing validation does not pass open on Twirp errors (0.207541ms)\n✔ livekit harness setup matrix (121.754875ms)\nℹ tests 45\nℹ suites 2\nℹ pass 45\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 510.566208\n", - "stderrTail": "warning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 0.15s\n Running unittests src/lib.rs (target/debug/deps/fluxer_webrtc_sender-4fb413a724225319)\n Running tests/event_contract.rs (target/debug/deps/event_contract-dc15840aabc01f6c)\n Doc-tests fluxer_webrtc_sender\n" - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:31.695Z", - "endedAt": "2026-06-06T22:17:34.808Z", - "durationMs": 3113, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/webrtc-sender@0.0.0 test:livekit /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> node scripts/livekit-harness.mjs\n\n[livekit-harness] connecting to ws://127.0.0.1:8088/livekit room=fluxer-webrtc-harness-84848-43854e34-d3a9-4e33-bf45-b7ef3611083a\n[livekit-harness] PASS: 19 checks, inboundAudioFrames=72, inboundVideoFrames=13, videoCallbacks=13, receivedFps=0, producedFps=15.78, maxVideoFrameGapMs=0, maxAudioFrameGapMs=0, maxObservedPacketLoss=3, maxObservedPacketLossDelta=0, resolutionMismatches=0, droppedVideoFrameCallbacks=0\n", - "stderrTail": "" - }, - { - "name": "mac-screen-capture-build", - "command": "pnpm --dir native/mac-screen-capture build", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:34.808Z", - "endedAt": "2026-06-06T22:17:36.805Z", - "durationMs": 1997, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/mac-screen-capture@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture/mac-screen-capture.darwin-arm64.node\n", - "stderrTail": " Finished `release` profile [optimized] target(s) in 0.04s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture/mac-screen-capture.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "mac-screen-capture-tests", - "command": "pnpm --dir native/mac-screen-capture test", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:36.805Z", - "endedAt": "2026-06-06T22:17:39.173Z", - "durationMs": 2368, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/mac-screen-capture@0.0.0 test /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n> pnpm test:cargo && node --test index.test.mjs\n\n../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/mac-screen-capture@0.0.0 test:cargo /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n> cargo test --manifest-path Cargo.toml\n\n\nrunning 15 tests\ntest nv12_packing::tests::pack_nv12_rejects_short_source ... ok\ntest nv12_packing::tests::pack_nv12_rejects_undersized_stride ... ok\ntest nv12_packing::tests::pack_nv12_strips_row_padding ... ok\ntest nv12_packing::tests::pack_nv12_zero_padding_is_identity ... ok\ntest nv12_packing::tests::pack_nv12_rejects_wrong_dst_size ... ok\ntest nv12_packing::tests::packed_size_matches_yuv420_layout ... ok\ntest nv12_packing::tests::packed_size_rejects_zero_dims ... ok\ntest nv12_packing::tests::packed_size_rejects_odd_height ... ok\ntest os_version::tests::classify_macos_11_is_unsupported ... ok\ntest os_version::tests::classify_macos_12_3_is_supported ... ok\ntest os_version::tests::classify_unknown_version_is_unsupported ... ok\ntest os_version::tests::format_version_trims_zero_patch ... ok\ntest os_version::tests::meets_floor_exact_match ... ok\ntest os_version::tests::meets_floor_higher_major ... ok\ntest os_version::tests::rejects_below_floor ... ok\n\ntest result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ mac-screen-capture loader wrapper\n ✔ forwards source id, kind, and dimensions to native binding (2.259875ms)\n ✔ defaults sourceKind to screen and frameRate to 30 (0.213958ms)\n ✔ forwards display and window sources from native binding without rewriting ids (0.2195ms)\n ✔ reports ScreenCaptureKit support and permission from native binding (0.7435ms)\n ✔ re-emits native frame events to listeners (0.327166ms)\n ✔ emits closed once when native lifecycle closes and stop is called later (0.299333ms)\n ✔ lifecycle error emits Error event (0.176125ms)\n✔ mac-screen-capture loader wrapper (5.402834ms)\nℹ tests 7\nℹ suites 1\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 353.937417\n", - "stderrTail": " Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s\n Running unittests src/lib.rs (target/debug/deps/fluxer_mac_screen_capture-41d64e8eafcd58d2)\n Doc-tests fluxer_mac_screen_capture\n" - }, - { - "name": "mac-app-audio-build", - "command": "pnpm --dir native/mac-app-audio build", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:39.173Z", - "endedAt": "2026-06-06T22:17:41.191Z", - "durationMs": 2018, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/mac-app-audio@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio/mac-app-audio.darwin-arm64.node\n", - "stderrTail": " Finished `release` profile [optimized] target(s) in 0.04s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio/mac-app-audio.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "mac-app-audio-rust-tests", - "command": "pnpm --dir native/mac-app-audio test:cargo", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:41.191Z", - "endedAt": "2026-06-06T22:17:42.306Z", - "durationMs": 1115, - "stdoutTail": "../linux-audio-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-evdev |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-notifications |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-portals |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../linux-screen-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../system-hunspell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"linux\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-clipboard |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-game-capture |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-process-loopback |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-shell |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../win-toast |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n../windows-input-hook |  WARN  Unsupported platform: wanted: {\"cpu\":[\"x64\",\"arm64\"],\"os\":[\"win32\"],\"libc\":[\"any\"]} (current: {\"os\":\"darwin\",\"cpu\":\"arm64\",\"libc\":\"unknown\"})\n\n> @fluxer/mac-app-audio@0.0.0 test:cargo /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n> cargo test --manifest-path Cargo.toml\n\n\nrunning 49 tests\ntest audio_converter::tests::build_input_non_interleaved ... ok\ntest audio_converter::tests::build_input_interleaved ... ok\ntest audio_converter::tests::build_output_mono_44_1k ... ok\ntest audio_converter::tests::build_output_stereo_48k ... ok\ntest audio_converter::tests::converted_frame_count_exact ... ok\ntest audio_converter::tests::lpcm_fourcc ... ok\ntest audio_converter::tests::input_frame_count_interleaved_and_planar ... ok\ntest audio_converter::tests::normalize_signed_int16 ... ok\ntest audio_converter::tests::interleave_planar_stereo ... ok\ntest audio_converter::tests::duplicate_mono_to_stereo ... ok\ntest audio_converter::tests::linear_resample_to_target_rate ... ok\ntest audio_converter::tests::handle_float64_input ... ok\ntest audio_converter::tests::output_frame_capacity_rounds_up ... ok\ntest foundation::tests::mutable_array_initial_count_is_zero ... ok\ntest foundation::tests::mutable_dictionary_initial_count_is_zero ... ok\ntest foundation::tests::nsstring_to_string_handles_none ... ok\ntest foundation::tests::nsstring_round_trip ... ok\ntest os_version::tests::classify_macos_10_15_is_unsupported_and_mentions_min_version ... ok\ntest os_version::tests::classify_macos_12_3_is_sck_only ... ok\ntest os_version::tests::classify_macos_14_2_has_both_backends ... ok\ntest os_version::tests::classify_macos_15_is_supported ... ok\ntest os_version::tests::classify_unknown_version_is_unsupported ... ok\ntest os_version::tests::coreaudio_floor_14_2 ... ok\ntest os_version::tests::format_version_trims_zero_patch ... ok\ntest os_version::tests::meets_floor_exact_match ... ok\ntest os_version::tests::meets_floor_higher_major ... ok\ntest os_version::tests::meets_floor_higher_minor ... ok\ntest os_version::tests::rejects_below_floor_major ... ok\ntest os_version::tests::rejects_below_floor_minor ... ok\ntest os_version::tests::rejects_macos_10_15_for_sck ... ok\ntest process_tree::tests::collect_related_returns_tree ... ok\ntest process_tree::tests::same_launch_tree_excludes_unrelated ... ok\ntest process_tree::tests::same_launch_tree_includes_direct_child ... ok\ntest process_tree::tests::same_launch_tree_includes_pgrp_peer ... ok\ntest related_app::tests::helper_bundle_strips_suffix ... ok\ntest related_app::tests::helper_name_strips_helper_suffix ... ok\ntest related_app::tests::related_by_dot_prefix ... ok\ntest related_app::tests::related_via_bundle_helper_base ... ok\ntest related_app::tests::related_via_name_helper_base ... ok\ntest related_app::tests::unrelated_returns_false ... ok\ntest sck::tests::cgrect_intersection_handles_negative_and_disjoint ... ok\ntest source_state::tests::cancel_idle_short_circuits ... ok\ntest source_state::tests::double_start_rejected ... ok\ntest source_state::tests::happy_path ... ok\ntest source_state::tests::is_allowed_exhaustive ... ok\ntest source_state::tests::mark_fatal_forces_stopped ... ok\ntest source_state::tests::mark_fatal_idempotent ... ok\ntest source_state::tests::start_while_stopping_rejected ... ok\ntest source_state::tests::stop_before_start_rejected ... ok\n\ntest result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n", - "stderrTail": " Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s\n Running unittests src/lib.rs (target/debug/deps/fluxer_mac_app_audio-b86bec7aea37ec64)\n Doc-tests fluxer_mac_app_audio\n" - }, - { - "name": "mac-app-audio-js-tests", - "command": "'/Users/hampus/Development/fluxer/node_modules/.pnpm/node_modules/.bin/vitest' run", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-06T22:17:42.306Z", - "endedAt": "2026-06-06T22:17:42.978Z", - "durationMs": 672, - "stdoutTail": "\n RUN v4.0.18 /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n\n ✓ index.test.mjs (5 tests) 4ms\n\n Test Files 1 passed (1)\n Tests 5 passed (5)\n Start at 00:17:42\n Duration 399ms (transform 13ms, setup 0ms, import 285ms, tests 4ms, environment 0ms)\n\n", - "stderrTail": "" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/2026-06-07T14-52-39-687Z/summary.json b/fluxer_desktop/native-media-reports/2026-06-07T14-52-39-687Z/summary.json deleted file mode 100644 index be3ef79c3..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-07T14-52-39-687Z/summary.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "status": "fail", - "mode": "smoke", - "strict": false, - "platform": "linux", - "arch": "arm64", - "hostname": "d4e5fefa7fc7", - "startedAt": "2026-06-07T14:52:39.689Z", - "endedAt": "2026-06-07T14:53:04.641Z", - "reportDir": "/workspaces/fluxer/fluxer_desktop/native-media-reports/2026-06-07T14-52-39-687Z", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "linux-screen-capture-build", - "command": "pnpm --dir native/linux-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-screen-capture-tests", - "command": "pnpm --dir native/linux-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-audio-capture-build", - "command": "pnpm --dir native/linux-audio-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-audio-capture-tests", - "command": "pnpm --dir native/linux-audio-capture test", - "category": "platform-native", - "cwd": "." - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-07T14:52:39.690Z", - "endedAt": "2026-06-07T14:52:39.792Z", - "durationMs": 102, - "stdoutTail": "▶ NativeScreenCapture source identity and capability reporting\n ✔ normalizes display and window sources without changing source ids (1.614801ms)\n ✔ starts display and window captures with exact source id and kind and reports diagnostics (1.041547ms)\n ✔ fails fast when native frame sink is required but unavailable (0.500503ms)\n ✔ passes caller-provided capture id and native sink handle to the platform wrapper (0.467711ms)\n ✔ reports macOS and Linux display/window capture capabilities from platform backends (0.625753ms)\n✔ NativeScreenCapture source identity and capability reporting (4.771278ms)\n▶ NativeVoiceEngine connection lifecycle\n ✔ does not forward native events after disconnecting a pending connect (3.117018ms)\n ✔ disconnects stale native connect completion after a replacement connect starts (1.775302ms)\n✔ NativeVoiceEngine connection lifecycle (5.440365ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ publishes screen share with source pacing and exposes a native frame sink handle (0.897838ms)\n ✔ keeps screen-share audio published when screen video is unpublished (0.634254ms)\n ✔ replaces screen captures without releasing screen-share audio or duplicating priority (0.502586ms)\n ✔ forwards watch and unwatch operations with source, enablement, and quality intact (0.563378ms)\n ✔ reports VideoToolbox hardware capability from the native addon (0.49767ms)\n ✔ reports native voice engine capabilities from the native addon (0.56267ms)\n ✔ maps native addon microphone not-connected errors to the typed operation code (0.639129ms)\n✔ NativeVoiceEngine screen frame routing (4.546943ms)\nℹ tests 14\nℹ suites 3\nℹ pass 14\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 81.841144\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-07T14:52:39.792Z", - "endedAt": "2026-06-07T14:52:40.133Z", - "durationMs": 341, - "stdoutTail": "\n> fluxer_desktop@0.0.0 typecheck /workspaces/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-07T14:52:40.133Z", - "endedAt": "2026-06-07T14:52:57.557Z", - "durationMs": 17424, - "stdoutTail": "\n> @fluxer/webrtc-sender@0.0.0 build /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n+ /home/vscode/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/cargo build --release --target aarch64-unknown-linux-gnu --manifest-path Cargo.toml --features camera-native\n", - "stderrTail": " Compiling crossbeam-utils v0.8.21\n Compiling fluxer_screen_frame_bus v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/screen-frame-bus)\n Compiling fluxer_voice_core v0.1.0 (/workspaces/fluxer/fluxer_voice_core)\nwarning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: variable does not need to be mutable\n --> vendor/libwebrtc/src/native/desktop_capturer.rs:66:13\n |\n66 | let mut sys_handle = sys_dc::ffi::DesktopCapturerOptions {\n | ----^^^^^^^^^^\n | |\n | help: remove this `mut`\n |\n = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 4 warnings (run `cargo fix --lib -p libwebrtc` to apply 3 suggestions)\n Compiling crossbeam-queue v0.3.12\n Compiling fluxer_webrtc_sender v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender)\n Finished `release` profile [optimized] target(s) in 10.39s\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "fail", - "code": 1, - "signal": null, - "startedAt": "2026-06-07T14:52:57.557Z", - "endedAt": "2026-06-07T14:53:04.641Z", - "durationMs": 7084, - "stdoutTail": "\n> @fluxer/webrtc-sender@0.0.0 test /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- test-webrtc-sender-rust && node --test index.test.mjs scripts/livekit-harness.test.mjs\n\n+ /home/vscode/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/cargo test --features \"publisher,camera-native\"\n ELIFECYCLE  Test failed. See above for more details.\n WARN  Local package.json exists, but node_modules missing, did you mean to install?\n", - "stderrTail": " Compiling zerocopy-derive v0.8.50\n Compiling serde v1.0.228\n Compiling ring v0.17.14\n Compiling serde_urlencoded v0.7.1\n Compiling fluxer_screen_frame_bus v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/screen-frame-bus)\n Compiling rustls v0.23.40\n Compiling pbjson v0.6.0\n Compiling os_info v3.15.0\n Compiling fluxer_voice_core v0.1.0 (/workspaces/fluxer/fluxer_voice_core)\n Compiling tinytemplate v1.2.1\n Compiling pbjson-types v0.6.0\n Compiling zerocopy v0.8.50\n Compiling rustls-webpki v0.103.13\n Compiling livekit-protocol v0.7.8\n Compiling ppv-lite86 v0.2.21\n Compiling half v2.7.1\n Compiling rand_chacha v0.9.0\n Compiling rand_chacha v0.3.1\n Compiling ciborium-ll v0.2.2\n Compiling tokio-rustls v0.26.4\n Compiling rand v0.8.6\n Compiling rand v0.9.4\nerror[E0786]: found invalid metadata files for crate `rustls`\n --> /home/vscode/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/src/lib.rs:49:9\n |\n49 | pub use rustls;\n | ^^^^^^\n |\n = note: failed to open rmeta metadata: '/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/target/debug/deps/librustls-f66081aae860cdad.rmeta'\n\nFor more information about this error, try `rustc --explain E0786`.\nerror: could not compile `tokio-rustls` (lib) due to 1 previous error\nwarning: build failed, waiting for other jobs to finish...\nCommand failed with exit code 101: /home/vscode/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/cargo test --features \"publisher,camera-native\"\n" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/2026-06-09T-codex-macos-livekit-strict5-rerun.json b/fluxer_desktop/native-media-reports/2026-06-09T-codex-macos-livekit-strict5-rerun.json deleted file mode 100644 index c9fac5503..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-09T-codex-macos-livekit-strict5-rerun.json +++ /dev/null @@ -1,765 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-09T04:40:52.879Z", - "endedAt": "2026-06-09T04:41:02.390Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://localhost:7880", - "serverApiUrl": "http://localhost:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-76954-91cb528a-6d21-4940-ad58-aa5c5e746211", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-76954", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-76954", - "subscriberIdentity": "fluxer-native-subscriber-76954", - "required": true, - "strict": true, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 5000, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": true, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": true, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-09T-codex-macos-livekit-strict5-rerun.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": true - }, - { - "name": "subscriber video resolution 320x180", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "publisher local camera publication", - "pass": true - }, - { - "name": "subscriber remote camera subscription", - "pass": true - }, - { - "name": "publisher outbound camera stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees publisher camera publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 10.01 below 14.25", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 3416, - "inboundVideoFrames": 319, - "strictAudioFrames": 50, - "strictVideoFrames": 50, - "videoCallbacks": 319, - "videoBytes": 55728000, - "receivedFps": 10.01, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 104, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 83, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 36, - "maxObservedPacketLoss": 76, - "maxObservedPacketLossDelta": 43, - "videoResolutionCounts": { - "320x180": 50 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780980053827, - "stoppedAtMs": null, - "framesPushed": 0, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 0, - "lastPushAtMs": null, - "elapsedMs": 8570, - "producedFps": 0 - }, - "secondaryPublisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780980053827, - "stoppedAtMs": null, - "framesPushed": 0, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 0, - "lastPushAtMs": null, - "elapsedMs": 8570, - "producedFps": 0 - }, - "serverPublishing": { - "fetchedAtMs": 1780980057383, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-2-76954", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSwHXmU7ctxa87", - "name": "screen", - "kind": "video", - "source": "screen_share", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMnWff3NPczSoR", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsTFRWHNvJ7MgE", - "name": "screen-audio", - "kind": "audio", - "source": "screen_share_audio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-publisher-76954", - "trackCount": 4, - "tracks": [ - { - "sid": "TR_VSp9aehQCmDiq4", - "name": "screen", - "kind": "video", - "source": "screen_share", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AM8ZuGaDACq9FB", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsD2wGcUaXveFj", - "name": "screen-audio", - "kind": "audio", - "source": "screen_share_audio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VC7GWwmaJQZJio", - "name": "camera", - "kind": "video", - "source": "camera", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-76954", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_VC7GWwmaJQZJio", - "trackName": "camera", - "source": "camera", - "width": 480, - "height": 360, - "timestampUs": 526246627000, - "identity": "fluxer-native-publisher-76954", - "kind": "video" - }, - "lastStrictVideoMeta": { - "atMs": 1780980062302, - "identity": "fluxer-native-publisher-76954", - "kind": "video", - "source": "screen_share", - "trackName": "screen", - "trackSid": "TR_VSp9aehQCmDiq4", - "width": 320, - "height": 180 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VC7GWwmaJQZJio", - "source": "camera", - "kind": "video", - "bitrateKbps": 158.8, - "packetsLost": 0, - "fps": 25, - "codec": "video/VP8" - }, - { - "trackSid": "TR_VC7GWwmaJQZJio", - "source": "camera", - "kind": "video", - "bitrateKbps": 112.8, - "packetsLost": 0, - "fps": 20, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AsD2wGcUaXveFj", - "source": "screen_share_audio", - "kind": "audio", - "bitrateKbps": 260.8, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AM8ZuGaDACq9FB", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSp9aehQCmDiq4", - "source": "screen_share", - "kind": "video", - "bitrateKbps": 7.2, - "packetsLost": 0, - "fps": 9, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 0, - "framesProduced": 0, - "framesAccepted": 0, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 0, - "captureFailures": 0, - "maxQueueAgeMs": 0, - "maxPushLatencyMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - } - ], - "inbound": [ - { - "participantSid": "PA_RnrcQgHxNGSb", - "trackSid": "TR_AMnWff3NPczSoR", - "kind": "audio", - "bitrateKbps": 94.8, - "packetsLost": 32, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_RnrcQgHxNGSb", - "trackSid": "TR_AsTFRWHNvJ7MgE", - "kind": "audio", - "bitrateKbps": 236, - "packetsLost": 28, - "jitterMs": 0, - "audioLevel": 0.336, - "codec": "audio/opus" - }, - { - "participantSid": "PA_RnrcQgHxNGSb", - "trackSid": "TR_VSwHXmU7ctxa87", - "kind": "video", - "bitrateKbps": 7.1, - "packetsLost": 5, - "jitterMs": 1, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 8, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 15, - "outgoingVideoPacingTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "source", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 55, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AMnWff3NPczSoR", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99.2, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSwHXmU7ctxa87", - "source": "screen_share", - "kind": "video", - "bitrateKbps": 10.6, - "packetsLost": 0, - "fps": 9, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 0, - "framesProduced": 0, - "framesAccepted": 0, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 0, - "captureFailures": 0, - "maxQueueAgeMs": 0, - "maxPushLatencyMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - }, - { - "trackSid": "TR_AsTFRWHNvJ7MgE", - "source": "screen_share_audio", - "kind": "audio", - "bitrateKbps": 261.1, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_AM8ZuGaDACq9FB", - "kind": "audio", - "bitrateKbps": 88.9, - "packetsLost": 25, - "jitterMs": 0, - "audioLevel": 0.331, - "codec": "audio/opus" - }, - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_AsD2wGcUaXveFj", - "kind": "audio", - "bitrateKbps": 250.9, - "packetsLost": 30, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_VC7GWwmaJQZJio", - "kind": "video", - "bitrateKbps": 113.7, - "packetsLost": 7, - "jitterMs": 0, - "codec": "video/VP8" - }, - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_VSp9aehQCmDiq4", - "kind": "video", - "bitrateKbps": 9.3, - "packetsLost": 14, - "jitterMs": 1, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 8, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 15, - "outgoingVideoPacingTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "source", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 56, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_AsD2wGcUaXveFj", - "kind": "audio", - "bitrateKbps": 260.5, - "packetsLost": 5, - "jitterMs": 0, - "audioLevel": 0.318, - "codec": "audio/opus" - }, - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_VC7GWwmaJQZJio", - "kind": "video", - "bitrateKbps": 112.7, - "packetsLost": 1, - "jitterMs": 0, - "codec": "video/VP8" - }, - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_AM8ZuGaDACq9FB", - "kind": "audio", - "bitrateKbps": 98.7, - "packetsLost": 1, - "jitterMs": 0, - "audioLevel": 0.313, - "codec": "audio/opus" - }, - { - "participantSid": "PA_szoL7dTduPBR", - "trackSid": "TR_VSp9aehQCmDiq4", - "kind": "video", - "bitrateKbps": 7.2, - "packetsLost": 10, - "jitterMs": 0, - "codec": "video/VP8" - }, - { - "participantSid": "PA_RnrcQgHxNGSb", - "trackSid": "TR_VSwHXmU7ctxa87", - "kind": "video", - "bitrateKbps": 7.7, - "packetsLost": 3, - "jitterMs": 1, - "codec": "video/VP8" - }, - { - "participantSid": "PA_RnrcQgHxNGSb", - "trackSid": "TR_AMnWff3NPczSoR", - "kind": "audio", - "bitrateKbps": 98.7, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.323, - "codec": "audio/opus" - }, - { - "participantSid": "PA_RnrcQgHxNGSb", - "trackSid": "TR_AsTFRWHNvJ7MgE", - "kind": "audio", - "bitrateKbps": 267.5, - "packetsLost": 1, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 0, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoPacingTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "idle", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5-localhost.json b/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5-localhost.json deleted file mode 100644 index c2c941526..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5-localhost.json +++ /dev/null @@ -1,752 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-09T04:11:22.147Z", - "endedAt": "2026-06-09T04:11:46.733Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://localhost:7880", - "serverApiUrl": "http://localhost:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-39834-83ba9452-cd15-4567-a196-47587b5a4a47", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-39834", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-39834", - "subscriberIdentity": "fluxer-native-subscriber-39834", - "required": true, - "strict": true, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 5000, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5-localhost.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": true - }, - { - "name": "subscriber video resolution 320x180", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "publisher local camera publication", - "pass": true - }, - { - "name": "subscriber remote camera subscription", - "pass": true - }, - { - "name": "publisher outbound camera stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees publisher camera publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "A/V drift 97ms exceeds 80ms; observed 101 video frames outside 320x180", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 2952, - "inboundVideoFrames": 268, - "strictAudioFrames": 49, - "strictVideoFrames": 201, - "videoCallbacks": 268, - "videoBytes": 46684800, - "receivedFps": 40.18, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 71, - "maxAudioFrameGapMs": 101, - "maxVideoFrameGapOverallMs": 242, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 97, - "maxObservedPacketLoss": 49, - "maxObservedPacketLossDelta": 30, - "videoResolutionCounts": { - "480x360": 101, - "320x180": 100 - }, - "videoResolutionMismatchCount": 101, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780978299271, - "stoppedAtMs": null, - "framesPushed": 0, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 0, - "lastPushAtMs": null, - "elapsedMs": 7471, - "producedFps": 0 - }, - "secondaryPublisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780978299271, - "stoppedAtMs": null, - "framesPushed": 0, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 0, - "lastPushAtMs": null, - "elapsedMs": 7471, - "producedFps": 0 - }, - "serverPublishing": { - "fetchedAtMs": 1780978301481, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-39834", - "trackCount": 4, - "tracks": [ - { - "sid": "TR_VSgsehdpyfYRtH", - "name": "screen", - "kind": "video", - "source": "screen_share", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMRH3WDE9scsXz", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_As4hoJPNkjh8Rv", - "name": "screen-audio", - "kind": "audio", - "source": "screen_share_audio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VC9jnXZixHviLZ", - "name": "camera", - "kind": "video", - "source": "camera", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-publisher-2-39834", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSQsd4UAwpwKy7", - "name": "screen", - "kind": "video", - "source": "screen_share", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AsHmVYBSWSQdfU", - "name": "screen-audio", - "kind": "audio", - "source": "screen_share_audio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AMq8at99MRi83a", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-39834", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_VSgsehdpyfYRtH", - "width": 320, - "height": 180, - "timestampUs": 524491017000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSgsehdpyfYRtH", - "source": "screen_share", - "kind": "video", - "bitrateKbps": 6.9, - "packetsLost": 0, - "fps": 10, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 0, - "framesProduced": 0, - "framesAccepted": 0, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 0, - "captureFailures": 0, - "maxQueueAgeMs": 0, - "maxPushLatencyMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - }, - { - "trackSid": "TR_As4hoJPNkjh8Rv", - "source": "screen_share_audio", - "kind": "audio", - "bitrateKbps": 261.2, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMRH3WDE9scsXz", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.9, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VC9jnXZixHviLZ", - "source": "camera", - "kind": "video", - "bitrateKbps": 114.9, - "packetsLost": 0, - "fps": 25, - "codec": "video/VP8" - }, - { - "trackSid": "TR_VC9jnXZixHviLZ", - "source": "camera", - "kind": "video", - "bitrateKbps": 92, - "packetsLost": 0, - "fps": 20, - "codec": "video/VP8" - } - ], - "inbound": [ - { - "participantSid": "PA_ychdbeoTL2ht", - "trackSid": "TR_AMq8at99MRi83a", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 1, - "jitterMs": 0, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ychdbeoTL2ht", - "trackSid": "TR_AsHmVYBSWSQdfU", - "kind": "audio", - "bitrateKbps": 261.4, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.344, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ychdbeoTL2ht", - "trackSid": "TR_VSQsd4UAwpwKy7", - "kind": "video", - "bitrateKbps": 13.1, - "packetsLost": 3, - "jitterMs": 6, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 8, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 15, - "outgoingVideoPacingTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "source", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 29, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_VSQsd4UAwpwKy7", - "source": "screen_share", - "kind": "video", - "bitrateKbps": 15.3, - "packetsLost": 0, - "fps": 10, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 0, - "framesProduced": 0, - "framesAccepted": 0, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 0, - "captureFailures": 0, - "maxQueueAgeMs": 0, - "maxPushLatencyMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMq8at99MRi83a", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AsHmVYBSWSQdfU", - "source": "screen_share_audio", - "kind": "audio", - "bitrateKbps": 261.2, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_As4hoJPNkjh8Rv", - "kind": "audio", - "bitrateKbps": 254.9, - "packetsLost": 0, - "jitterMs": 0, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_AMRH3WDE9scsXz", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_VSgsehdpyfYRtH", - "kind": "video", - "bitrateKbps": 7.3, - "packetsLost": 0, - "jitterMs": 2, - "codec": "video/VP8" - }, - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_VC9jnXZixHviLZ", - "kind": "video", - "bitrateKbps": 96.7, - "packetsLost": 0, - "jitterMs": 0, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 8, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 15, - "outgoingVideoPacingTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "source", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 29, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_VSgsehdpyfYRtH", - "kind": "video", - "bitrateKbps": 7.3, - "packetsLost": 1, - "jitterMs": 2, - "codec": "video/VP8" - }, - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_AMRH3WDE9scsXz", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_VC9jnXZixHviLZ", - "kind": "video", - "bitrateKbps": 96.6, - "packetsLost": 1, - "jitterMs": 0, - "codec": "video/VP8" - }, - { - "participantSid": "PA_d6p3en8AQ9YP", - "trackSid": "TR_As4hoJPNkjh8Rv", - "kind": "audio", - "bitrateKbps": 259.7, - "packetsLost": 1, - "jitterMs": 0, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ychdbeoTL2ht", - "trackSid": "TR_AsHmVYBSWSQdfU", - "kind": "audio", - "bitrateKbps": 242.6, - "packetsLost": 22, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ychdbeoTL2ht", - "trackSid": "TR_VSQsd4UAwpwKy7", - "kind": "video", - "bitrateKbps": 15.3, - "packetsLost": 2, - "jitterMs": 7, - "codec": "video/VP8" - }, - { - "participantSid": "PA_ychdbeoTL2ht", - "trackSid": "TR_AMq8at99MRi83a", - "kind": "audio", - "bitrateKbps": 96.8, - "packetsLost": 19, - "jitterMs": 0, - "audioLevel": 0.322, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 0, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoPacingTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "idle", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5.json b/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5.json deleted file mode 100644 index 2ac6eb370..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5.json +++ /dev/null @@ -1,309 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-09T04:10:37.354Z", - "endedAt": "2026-06-09T04:11:07.359Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "serverApiUrl": "http://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-39654-62723187-aa62-4359-9c19-cbc0403532e3", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-39654", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-39654", - "subscriberIdentity": "fluxer-native-subscriber-39654", - "required": true, - "strict": true, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 5000, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-livekit-strict5.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": false - }, - { - "name": "subscriber video resolution 320x180", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher local camera publication", - "pass": false - }, - { - "name": "subscriber remote camera subscription", - "pass": false - }, - { - "name": "publisher outbound camera stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": false - }, - { - "name": "server publishing API participants listed", - "pass": false - }, - { - "name": "server sees publisher screenshare publication", - "pass": false - }, - { - "name": "server sees publisher microphone publication", - "pass": false - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": false - }, - { - "name": "server sees publisher camera publication", - "pass": false - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": false - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": false - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1252:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1679:3)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1931:18)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1964:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/livekit-harness.json b/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/livekit-harness.json deleted file mode 100644 index 158b6cad8..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/livekit-harness.json +++ /dev/null @@ -1,331 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-09T04:09:48.418Z", - "endedAt": "2026-06-09T04:09:50.279Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://localhost:7880", - "serverApiUrl": "http://localhost:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-39291-5ee61a43-3009-4112-bca3-ae4b4667a371", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-39291", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-39291", - "subscriberIdentity": "fluxer-native-subscriber-39291", - "required": false, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/livekit-harness.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 320x180", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 3", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 1", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 3", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 72, - "inboundVideoFrames": 5, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 5, - "videoBytes": 432000, - "receivedFps": 0, - "requiredFps": null, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 132, - "maxAudioFrameGapOverallMs": 101, - "avDriftMs": 2, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780978189023, - "stoppedAtMs": null, - "framesPushed": 0, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 0, - "lastPushAtMs": null, - "elapsedMs": 1264, - "producedFps": 0 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": { - "participantSid": "PA_tmqNx963FdVV", - "trackSid": "TR_VS7wQpyDuhX3NX", - "width": 320, - "height": 180, - "timestampUs": 524374518000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VS7wQpyDuhX3NX", - "source": "screen_share", - "kind": "video", - "bitrateKbps": 0, - "packetsLost": 0, - "fps": 10, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 0, - "framesProduced": 0, - "framesAccepted": 0, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 0, - "captureFailures": 0, - "maxQueueAgeMs": 0, - "maxPushLatencyMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMq6BwJ6fu5yXx", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 0, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 8, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 15, - "outgoingVideoPacingTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "source", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 23, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 2, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_tmqNx963FdVV", - "trackSid": "TR_AMq6BwJ6fu5yXx", - "kind": "audio", - "bitrateKbps": 0, - "packetsLost": 0, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_tmqNx963FdVV", - "trackSid": "TR_VS7wQpyDuhX3NX", - "kind": "video", - "bitrateKbps": 0, - "packetsLost": 0, - "jitterMs": 0, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 0, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoPacingTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "idle", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/summary.json b/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/summary.json deleted file mode 100644 index 75b598a80..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke/summary.json +++ /dev/null @@ -1,227 +0,0 @@ -{ - "status": "pass", - "mode": "smoke", - "strict": false, - "platform": "darwin", - "arch": "arm64", - "hostname": "Hampuss-Mac-Studio.local", - "startedAt": "2026-06-09T04:08:25.060Z", - "endedAt": "2026-06-09T04:10:06.715Z", - "reportDir": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/2026-06-09T0407-codex-macos-smoke", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "mac-screen-capture-build", - "command": "pnpm --dir native/mac-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-screen-capture-tests", - "command": "pnpm --dir native/mac-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-build", - "command": "pnpm --dir native/mac-app-audio build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-rust-tests", - "command": "pnpm --dir native/mac-app-audio test:cargo", - "category": "platform-native", - "cwd": "." - }, - { - "name": "mac-app-audio-js-tests", - "command": "'/Users/hampus/Development/fluxer/node_modules/.pnpm/node_modules/.bin/vitest' run", - "category": "platform-native", - "cwd": "native/mac-app-audio" - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:08:25.062Z", - "endedAt": "2026-06-09T04:08:25.338Z", - "durationMs": 276, - "stdoutTail": "▶ NativeScreenCapture source identity and capability reporting\n ✔ normalizes display and window sources without changing source ids (2.653541ms)\n ✔ starts display and window captures with exact source id and kind and reports diagnostics (1.257458ms)\n ✔ fails fast when native frame sink is required but unavailable (0.6175ms)\n ✔ passes caller-provided capture id and native sink handle to the platform wrapper (0.494583ms)\n ✔ reports macOS and Linux display/window capture capabilities from platform backends (0.748042ms)\n✔ NativeScreenCapture source identity and capability reporting (6.317833ms)\n▶ NativeVoiceEngine v2 bridge contract fixtures\n ✔ registers and removes every v2 IPC channel from the bridge contract fixture (2.216209ms)\n✔ NativeVoiceEngine v2 bridge contract fixtures (2.659666ms)\n▶ NativeVoiceEngine v2 IPC request fixtures\n ✔ replays fixture: valid connect reaches the native engine (0.942959ms)\n ✔ replays fixture: invalid connect args throw (0.602334ms)\n ✔ replays fixture: invalid mic enabled args return typed operation failure (0.997125ms)\n ✔ replays fixture: valid microphone publish returns typed success (0.571541ms)\n ✔ replays fixture: invalid microphone publish returns typed operation failure (0.397ms)\n ✔ replays fixture: valid zero-copy screen publish reaches native engine (0.593ms)\n ✔ replays fixture: explicit non-zero-copy screen publish is rejected (0.448708ms)\n ✔ replays fixture: hardware screen encoding without zero-copy is rejected (0.5ms)\n ✔ replays fixture: valid native screen audio publish reaches native engine (0.410292ms)\n ✔ replays fixture: invalid screen audio route throws (0.41325ms)\n ✔ replays fixture: valid camera publish reaches native engine (0.3655ms)\n ✔ replays fixture: invalid camera publish throws (0.345917ms)\n ✔ replays fixture: connected stats return normalized empty stats (0.378125ms)\n ✔ replays fixture: valid participant volume reaches native engine (0.356667ms)\n ✔ replays fixture: invalid participant volume throws (0.33175ms)\n ✔ replays fixture: valid remote subscription reaches native engine (0.372709ms)\n ✔ replays fixture: invalid remote subscription quality throws (0.322209ms)\n ✔ replays fixture: valid data publish reaches native engine (0.440584ms)\n ✔ replays fixture: invalid data payload throws (0.37625ms)\n✔ NativeVoiceEngine v2 IPC request fixtures (9.494209ms)\n▶ NativeVoiceEngine connection lifecycle\n ✔ prewarms the native voice backend once through IPC (0.402042ms)\n ✔ reuses the prewarmed native voice engine instance for connect (0.345084ms)\n ✔ prepares a fresh idle native voice engine after disconnect (0.408292ms)\n ✔ does not forward native events after disconnecting a pending connect (6.291459ms)\n ✔ disconnects stale native connect completion after a replacement connect starts (1.882667ms)\n✔ NativeVoiceEngine connection lifecycle (9.5095ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ publishes screen share with source pacing and exposes a native frame sink handle (0.514834ms)\n ✔ forwards device screen share publish options to the native engine (0.381375ms)\n ✔ keeps screen-share audio published when screen video is unpublished (0.475375ms)\n ✔ replaces screen captures without releasing screen-share audio or duplicating priority (0.35675ms)\n ✔ updates active screen-share encoding over IPC (0.355959ms)\n ✔ republishes active screen-share when update changes codec or hardware mode (0.2975ms)\n ✔ forwards watch and unwatch operations with source, enablement, and quality intact (0.32775ms)\n ✔ reports VideoToolbox hardware capability from the native addon (0.26625ms)\n ✔ reports native voice engine capabilities from the native addon (0.29925ms)\n ✔ maps native addon microphone not-connected errors to the typed operation code (0.355417ms)\n✔ NativeVoiceEngine screen frame routing (3.728334ms)\nℹ tests 40\nℹ suites 5\nℹ pass 40\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 207.168208\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:08:25.339Z", - "endedAt": "2026-06-09T04:08:26.138Z", - "durationMs": 799, - "stdoutTail": "\n> fluxer_desktop@0.0.0 typecheck /Users/hampus/Development/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:08:26.138Z", - "endedAt": "2026-06-09T04:08:46.274Z", - "durationMs": 20136, - "stdoutTail": "\n> @fluxer/webrtc-sender@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml --features camera-native\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/webrtc-sender.darwin-arm64.node\n", - "stderrTail": "warning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\nwarning: struct `DmabufFrameDesc` is never constructed\n --> src/texture_source.rs:14:12\n |\n14 | pub struct DmabufFrameDesc {\n | ^^^^^^^^^^^^^^^\n |\n = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\nwarning: variants `InvalidDimensions`, `UnsupportedFormat`, `InvalidPlanes`, and `SdkNativeTextureUnsupported` are never constructed\n --> src/texture_source.rs:29:5\n |\n27 | pub enum TextureEncodeError {\n | ------------------ variants in this enum\n28 | NoTexture,\n29 | InvalidDimensions,\n | ^^^^^^^^^^^^^^^^^\n30 | UnsupportedFormat,\n | ^^^^^^^^^^^^^^^^^\n31 | InvalidPlanes,\n | ^^^^^^^^^^^^^\n...\n34 | SdkNativeTextureUnsupported,\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n |\n = note: `TextureEncodeError` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis\n\nwarning: function `fourcc` is never used\n --> src/texture_source.rs:75:10\n |\n75 | const fn fourcc(bytes: [u8; 4]) -> u32 {\n | ^^^^^^\n\nwarning: constant `DRM_FORMAT_XRGB8888` is never used\n --> src/texture_source.rs:79:7\n |\n79 | const DRM_FORMAT_XRGB8888: u32 = fourcc(*b\"XR24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ARGB8888` is never used\n --> src/texture_source.rs:80:7\n |\n80 | const DRM_FORMAT_ARGB8888: u32 = fourcc(*b\"AR24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_XBGR8888` is never used\n --> src/texture_source.rs:81:7\n |\n81 | const DRM_FORMAT_XBGR8888: u32 = fourcc(*b\"XB24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ABGR8888` is never used\n --> src/texture_source.rs:82:7\n |\n82 | const DRM_FORMAT_ABGR8888: u32 = fourcc(*b\"AB24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_XRGB2101010` is never used\n --> src/texture_source.rs:83:7\n |\n83 | const DRM_FORMAT_XRGB2101010: u32 = fourcc(*b\"XR30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ARGB2101010` is never used\n --> src/texture_source.rs:84:7\n |\n84 | const DRM_FORMAT_ARGB2101010: u32 = fourcc(*b\"AR30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_XBGR2101010` is never used\n --> src/texture_source.rs:85:7\n |\n85 | const DRM_FORMAT_XBGR2101010: u32 = fourcc(*b\"XB30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ABGR2101010` is never used\n --> src/texture_source.rs:86:7\n |\n86 | const DRM_FORMAT_ABGR2101010: u32 = fourcc(*b\"AB30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_NV12` is never used\n --> src/texture_source.rs:87:7\n |\n87 | const DRM_FORMAT_NV12: u32 = fourcc(*b\"NV12\");\n | ^^^^^^^^^^^^^^^\n\nwarning: function `drm_format_supported` is never used\n --> src/texture_source.rs:89:8\n |\n89 | pub fn drm_format_supported(drm_format: u32) -> bool {\n | ^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `MAX_TEXTURE_EDGE` is never used\n --> src/texture_source.rs:104:7\n |\n104 | const MAX_TEXTURE_EDGE: u32 = 8192;\n | ^^^^^^^^^^^^^^^^\n\nwarning: function `validate_dimensions` is never used\n --> src/texture_source.rs:106:4\n |\n106 | fn validate_dimensions(width: u32, height: u32) -> Result<(), TextureEncodeError> {\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: function `validate_dmabuf_desc` is never used\n --> src/texture_source.rs:131:8\n |\n131 | pub fn validate_dmabuf_desc(desc: &DmabufFrameDesc) -> Result<(), TextureEncodeError> {\n | ^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `sdk_accepts_dmabuf_texture_buffers` is never used\n --> src/texture_source.rs:244:4\n |\n244 | fn sdk_accepts_dmabuf_texture_buffers() -> bool {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `should_attempt_dmabuf_encode` is never used\n --> src/texture_source.rs:262:8\n |\n262 | pub fn should_attempt_dmabuf_encode(\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `try_publish_dmabuf` is never used\n --> src/texture_source.rs:335:12\n |\n335 | pub fn try_publish_dmabuf(\n | ^^^^^^^^^^^^^^^^^^\n\nwarning: function `publish_native_buffer` is never used\n --> src/texture_source.rs:362:8\n |\n362 | fn publish_native_buffer(source: &NativeVideoSource, buffer: NativeBuffer, timestamp_us: i64) {\n | ^^^^^^^^^^^^^^^^^^^^^\n\nwarning: variants `Bgra` and `Nv12` are never constructed\n --> src/engine.rs:256:5\n |\n255 | enum PendingVideoFrame {\n | ----------------- variants in this enum\n256 | Bgra {\n | ^^^^\n...\n264 | Nv12 {\n | ^^^^\n\nwarning: `fluxer_webrtc_sender` (lib) generated 21 warnings\n Finished `release` profile [optimized] target(s) in 0.47s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/webrtc-sender.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:08:46.274Z", - "endedAt": "2026-06-09T04:09:48.093Z", - "durationMs": 61819, - "stdoutTail": "_and_raw ... ok\ntest events::tests::json_u8_array_serializes_bytes_as_numbers ... ok\ntest events::tests::leaves_plain_ascii_and_unicode_untouched ... ok\ntest events::tests::raw_array_preserves_prebuilt_json_objects ... ok\ntest events::tests::track_source_strings_match_livekit_js_sources ... ok\ntest hardware_encoder::tests::publish_codec_runtime_gate_accepts_h265_when_reported_available ... ok\ntest hardware_encoder::tests::publish_codec_runtime_gate_only_requires_hardware_for_h265 ... ok\ntest hardware_encoder::tests::reports_available_nvenc_when_compiled_and_runtime_ready ... ok\ntest hardware_encoder::tests::reports_available_videotoolbox_with_h264_and_h265 ... ok\ntest hardware_encoder::tests::reports_driver_sdk_incompatibility_before_encode_session ... ok\ntest hardware_encoder::tests::reports_h264_only_when_hevc_guid_is_absent ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_compiled ... ok\ntest hardware_encoder::tests::reports_missing_runtime_prerequisites_when_probe_is_absent ... ok\ntest hardware_encoder::tests::reports_no_cuda_devices_using_obs_style_reason ... ok\ntest hardware_encoder::tests::reports_no_supported_codecs_when_guid_query_finds_no_fluxer_codecs ... ok\ntest hardware_encoder::tests::reports_not_compiled_before_runtime_missing ... ok\ntest hardware_encoder::tests::reports_unsupported_platforms_clearly ... ok\ntest hardware_encoder::tests::reports_videotoolbox_h264_only_when_hevc_session_is_unavailable ... ok\ntest hardware_encoder::tests::reports_videotoolbox_unavailable_with_status_details ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_publish_backend_not_compiled ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_runtime_dll_names_when_missing ... ok\ntest hardware_encoder::tests::reports_windows_nvenc_without_linux_zero_copy_inputs ... ok\ntest send_control::tests::audio_buffer_growth_caps_at_configured_max_and_reset_clears_pressure ... ok\ntest send_control::tests::audio_buffer_expands_on_gaps_and_shrinks_after_stability ... ok\ntest send_control::tests::audio_buffer_ignores_clock_regression_without_false_rebuffer ... ok\ntest send_control::tests::audio_buffer_max_is_clamped_to_real_time_bounds ... ok\ntest send_control::tests::idle_snapshot_reflects_audio_pressure_without_video_state ... ok\ntest send_control::tests::video_controller_continues_degrading_under_sustained_latency_pressure ... ok\ntest send_control::tests::video_controller_degrades_on_encoder_drop_pressure ... ok\ntest send_control::tests::video_controller_degrades_on_encoder_egress_pressure ... ok\ntest send_control::tests::audio_buffer_stress_stays_within_realtime_bounds_under_jitter ... ok\ntest send_control::tests::video_controller_degrades_on_latency_pressure_without_coalescing ... ok\ntest send_control::tests::video_controller_ignores_pure_coalescing_jitter ... ok\ntest send_control::tests::audio_buffer_ignores_concurrent_stale_pushes_without_moving_last_push_backwards ... ok\ntest send_control::tests::video_controller_ignores_single_encoder_egress_sample ... ok\ntest send_control::tests::video_controller_ignores_sustained_coalescing_jitter_across_windows ... ok\ntest send_control::tests::video_controller_keeps_requested_fps_when_adaptive_send_is_disabled ... ok\ntest send_control::tests::video_controller_preserves_pressure_reason_while_degraded_but_stable ... ok\ntest send_control::tests::video_controller_recovers_after_stable_windows ... ok\ntest send_control::tests::video_controller_recovers_from_minimum_in_two_stable_steps ... ok\ntest send_control::tests::video_controller_requires_a_full_stable_recovery_window ... ok\ntest send_control::tests::video_stats_effective_fps_uses_capture_time_bounds_for_out_of_order_records ... ok\ntest send_control::tests::video_stats_snapshot_counts_coalescing_drops_failures_and_effective_fps ... ok\ntest send_control::tests::video_controller_stress_keeps_target_inside_configured_bounds ... ok\ntest stats::tests::bitrate_computes_kbps_from_byte_delta ... ok\ntest stats::tests::bitrate_first_sample_is_zero ... ok\ntest stats::tests::bitrate_half_second_doubles_rate ... ok\ntest stats::tests::bitrate_rejects_backwards_time_and_bytes ... ok\ntest stats::tests::empty_stats_serialise_to_null_rtt_and_empty_arrays ... ok\ntest stats::tests::sanitize_audio_level_clamps ... ok\ntest stats::tests::full_stats_serialise_to_exact_contract_shape ... ok\ntest stats::tests::sanitize_kbps_drops_nan_inf_negative_and_rounds ... ok\ntest send_control::tests::video_stats_handles_concurrent_recording_without_lost_counts ... ok\ntest stats::tests::send_health_serialises_to_exact_contract_shape ... ok\ntest stats::tests::unit_conversions_seconds_to_ms ... ok\ntest stats::tests::video_inbound_omits_audio_level_audio_omits_fps ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_populates_all_plane_arrays ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_incomplete_native_inputs ... ok\ntest texture_source::tests::dmabuf_desc_from_parts_rejects_negative_fds ... ok\ntest texture_source::tests::drm_format_gate_accepts_obs_vkcapture_texture_formats ... ok\ntest texture_source::tests::dxgi_format_gate_accepts_8bit_rgba_bgra_only ... ok\ntest texture_source::tests::error_strings_are_stable ... ok\ntest texture_source::tests::probe_is_available_only_for_explicit_hardware_codecs ... ok\ntest texture_source::tests::should_attempt_dmabuf_is_sdk_gated_after_validation_capability ... ok\ntest texture_source::tests::should_attempt_falls_back_when_capability_unavailable ... ok\ntest texture_source::tests::should_attempt_validates_frame_when_capability_available ... ok\ntest texture_source::tests::validate_accepts_a_clean_bgra_texture ... ok\ntest texture_source::tests::validate_dmabuf_accepts_supported_formats_with_optional_uuid ... ok\ntest texture_source::tests::validate_dmabuf_rejects_invalid_planes ... ok\ntest texture_source::tests::validate_precedence_handle_before_dims_before_format ... ok\ntest texture_source::tests::validate_rejects_odd_zero_and_oversized_dims ... ok\ntest texture_source::tests::validate_rejects_unsupported_format ... ok\ntest texture_source::tests::validate_rejects_zero_handle_as_no_texture ... ok\ntest yuv::tests::bgra_black_white_grey_levels ... ok\ntest engine::tests::pcm16_audio_frame_rejects_implausible_format_and_overlong_chunks ... ok\ntest yuv::tests::bgra_golden_2x2_solid_colour_exact_bytes ... ok\ntest yuv::tests::bgra_golden_strided_2x2_skips_row_padding ... ok\ntest yuv::tests::bgra_plane_sizes ... ok\ntest yuv::tests::bgra_primaries_have_expected_chroma_signs ... ok\ntest yuv::tests::copy_nv12_planes_preserves_nv12_layout ... ok\ntest yuv::tests::copy_nv12_planes_rejects_short_buffers ... ok\ntest yuv::tests::copy_nv12_planes_respects_destination_stride ... ok\ntest yuv::tests::nv12_4x4_deinterleaves_two_chroma_columns ... ok\ntest yuv::tests::nv12_golden_4x2_packed_to_i420 ... ok\ntest yuv::tests::nv12_packed_2x2_deinterleaves ... ok\ntest yuv::tests::nv12_respects_row_padding ... ok\ntest yuv::tests::nv12_short_buffer_is_rejected ... ok\ntest yuv::tests::rejects_odd_or_tiny_dimensions ... ok\ntest yuv::tests::rgb_golden_2x2_solid_colour_matches_bgra_path ... ok\ntest yuv::tests::rgb_plane_sizes_and_levels ... ok\ntest yuv::tests::rgb_rejects_odd_dims_and_short_buffer ... ok\n\ntest result: ok. 144 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 6 tests\ntest video_frame_meta_matches_contract ... ok\ntest outbound_fixtures_match_contract ... ok\ntest inbound_covers_subscribe_and_lifecycle ... ok\ntest outbound_covers_the_single_identity_publish_path ... ok\ntest inbound_fixtures_match_contract ... ok\ntest e2ee_state_is_ok_on_the_wire ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ webrtc-sender loader wrapper\n ✔ resolves the native addon filename for every desktop OS/arch target (0.586ms)\n ✔ rejects unsupported platform/architecture pairs explicitly (0.172042ms)\n ✔ returns a native hardware encoder capability when the binding exports one (1.042375ms)\n ✔ delegates dropped video callback metrics through the VoiceEngine wrapper (0.114833ms)\n ✔ reports VoiceEngine feature capabilities from the wrapped native prototype (0.322209ms)\n ✔ delegates native voice engine prewarm when the binding exports it (0.055542ms)\n ✔ delegates device microphone publish through the VoiceEngine wrapper (0.10525ms)\n ✔ delegates native camera devices and publish options through the VoiceEngine wrapper (0.117583ms)\n ✔ delegates device screen-share publish through the VoiceEngine wrapper (0.119541ms)\n ✔ reports device screen-share publish as unavailable for older native bindings (0.114084ms)\n ✔ delegates screen-share simulcast selection through the VoiceEngine wrapper (0.104083ms)\n ✔ delegates connect options through the VoiceEngine wrapper (0.10775ms)\n ✔ delegates native screen frame sink handle creation through the VoiceEngine wrapper (0.060958ms)\n ✔ treats native screen frame sink handles as unavailable for older voice bindings (0.065791ms)\n ✔ defaults dropped video callback metrics to zero for older native bindings (0.050375ms)\n ✔ delegates remote track subscription updates through the VoiceEngine wrapper (0.071958ms)\n ✔ ignores remote track subscription updates for older native bindings (0.054042ms)\n ✔ delegates data packets through the VoiceEngine wrapper (0.129167ms)\n ✔ rejects invalid data packet payloads in the wrapper (0.146916ms)\n ✔ returns an unavailable hardware encoder capability without a binding (0.039833ms)\n✔ webrtc-sender loader wrapper (4.374833ms)\n▶ livekit harness setup matrix\n ✔ maps all supported codec spellings to expected mime types (1.00325ms)\n ✔ parses codec lists with fallback and rejects empty effective lists (0.878375ms)\n ✔ default non-strict config keeps the fast local live setup small (0.956958ms)\n ✔ strict config expands to multi-codec secondary-publisher scenarios (0.648125ms)\n ✔ stress knobs parse packet loss, resolution stability, audio gap, and video pattern (0.379917ms)\n ✔ stress knobs reject impossible send pacing configurations (1.902ms)\n ✔ explicit secondary codec expectations override rotating defaults (0.287792ms)\n ✔ secondary screen codec aliases are accepted and conflicting aliases are rejected (0.592416ms)\n ✔ external token setup derives identities, requires matching tokens, and redacts reports (0.933834ms)\n ✔ report sanitization strips URL credentials, query strings, and fragments (0.452459ms)\n ✔ jwt subject and TCP target parsing catch malformed live setups early (0.230125ms)\n ✔ admin token has the room-scoped grant needed for server publishing validation (0.303459ms)\n ✔ scenario room names are stable and bounded for generated-token suites (0.07075ms)\n ✔ strict report checks fail when the strict window has too few samples to measure gaps (137.344709ms)\n ✔ strict report checks use only samples inside the measured window (6.192458ms)\n ✔ strict packet loss gate uses loss deltas inside the strict window (10.880291ms)\n ✔ server publishing checks cover primary and secondary screen, audio, and camera tracks (0.760666ms)\n ✔ server publishing checks fail closed on missing server tracks (0.331583ms)\n ✔ server publishing checks reject wrong participant identity (0.246792ms)\n ✔ server publishing checks reject wrong kind, source, and screen mime (0.27275ms)\n ✔ server publishing checks reject muted or missing mute state on expected tracks (0.213459ms)\n ✔ server publishing checks handle protobuf numeric enums without accepting data tracks as video (0.200917ms)\n ✔ server publishing checks fail closed on missing secondary, audio, camera, and screen-audio tracks (0.217459ms)\n ✔ strict server publishing validation does not pass open on Twirp errors (0.242125ms)\n✔ livekit harness setup matrix (166.608375ms)\nℹ tests 44\nℹ suites 2\nℹ pass 44\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 973.929333\n", - "stderrTail": "segmentation v1.13.2\n Compiling convert_case v0.11.0\n Compiling image v0.25.10\n Compiling cxx-build v1.0.194\n Compiling flume v0.11.1\n Compiling core-media-sys v0.1.2\n Compiling napi v3.9.0\n Compiling thiserror-impl v1.0.69\n Compiling objc v0.2.7\n Compiling cocoa v0.20.2\n Compiling cocoa-foundation v0.2.1\n Compiling darling_core v0.14.4\n Compiling block2 v0.6.2\n Compiling objc2-core-foundation v0.3.2\n Compiling libloading v0.9.0\n Compiling ctor v1.0.7\n Compiling anstyle v1.0.14\n Compiling metal v0.18.0\n Compiling parking_lot v0.12.5\n Compiling objc2-foundation v0.3.2\n Compiling der v0.7.10\n Compiling darling_macro v0.14.4\n Compiling clap_lex v1.1.0\n Compiling darling v0.14.4\n Compiling yoke v0.8.2\n Compiling crypto-common v0.1.6\n Compiling block-buffer v0.10.4\n Compiling crypto-bigint v0.5.5\n Compiling digest v0.10.7\n Compiling spki v0.7.3\n Compiling hmac v0.12.1\n Compiling signature v2.2.0\n Compiling pkcs8 v0.10.2\n Compiling hkdf v0.12.4\n Compiling sha2 v0.10.9\n Compiling rfc6979 v0.4.0\n Compiling sec1 v0.7.3\n Compiling pkcs1 v0.7.5\n Compiling ed25519 v2.2.3\n Compiling sha1 v0.10.6\n Compiling core-video-sys v0.1.4\n Compiling ciborium-io v0.2.2\n Compiling rtrb v0.3.4\n Compiling livekit v0.7.43\n Compiling clap_builder v4.6.0\n Compiling fluxer_gpu_rebuild v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/gpu-rebuild)\n Compiling elliptic-curve v0.13.8\n Compiling napi-sys v3.2.1\n Compiling napi-derive-backend v5.0.4\n Compiling ecdsa v0.16.9\n Compiling primeorder v0.13.6\n Compiling device-info v0.1.1\n Compiling p384 v0.13.1\n Compiling p256 v0.13.2\n Compiling fluxer_webrtc_sender v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender)\n Compiling itertools v0.13.0\n Compiling libloading v0.8.9\n Compiling same-file v1.0.6\n Compiling nohash-hasher v0.2.0\n Compiling rustc-hash v2.1.2\n Compiling cast v0.3.0\n Compiling walkdir v2.5.0\n Compiling clap v4.6.1\n Compiling prost-types v0.12.6\n Compiling futures-executor v0.3.32\n Compiling fluxer_screen_frame_bus v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/screen-frame-bus)\n Compiling fluxer_desktop_native v0.1.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/rust)\n Compiling futures v0.3.32\n Compiling oorandom v11.1.5\n Compiling anes v0.1.6\n Compiling criterion-plot v0.6.0\n Compiling napi-derive v3.5.6\n Compiling prost-build v0.12.6\n Compiling pbjson-build v0.6.2\n Compiling tokio-util v0.7.18\n Compiling pbjson v0.6.0\n Compiling tokio-stream v0.1.18\n Compiling bmrng v0.5.2\n Compiling tinytemplate v1.2.1\n Compiling livekit-runtime v0.4.0\n Compiling pbjson-types v0.6.0\n Compiling ppv-lite86 v0.2.21\n Compiling ed25519-dalek v2.2.0\n Compiling half v2.7.1\n Compiling rand_chacha v0.9.0\n Compiling rand_chacha v0.3.1\n Compiling rustls-webpki v0.103.13\n Compiling ciborium-ll v0.2.2\n Compiling rand v0.9.4\n Compiling rand v0.8.6\n Compiling ciborium v0.2.2\n Compiling criterion v0.7.0\n Compiling crossbeam-queue v0.3.12\n Compiling rsa v0.9.10\n Compiling mozjpeg v0.10.13\n Compiling jsonwebtoken v10.4.0\n Compiling nokhwa-core v0.1.9\n Compiling from_variants_impl v1.0.2\n Compiling nokhwa-bindings-linux v0.1.4\n Compiling nokhwa-bindings-windows v0.4.6\n Compiling from_variants v1.0.2\n Compiling zerovec v0.11.6\n Compiling zerotrie v0.2.4\n Compiling nokhwa v0.10.11\n Compiling os_info v3.15.0\n Compiling tokio-rustls v0.26.4\n Compiling hyper-rustls v0.27.9\n Compiling livekit-protocol v0.7.8\n Compiling tinystr v0.8.3\n Compiling potential_utf v0.1.5\n Compiling icu_collections v2.2.0\n Compiling icu_locale_core v2.2.0\n Compiling icu_provider v2.2.0\n Compiling icu_properties v2.2.0\n Compiling icu_normalizer v2.2.0\n Compiling idna_adapter v1.2.2\n Compiling idna v1.1.0\n Compiling url v2.5.8\n Compiling tower-http v0.6.11\n Compiling tungstenite v0.29.0\n Compiling reqwest v0.12.28\n Compiling tokio-tungstenite v0.29.0\n Compiling webrtc-sys-build v0.3.18\n Compiling webrtc-sys v0.3.33 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys)\n Compiling livekit-datatrack v0.1.8\n Compiling livekit-api v0.5.0\n Compiling libwebrtc v0.3.35 (/Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/vendor/libwebrtc)\nwarning: unused import: `enum_dispatch`\n --> vendor/libwebrtc/src/audio_source.rs:15:13\n |\n15 | use crate::{enum_dispatch, imp::audio_source as imp_as};\n | ^^^^^^^^^^^^^\n |\n = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\nwarning: unnecessary `unsafe` block\n --> vendor/libwebrtc/src/native/apm.rs:32:25\n |\n32 | sys_handle: unsafe {\n | ^^^^^^ unnecessary `unsafe` block\n |\n = note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/libwebrtc/src/native/audio_mixer.rs:30:34\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option;\n | ^^^^^ the lifetime is elided here ^^^^^^^^^^ the same lifetime is hidden here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n30 | fn get_audio_frame_with_info(&self, target_sample_rate: u32) -> Option>;\n | ++++\n\nwarning: `libwebrtc` (lib) generated 3 warnings (run `cargo fix --lib -p libwebrtc` to apply 2 suggestions)\nwarning: struct `DmabufFrameDesc` is never constructed\n --> src/texture_source.rs:14:12\n |\n14 | pub struct DmabufFrameDesc {\n | ^^^^^^^^^^^^^^^\n |\n = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\nwarning: variants `InvalidDimensions`, `UnsupportedFormat`, `InvalidPlanes`, and `SdkNativeTextureUnsupported` are never constructed\n --> src/texture_source.rs:29:5\n |\n27 | pub enum TextureEncodeError {\n | ------------------ variants in this enum\n28 | NoTexture,\n29 | InvalidDimensions,\n | ^^^^^^^^^^^^^^^^^\n30 | UnsupportedFormat,\n | ^^^^^^^^^^^^^^^^^\n31 | InvalidPlanes,\n | ^^^^^^^^^^^^^\n...\n34 | SdkNativeTextureUnsupported,\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n |\n = note: `TextureEncodeError` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis\n\nwarning: function `fourcc` is never used\n --> src/texture_source.rs:75:10\n |\n75 | const fn fourcc(bytes: [u8; 4]) -> u32 {\n | ^^^^^^\n\nwarning: constant `DRM_FORMAT_XRGB8888` is never used\n --> src/texture_source.rs:79:7\n |\n79 | const DRM_FORMAT_XRGB8888: u32 = fourcc(*b\"XR24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ARGB8888` is never used\n --> src/texture_source.rs:80:7\n |\n80 | const DRM_FORMAT_ARGB8888: u32 = fourcc(*b\"AR24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_XBGR8888` is never used\n --> src/texture_source.rs:81:7\n |\n81 | const DRM_FORMAT_XBGR8888: u32 = fourcc(*b\"XB24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ABGR8888` is never used\n --> src/texture_source.rs:82:7\n |\n82 | const DRM_FORMAT_ABGR8888: u32 = fourcc(*b\"AB24\");\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_XRGB2101010` is never used\n --> src/texture_source.rs:83:7\n |\n83 | const DRM_FORMAT_XRGB2101010: u32 = fourcc(*b\"XR30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ARGB2101010` is never used\n --> src/texture_source.rs:84:7\n |\n84 | const DRM_FORMAT_ARGB2101010: u32 = fourcc(*b\"AR30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_XBGR2101010` is never used\n --> src/texture_source.rs:85:7\n |\n85 | const DRM_FORMAT_XBGR2101010: u32 = fourcc(*b\"XB30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_ABGR2101010` is never used\n --> src/texture_source.rs:86:7\n |\n86 | const DRM_FORMAT_ABGR2101010: u32 = fourcc(*b\"AB30\");\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `DRM_FORMAT_NV12` is never used\n --> src/texture_source.rs:87:7\n |\n87 | const DRM_FORMAT_NV12: u32 = fourcc(*b\"NV12\");\n | ^^^^^^^^^^^^^^^\n\nwarning: function `drm_format_supported` is never used\n --> src/texture_source.rs:89:8\n |\n89 | pub fn drm_format_supported(drm_format: u32) -> bool {\n | ^^^^^^^^^^^^^^^^^^^^\n\nwarning: constant `MAX_TEXTURE_EDGE` is never used\n --> src/texture_source.rs:104:7\n |\n104 | const MAX_TEXTURE_EDGE: u32 = 8192;\n | ^^^^^^^^^^^^^^^^\n\nwarning: function `validate_dimensions` is never used\n --> src/texture_source.rs:106:4\n |\n106 | fn validate_dimensions(width: u32, height: u32) -> Result<(), TextureEncodeError> {\n | ^^^^^^^^^^^^^^^^^^^\n\nwarning: function `validate_dmabuf_desc` is never used\n --> src/texture_source.rs:131:8\n |\n131 | pub fn validate_dmabuf_desc(desc: &DmabufFrameDesc) -> Result<(), TextureEncodeError> {\n | ^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `sdk_accepts_dmabuf_texture_buffers` is never used\n --> src/texture_source.rs:244:4\n |\n244 | fn sdk_accepts_dmabuf_texture_buffers() -> bool {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `should_attempt_dmabuf_encode` is never used\n --> src/texture_source.rs:262:8\n |\n262 | pub fn should_attempt_dmabuf_encode(\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `try_publish_dmabuf` is never used\n --> src/texture_source.rs:335:12\n |\n335 | pub fn try_publish_dmabuf(\n | ^^^^^^^^^^^^^^^^^^\n\nwarning: function `publish_native_buffer` is never used\n --> src/texture_source.rs:362:8\n |\n362 | fn publish_native_buffer(source: &NativeVideoSource, buffer: NativeBuffer, timestamp_us: i64) {\n | ^^^^^^^^^^^^^^^^^^^^^\n\nwarning: variants `Bgra` and `Nv12` are never constructed\n --> src/engine.rs:256:5\n |\n255 | enum PendingVideoFrame {\n | ----------------- variants in this enum\n256 | Bgra {\n | ^^^^\n...\n264 | Nv12 {\n | ^^^^\n\nwarning: variant `Failed` is never constructed\n --> src/config.rs:62:5\n |\n57 | pub enum PublisherState {\n | -------------- variant in this enum\n...\n62 | Failed,\n | ^^^^^^\n |\n = note: `PublisherState` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis\n = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\nwarning: static `PREWARMED_LIVEKIT_RUNTIME` is never used\n --> src/engine.rs:102:8\n |\n102 | static PREWARMED_LIVEKIT_RUNTIME: LazyLock>>> =\n | ^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `prewarm_voice_engine` is never used\n --> src/engine.rs:203:8\n |\n203 | pub fn prewarm_voice_engine() -> napi::Result<()> {\n | ^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `prewarm_voice_engine_inner` is never used\n --> src/engine.rs:212:4\n |\n212 | fn prewarm_voice_engine_inner() -> napi::Result<()> {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: variant `Nv12` is never constructed\n --> src/engine.rs:264:5\n |\n255 | enum PendingVideoFrame {\n | ----------------- variant in this enum\n...\n264 | Nv12 {\n | ^^^^\n\nwarning: `fluxer_webrtc_sender` (lib test) generated 9 warnings (4 duplicates)\nwarning: `fluxer_webrtc_sender` (lib) generated 21 warnings\n Finished `test` profile [unoptimized + debuginfo] target(s) in 58.51s\n Running unittests src/lib.rs (target/debug/deps/fluxer_webrtc_sender-986568c9de5e7b47)\n Running tests/event_contract.rs (target/debug/deps/event_contract-ed4cabbef0fb9f7c)\n Doc-tests fluxer_webrtc_sender\n" - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:09:48.093Z", - "endedAt": "2026-06-09T04:09:50.341Z", - "durationMs": 2248, - "stdoutTail": "\n> @fluxer/webrtc-sender@0.0.0 test:livekit /Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender\n> node scripts/livekit-harness.mjs\n\n[livekit-harness] connecting to ws://localhost:7880 room=fluxer-webrtc-harness-39291-5ee61a43-3009-4112-bca3-ae4b4667a371\n[livekit-harness] PASS: 19 checks, inboundAudioFrames=72, inboundVideoFrames=5, videoCallbacks=5, receivedFps=0, producedFps=0, maxVideoFrameGapMs=0, maxAudioFrameGapMs=0, maxObservedPacketLoss=0, maxObservedPacketLossDelta=0, resolutionMismatches=0, droppedVideoFrameCallbacks=0\n", - "stderrTail": "" - }, - { - "name": "mac-screen-capture-build", - "command": "pnpm --dir native/mac-screen-capture build", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:09:50.341Z", - "endedAt": "2026-06-09T04:09:55.416Z", - "durationMs": 5075, - "stdoutTail": "\n> @fluxer/mac-screen-capture@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture/mac-screen-capture.darwin-arm64.node\n", - "stderrTail": " Finished `release` profile [optimized] target(s) in 0.33s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture/mac-screen-capture.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "mac-screen-capture-tests", - "command": "pnpm --dir native/mac-screen-capture test", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:09:55.416Z", - "endedAt": "2026-06-09T04:09:59.295Z", - "durationMs": 3879, - "stdoutTail": "\n> @fluxer/mac-screen-capture@0.0.0 test /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n> pnpm test:cargo && node --test index.test.mjs\n\n\n> @fluxer/mac-screen-capture@0.0.0 test:cargo /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture\n> cargo test --manifest-path Cargo.toml\n\n\nrunning 78 tests\ntest audio_pool::tests::append_concatenates_planar_to_interleaved_like_payload ... ok\ntest audio_pool::tests::as_mut_ptr_returns_slot_base_address ... ok\ntest audio_pool::tests::append_rejects_overflow ... ok\ntest audio_pool::tests::acquire_release_cycle_increments_counters ... ok\ntest audio_pool::tests::capacity_one_pool_round_trips ... ok\ntest audio_pool::tests::default_pool_dimensions_match_constants ... ok\ntest audio_pool::tests::drop_returns_slot_to_free_list ... ok\ntest audio_pool::tests::into_external_parts_drop_returns_slot_to_pool ... ok\ntest audio_pool::tests::into_external_parts_exposes_filled_pointer_and_length ... ok\ntest audio_pool::tests::new_rejects_bytes_per_slot_above_max ... ok\ntest audio_pool::tests::new_rejects_zero_bytes_per_slot ... ok\ntest audio_pool::tests::new_rejects_zero_capacity ... ok\ntest audio_pool::tests::into_external_parts_holds_slot_across_send ... ok\ntest audio_pool::tests::pool_exhausts_at_cap_seventeenth_acquire_returns_none ... ok\ntest audio_pool::tests::recycled_slot_persists_buffer_storage_until_overwritten ... ok\ntest audio_pool::tests::stats_in_flight_matches_simultaneous_holders ... ok\ntest audio_pool::tests::write_rejects_payload_larger_than_slot ... ok\ntest audio_pool::tests::write_then_data_slice_matches_payload ... ok\ntest config::tests::audio_sample_format_strings_and_widths ... ok\ntest config::tests::builder_accepts_48khz_stereo_audio ... ok\ntest config::tests::builder_accepts_fps_120 ... ok\ntest config::tests::builder_accepts_hdr_with_displayp3 ... ok\ntest config::tests::builder_captures_audio_toggle_preserves_frame_interval ... ok\ntest config::tests::builder_defaults_produce_valid_config ... ok\ntest config::tests::builder_rejects_audio_channel_count_zero ... ok\ntest config::tests::builder_rejects_fps_above_max ... ok\ntest config::tests::builder_rejects_audio_sample_rate_zero ... ok\ntest config::tests::builder_rejects_fps_zero ... ok\ntest config::tests::builder_rejects_hdr_without_displayp3 ... ok\ntest config::tests::builder_rejects_queue_depth_above_max ... ok\ntest config::tests::builder_rejects_queue_depth_zero ... ok\ntest config::tests::captures_audio_defaults_to_false ... ok\ntest config::tests::default_config_validates ... ok\ntest config::tests::color_space_names_present ... ok\ntest config::tests::failure_reason_strings_non_empty ... ok\ntest config::tests::mac_screen_share_audio_frame_constructs_valid ... ok\ntest config::tests::mac_screen_share_audio_frame_rejects_invalid_sample_rate ... ok\ntest config::tests::mac_screen_share_audio_frame_rejects_too_many_channels ... ok\ntest config::tests::mac_screen_share_audio_frame_with_bytes_rejects_bad_sample_rate ... ok\ntest config::tests::mac_screen_share_audio_frame_with_bytes_round_trip ... ok\ntest config::tests::minimum_frame_interval_120_fps_under_8_3ms ... ok\ntest config::tests::minimum_frame_interval_30_fps_factor_applied ... ok\ntest config::tests::minimum_frame_interval_60_fps_is_15_ms ... ok\ntest config::tests::pixel_format_fourcc_matches_obs_constants ... ok\ntest iosurface_pair::tests::new_pair_is_empty ... ok\ntest iosurface_pair::tests::take_current_on_empty_returns_none ... ok\ntest napi_surface_macos::audio_extract_tests::ablstorage_layout_matches_apple_abl ... ok\ntest napi_surface_macos::audio_extract_tests::ablstorage_size_is_large_enough_for_audio_buffer_list ... ok\ntest napi_surface_macos::audio_extract_tests::extract_audio_bytes_rejects_metadata_sample_buffer_gracefully ... ok\ntest napi_surface_macos::dispatch_queue_tests::audio_frame_payload_into_input_carries_slot_and_metadata ... ok\ntest napi_surface_macos::dispatch_queue_tests::build_capture_config_carries_audio_settings ... ok\ntest napi_surface_macos::dispatch_queue_tests::build_capture_config_preserves_frame_interval_when_audio_enabled ... ok\ntest napi_surface_macos::dispatch_queue_tests::classify_pcm_flags_detects_f32_interleaved_as_f32_planar ... ok\ntest napi_surface_macos::dispatch_queue_tests::classify_pcm_flags_detects_f32_planar ... ok\ntest napi_surface_macos::dispatch_queue_tests::classify_pcm_flags_detects_i16_interleaved ... ok\ntest napi_surface_macos::dispatch_queue_tests::classify_pcm_flags_rejects_unknown ... ok\ntest napi_surface_macos::dispatch_queue_tests::sample_queue_construction_does_not_trap_libdispatch ... ok\ntest napi_surface_macos::external_buffer_tests::allocator_counter_self_test_reports_nonzero_when_allocating ... ok\ntest napi_surface_macos::external_buffer_tests::audio_buffer_external_pointer_round_trip ... ok\ntest audio_pool::tests::multi_thread_acquire_release_stress_8_threads_1000_ops_no_deadlock ... ok\ntest napi_surface_macos::external_buffer_tests::external_buffer_finalize_runs_exactly_once_per_frame ... ok\ntest napi_surface_macos::external_buffer_tests::buffer_external_drop_fn_runs_on_pool_thread_or_napi_thread ... ok\ntest napi_surface_macos::external_buffer_tests::multi_frame_external_buffer_lifecycle ... ok\ntest napi_surface_macos::external_buffer_tests::pool_exhaustion_with_external_buffers_holds_diagnostic ... ok\ntest os_version::tests::classify_macos_11_is_unsupported ... ok\ntest os_version::tests::classify_macos_12_3_is_supported ... ok\ntest os_version::tests::classify_unknown_version_is_unsupported ... ok\ntest os_version::tests::format_version_trims_zero_patch ... ok\ntest os_version::tests::meets_floor_exact_match ... ok\ntest os_version::tests::meets_floor_higher_major ... ok\ntest os_version::tests::rejects_below_floor ... ok\ntest napi_surface_macos::external_buffer_tests::zero_allocation_in_steady_state_acquire_write_external_release ... ok\ntest napi_surface_macos::dispatch_queue_tests::parallel_sample_queue_construction_is_safe ... ok\ntest napi_surface_macos::dispatch_queue_tests::sample_queue_dispatches_blocks_without_crashing ... ok\ntest iosurface_pair::macos_tests::push_increments_use_count_and_take_current_does_not_release ... ok\ntest iosurface_pair::macos_tests::push_rotates_and_drop_releases_both_slots ... ok\ntest iosurface_pair::macos_tests::clear_releases_both_slots ... ok\ntest iosurface_pair::macos_tests::third_push_evicts_oldest_and_releases_it ... ok\n\ntest result: ok. 78 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n▶ mac-screen-capture loader wrapper\n ✔ forwards source id, kind, and dimensions to native binding (1.445ms)\n ✔ defaults sourceKind to screen and frameRate to 30 (0.117166ms)\n ✔ forwards display and window sources from native binding without rewriting ids (0.12875ms)\n ✔ reports ScreenCaptureKit support and permission from native binding (0.612667ms)\n ✔ installs a native frame sink handle once before start (0.174542ms)\n ✔ fails before native start when a native frame sink is required but missing (0.416709ms)\n ✔ emits closed once when native lifecycle closes and stop is called later (0.153417ms)\n ✔ lifecycle error emits Error event (0.120416ms)\n✔ mac-screen-capture loader wrapper (3.986375ms)\nℹ tests 8\nℹ suites 1\nℹ pass 8\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 371.859541\n", - "stderrTail": " Compiling fluxer_gpu_rebuild v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/gpu-rebuild)\n Compiling fluxer_screen_frame_bus v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/screen-frame-bus)\n Compiling fluxer_mac_screen_capture v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-screen-capture)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 2.23s\n Running unittests src/lib.rs (target/debug/deps/fluxer_mac_screen_capture-7d8f5a432df33d79)\n Doc-tests fluxer_mac_screen_capture\n" - }, - { - "name": "mac-app-audio-build", - "command": "pnpm --dir native/mac-app-audio build", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:09:59.295Z", - "endedAt": "2026-06-09T04:10:03.149Z", - "durationMs": 3854, - "stdoutTail": "\n> @fluxer/mac-app-audio@0.0.0 build /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n+ /Users/hampus/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo build --release --target aarch64-apple-darwin --manifest-path Cargo.toml\n+ codesign --force --sign - /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio/mac-app-audio.darwin-arm64.node\n", - "stderrTail": " Finished `release` profile [optimized] target(s) in 0.27s\n/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio/mac-app-audio.darwin-arm64.node: replacing existing signature\n" - }, - { - "name": "mac-app-audio-rust-tests", - "command": "pnpm --dir native/mac-app-audio test:cargo", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:10:03.149Z", - "endedAt": "2026-06-09T04:10:05.465Z", - "durationMs": 2316, - "stdoutTail": "\n> @fluxer/mac-app-audio@0.0.0 test:cargo /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n> cargo test --manifest-path Cargo.toml\n\n\nrunning 49 tests\ntest audio_converter::tests::build_input_interleaved ... ok\ntest audio_converter::tests::build_output_mono_44_1k ... ok\ntest audio_converter::tests::build_input_non_interleaved ... ok\ntest audio_converter::tests::build_output_stereo_48k ... ok\ntest audio_converter::tests::converted_frame_count_exact ... ok\ntest audio_converter::tests::handle_float64_input ... ok\ntest audio_converter::tests::duplicate_mono_to_stereo ... ok\ntest audio_converter::tests::input_frame_count_interleaved_and_planar ... ok\ntest audio_converter::tests::interleave_planar_stereo ... ok\ntest audio_converter::tests::linear_resample_to_target_rate ... ok\ntest audio_converter::tests::lpcm_fourcc ... ok\ntest audio_converter::tests::normalize_signed_int16 ... ok\ntest audio_converter::tests::output_frame_capacity_rounds_up ... ok\ntest foundation::tests::nsstring_to_string_handles_none ... ok\ntest foundation::tests::mutable_array_initial_count_is_zero ... ok\ntest foundation::tests::mutable_dictionary_initial_count_is_zero ... ok\ntest os_version::tests::classify_macos_10_15_is_unsupported_and_mentions_min_version ... ok\ntest foundation::tests::nsstring_round_trip ... ok\ntest os_version::tests::classify_macos_12_3_is_sck_only ... ok\ntest os_version::tests::classify_macos_14_2_has_both_backends ... ok\ntest os_version::tests::classify_macos_15_is_supported ... ok\ntest os_version::tests::classify_unknown_version_is_unsupported ... ok\ntest os_version::tests::coreaudio_floor_14_2 ... ok\ntest os_version::tests::format_version_trims_zero_patch ... ok\ntest os_version::tests::meets_floor_exact_match ... ok\ntest os_version::tests::meets_floor_higher_major ... ok\ntest os_version::tests::meets_floor_higher_minor ... ok\ntest os_version::tests::rejects_below_floor_major ... ok\ntest os_version::tests::rejects_below_floor_minor ... ok\ntest os_version::tests::rejects_macos_10_15_for_sck ... ok\ntest process_tree::tests::collect_related_returns_tree ... ok\ntest process_tree::tests::same_launch_tree_excludes_unrelated ... ok\ntest process_tree::tests::same_launch_tree_includes_direct_child ... ok\ntest process_tree::tests::same_launch_tree_includes_pgrp_peer ... ok\ntest related_app::tests::helper_bundle_strips_suffix ... ok\ntest related_app::tests::helper_name_strips_helper_suffix ... ok\ntest related_app::tests::related_by_dot_prefix ... ok\ntest related_app::tests::related_via_bundle_helper_base ... ok\ntest related_app::tests::related_via_name_helper_base ... ok\ntest related_app::tests::unrelated_returns_false ... ok\ntest sck::tests::cgrect_intersection_handles_negative_and_disjoint ... ok\ntest source_state::tests::double_start_rejected ... ok\ntest source_state::tests::happy_path ... ok\ntest source_state::tests::cancel_idle_short_circuits ... ok\ntest source_state::tests::is_allowed_exhaustive ... ok\ntest source_state::tests::mark_fatal_forces_stopped ... ok\ntest source_state::tests::mark_fatal_idempotent ... ok\ntest source_state::tests::start_while_stopping_rejected ... ok\ntest source_state::tests::stop_before_start_rejected ... ok\n\ntest result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n\n", - "stderrTail": " Compiling fluxer_mac_app_audio v0.0.0 (/Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio)\n Finished `test` profile [unoptimized + debuginfo] target(s) in 1.54s\n Running unittests src/lib.rs (target/debug/deps/fluxer_mac_app_audio-b86bec7aea37ec64)\n Doc-tests fluxer_mac_app_audio\n" - }, - { - "name": "mac-app-audio-js-tests", - "command": "'/Users/hampus/Development/fluxer/node_modules/.pnpm/node_modules/.bin/vitest' run", - "category": "platform-native", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-09T04:10:05.465Z", - "endedAt": "2026-06-09T04:10:06.715Z", - "durationMs": 1250, - "stdoutTail": "\n RUN v4.0.18 /Users/hampus/Development/fluxer/fluxer_desktop/native/mac-app-audio\n\n ✓ index.test.mjs (6 tests) 2ms\n\n Test Files 1 passed (1)\n Tests 6 passed (6)\n Start at 06:10:06\n Duration 489ms (transform 29ms, setup 0ms, import 298ms, tests 2ms, environment 0ms)\n\n", - "stderrTail": "" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/2026-06-09T0413-codex-fedora-livekit-smoke.json b/fluxer_desktop/native-media-reports/2026-06-09T0413-codex-fedora-livekit-smoke.json deleted file mode 100644 index 5a4de10b0..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-09T0413-codex-fedora-livekit-smoke.json +++ /dev/null @@ -1,334 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-09T04:15:05.489Z", - "endedAt": "2026-06-09T04:15:36.366Z", - "platform": "linux", - "arch": "arm64", - "config": { - "url": "ws://10.211.55.2:7880", - "serverApiUrl": "http://10.211.55.2:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-283674-883784d5-5c9a-4466-84b5-dade644ebf82", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-283674", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-283674", - "subscriberIdentity": "fluxer-native-subscriber-283674", - "required": true, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 320, - "height": 180, - "expectedWidth": 320, - "expectedHeight": 180, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/media/psf/fluxer-dev/fluxer_desktop/native-media-reports/2026-06-09T0413-codex-fedora-livekit-smoke.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 320x180", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 3", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 1", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 3", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - } - ], - "skippedReason": null, - "error": "Error: timed out after 30000ms waiting for: publisher local screenshare publication, subscriber remote screenshare subscription, publisher outbound screenshare stats, publisher outbound screenshare codec video/VP8\n at waitForHarnessSuccess (file:///media/psf/fluxer-dev/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1232:8)\n at async runHarness (file:///media/psf/fluxer-dev/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1831:18)\n at async main (file:///media/psf/fluxer-dev/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1931:18)\n at async file:///media/psf/fluxer-dev/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1964:19", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing libcuda.so.1, libnvidia-encode.so.1" - }, - "metrics": { - "inboundAudioFrames": 2994, - "inboundVideoFrames": 300, - "strictAudioFrames": 297, - "strictVideoFrames": 300, - "videoCallbacks": 300, - "videoBytes": 25920000, - "receivedFps": 10, - "requiredFps": null, - "maxVideoFrameGapMs": 120, - "maxAudioFrameGapMs": 106, - "maxVideoFrameGapOverallMs": 120, - "maxAudioFrameGapOverallMs": 106, - "avDriftMs": 4, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "320x180": 300 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780978506249, - "stoppedAtMs": null, - "framesPushed": 0, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 0, - "lastPushAtMs": null, - "elapsedMs": 30121, - "producedFps": 0 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": { - "participantSid": "PA_XodNLgkfL4bV", - "trackSid": "TR_VSVvE7VrQKNqwD", - "width": 320, - "height": 180, - "timestampUs": 124248690000 - }, - "publisherStats": { - "rttMs": 5, - "outbound": [ - { - "trackSid": "TR_AM3ooPAYg5Q2M6", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "audioLevel": 0.305, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSVvE7VrQKNqwD", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 2.6, - "packetsLost": 0, - "fps": 9, - "width": 320, - "height": 180, - "sourceWidth": 320, - "sourceHeight": 180, - "targetBitrateKbps": 400, - "configuredFps": 15, - "targetFps": 15, - "effectiveFps": 0, - "framesProduced": 0, - "framesAccepted": 0, - "framesDropped": 0, - "framesCoalesced": 0, - "framesCaptured": 0, - "captureFailures": 0, - "maxQueueAgeMs": 0, - "maxPushLatencyMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable", - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 8, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 15, - "outgoingVideoPacingTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "source", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 27, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 5, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_XodNLgkfL4bV", - "trackSid": "TR_AM3ooPAYg5Q2M6", - "kind": "audio", - "bitrateKbps": 99.1, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_XodNLgkfL4bV", - "trackSid": "TR_VSVvE7VrQKNqwD", - "kind": "video", - "bitrateKbps": 2.6, - "packetsLost": 0, - "jitterMs": 1, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoQueueCapacity": 0, - "outgoingVideoMaxQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoPacingTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingVideoPacingMode": "idle", - "outgoingVideoBusActive": false, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/2026-06-14T18-09-25-591Z/summary.json b/fluxer_desktop/native-media-reports/2026-06-14T18-09-25-591Z/summary.json deleted file mode 100644 index 036ddd02c..000000000 --- a/fluxer_desktop/native-media-reports/2026-06-14T18-09-25-591Z/summary.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "status": "fail", - "mode": "smoke", - "strict": false, - "platform": "linux", - "arch": "arm64", - "hostname": "0a84a8e7079b", - "startedAt": "2026-06-14T18:09:25.592Z", - "endedAt": "2026-06-14T18:11:18.780Z", - "reportDir": "/workspaces/fluxer/fluxer_desktop/native-media-reports/2026-06-14T18-09-25-591Z", - "gates": [ - { - "name": "mode-valid", - "status": "pass", - "mode": "smoke" - }, - { - "name": "documentation-current", - "status": "pass", - "missing": [] - }, - { - "name": "strict-prerequisites", - "status": "skip", - "reason": "smoke mode" - }, - { - "name": "strict-command-plan", - "status": "skip", - "reason": "smoke mode" - } - ], - "plannedCommands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "cwd": "." - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "cwd": "." - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "cwd": "." - }, - { - "name": "webrtc-sender-livekit-harness", - "command": "pnpm --dir native/webrtc-sender test:livekit", - "category": "livekit", - "cwd": "." - }, - { - "name": "linux-screen-capture-build", - "command": "pnpm --dir native/linux-screen-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-screen-capture-tests", - "command": "pnpm --dir native/linux-screen-capture test", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-audio-capture-build", - "command": "pnpm --dir native/linux-audio-capture build", - "category": "platform-native", - "cwd": "." - }, - { - "name": "linux-audio-capture-tests", - "command": "pnpm --dir native/linux-audio-capture test", - "category": "platform-native", - "cwd": "." - } - ], - "commands": [ - { - "name": "main-process-native-media-unit-tests", - "command": "node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-14T18:09:25.593Z", - "endedAt": "2026-06-14T18:09:26.814Z", - "durationMs": 1221, - "stdoutTail": "▶ NativeScreenCapture source identity and capability reporting\n ✔ normalizes display and window sources without changing source ids (1.883928ms)\n ✔ starts display and window captures with exact source id and kind and reports diagnostics (1.130214ms)\n ✔ fails fast when native frame sink is required but unavailable (0.578253ms)\n ✔ passes caller-provided capture id and native sink handle to the platform wrapper (0.568254ms)\n ✔ reports macOS and Linux display/window capture capabilities from platform backends (0.846088ms)\n ✔ reports Windows native game capture disabled before loading the addon (0.545086ms)\n ✔ loads Windows native game capture only for the game capture build variant (0.948922ms)\n✔ NativeScreenCapture source identity and capability reporting (7.057249ms)\n▶ NativeVoiceEngine v2 bridge contract fixtures\n ✔ registers and removes every v2 IPC channel from the bridge contract fixture (1.949095ms)\n✔ NativeVoiceEngine v2 bridge contract fixtures (2.38543ms)\n▶ NativeVoiceEngine bridge version pair assertion\n ✔ asserts the host bridge version against the native addon at load time (0.859213ms)\n ✔ disables the native voice engine when the addon reports a mismatched bridge version (0.842422ms)\n ✔ disables the native voice engine when the addon lacks bridge version exports (0.395753ms)\n ✔ disables the native voice engine when the addon rejects the host bridge version (0.459586ms)\n✔ NativeVoiceEngine bridge version pair assertion (2.669932ms)\n▶ NativeVoiceEngine v2 IPC request fixtures\n ✔ replays fixture: valid connect reaches the native engine (0.856421ms)\n ✔ replays fixture: invalid connect args throw (0.387877ms)\n ✔ replays fixture: invalid mic enabled args return typed operation failure (0.984381ms)\n ✔ replays fixture: valid microphone publish returns typed success (0.584336ms)\n ✔ replays fixture: invalid microphone publish returns typed operation failure (0.438002ms)\n ✔ replays fixture: valid zero-copy screen publish reaches native engine (0.566461ms)\n ✔ replays fixture: explicit non-zero-copy screen publish is rejected (0.409836ms)\n ✔ replays fixture: hardware screen encoding without zero-copy is rejected (0.520878ms)\n ✔ replays fixture: valid native screen audio publish reaches native engine (0.494211ms)\n ✔ replays fixture: invalid screen audio route throws (0.494961ms)\n ✔ replays fixture: valid camera publish reaches native engine (0.389836ms)\n ✔ replays fixture: valid native camera sink publish reaches native engine (0.490294ms)\n ✔ replays fixture: valid processed camera publish reaches native engine (1.25884ms)\n ✔ replays fixture: valid processed camera frame reaches native engine (0.41771ms)\n ✔ replays fixture: invalid processed camera frame throws (0.359294ms)\n ✔ replays fixture: invalid camera publish throws (0.341377ms)\n ✔ replays fixture: connected stats return normalized empty stats (0.354044ms)\n ✔ replays fixture: valid participant volume reaches native engine (0.355585ms)\n ✔ replays fixture: invalid participant volume throws (0.334544ms)\n ✔ replays fixture: valid remote subscription reaches native engine (0.425128ms)\n ✔ replays fixture: invalid remote subscription quality throws (0.353668ms)\n ✔ replays fixture: valid data publish reaches native engine (0.394711ms)\n ✔ replays fixture: invalid data payload throws (0.351669ms)\n✔ NativeVoiceEngine v2 IPC request fixtures (11.876651ms)\n▶ NativeVoiceEngine connection lifecycle\n ✔ constructs the singleton and prewarms the native voice backend once at registration (0.389294ms)\n ✔ connect uses the prewarmed singleton instead of constructing an engine per call (0.332418ms)\n ✔ connect waits for a pending prewarm before dialing (9.277679ms)\n ✔ prewarm retries transient addon failures before succeeding (505.813953ms)\n ✔ set-audio-output-device retries transient negative device-count failures (4.178899ms)\n ✔ set-audio-output-device does not retry non-transient failures (2.9481ms)\n ✔ reuses the singleton across disconnect and reconnect cycles (2.513848ms)\n ✔ a second connect awaits the disconnect of the previous session before dialing (2.102095ms)\n ✔ a connect superseded before dialing rejects while latest replacement dials (3.610687ms)\n ✔ clears the native video-frame callback when a session is torn down (1.179257ms)\n ✔ tears down the active session when the owner renderer process exits (1.542634ms)\n ✔ tears down the active session on owner main-frame reload navigation (1.679135ms)\n ✔ keeps the active session for same-document and subframe navigation (1.513508ms)\n ✔ falls back to a no-op video-frame callback for engines without a clear export (0.897714ms)\n ✔ does not forward native events after disconnecting a pending connect (1.364383ms)\n ✔ starts replacement connect before stale native connect completion settles (1.805552ms)\n✔ NativeVoiceEngine connection lifecycle (542.09012ms)\n▶ NativeVoiceEngine screen frame routing\n ✔ publishes screen share with source pacing and exposes a native frame sink handle (1.161882ms)\n ✔ forwards device screen share publish options to the native engine (0.958214ms)\n ✔ keeps screen-share audio published when screen video is unpublished (1.080964ms)\n ✔ pushes screen-share float audio using the typed-array view length (0.929422ms)\n ✔ replaces screen captures without releasing screen-share audio or duplicating priority (0.832755ms)\n ✔ updates active screen-share encoding over IPC (0.951839ms)\n ✔ rejects screen-share encoding updates that change codec or hardware mode (0.791754ms)\n ✔ forwards watch and unwatch operations with source, enablement, and quality intact (0.782337ms)\n ✔ reports VideoToolbox hardware capability from the native addon (0.642129ms)\n ✔ reports native voice engine capabilities from the native addon (0.642879ms)\n ✔ maps native addon microphone not-connected errors to the typed operation code (1.749843ms)\n ✔ rejects device microphone operations while the audio device module is warming (5.035821ms)\n✔ NativeVoiceEngine screen frame routing (15.857342ms)\n▶ NativeVoiceEngine camera preview sessions\n ✔ starts a standalone preview engine when not connected and stops it on stop (0.98463ms)\n ✔ routes camera preview through the active session engine when connected (0.585586ms)\n ✔ stops a standalone preview on the singleton when a call connects (0.590337ms)\n ✔ routes a preview after a call ends through the same singleton engine (0.654712ms)\n ✔ stops a standalone preview when the owner renderer process exits (1.566134ms)\n ✔ stops a standalone preview on owner main-frame reload navigation (1.060881ms)\n ✔ routes camera capture updates through the active session engine (0.643712ms)\n ✔ rejects camera capture updates when not connected (0.563253ms)\n ✔ rejects invalid camera preview args (0.533378ms)\n ✔ drops video frames with a mismatched per-frame bridge version and logs once (0.644129ms)\n✔ NativeVoiceEngine camera preview sessions (8.004754ms)\n▶ NativeVoiceEngine camera background frame routing\n ✔ routes background frames and clears to the active session engine when connected (0.640379ms)\n ✔ routes background frames to the standalone preview engine (0.517586ms)\n ✔ returns false without a session and rejects invalid frames (0.555711ms)\n✔ NativeVoiceEngine camera background frame routing (1.789177ms)\n▶ NativeVoiceEngine readiness\n ✔ reports not ready with the load failure detail when the native module fails to load (0.56242ms)\n ✔ reports not ready when the addon does not expose a VoiceEngine constructor (1.861719ms)\n ✔ reports prewarm-pending readiness before the registration prewarm settles (0.598003ms)\n ✔ surfaces the prewarm failure detail as the not-ready reason and rejects the prewarm call (506.100539ms)\n ✔ reports ready once the singleton is constructed and prewarmed (3.705354ms)\n ✔ pushes a one-time engineReady event when readiness first becomes true (3.76148ms)\n ✔ does not push engineReady to destroyed web contents (4.784236ms)\n✔ NativeVoiceEngine readiness (521.834295ms)\nℹ tests 83\nℹ suites 9\nℹ pass 83\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 1197.319351\n", - "stderrTail": "" - }, - { - "name": "desktop-typecheck", - "command": "pnpm typecheck", - "category": "shared", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-14T18:09:26.814Z", - "endedAt": "2026-06-14T18:09:27.244Z", - "durationMs": 430, - "stdoutTail": "\n> fluxer_desktop@0.0.0 typecheck /workspaces/fluxer/fluxer_desktop\n> tsgo --noEmit\n\n", - "stderrTail": "" - }, - { - "name": "webrtc-sender-build", - "command": "pnpm --dir native/webrtc-sender build", - "category": "native-sender", - "status": "pass", - "code": 0, - "signal": null, - "startedAt": "2026-06-14T18:09:27.244Z", - "endedAt": "2026-06-14T18:10:34.871Z", - "durationMs": 67627, - "stdoutTail": "\n> @fluxer/webrtc-sender@0.0.0 build /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon\n\n[buildRustNodeAddon] cwd=/workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n+ /home/vscode/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/cargo build --release --target aarch64-unknown-linux-gnu --manifest-path Cargo.toml --features camera-native\n", - "stderrTail": " Compiling pest v2.8.6\n Compiling kstring v2.0.2\n Compiling memo-map v0.3.3\n Compiling time-macros v0.2.27\n Compiling nom v8.0.0\n Compiling crossbeam-utils v0.8.21\n Compiling typeid v1.0.3\n Compiling itertools v0.14.0\n Compiling downcast-rs v2.0.2\n Compiling hashbrown v0.11.2\n Compiling tinyvec v1.11.0\n Compiling minijinja v2.20.0\n Compiling minimal-lexical v0.2.1\n Compiling erased-serde v0.4.10\n Compiling dyn-eq v0.1.3\n Compiling dyn-hash v1.0.0\n Compiling regex-automata v0.4.14\n Compiling itertools v0.10.5\n Compiling derive-new v0.7.0\n Compiling bit-vec v0.9.1\n Compiling pastey v0.2.3\n Compiling unicode-normalization v0.1.25\n Compiling nom v7.1.3\n Compiling derive-new v0.5.9\n Compiling downcast-rs v1.2.1\n Compiling bit-vec v0.6.3\n Compiling bit-set v0.10.0\n Compiling pest_meta v2.8.6\n Compiling crunchy v0.2.4\n Compiling anymap3 v1.0.1\n Compiling inventory v0.3.24\n Compiling rayon-core v1.13.0\n Compiling tiny-keccak v2.0.2\n Compiling bit-set v0.5.3\n Compiling serde_urlencoded v0.7.1\n Compiling rustls v0.23.40\n Compiling crossbeam-epoch v0.9.18\n Compiling xattr v1.6.1\n Compiling hashbrown v0.16.1\n Compiling num-bigint v0.4.6\n Compiling png v0.18.1\n Compiling getrandom v0.4.2\n Compiling rand_core v0.10.1\n Compiling time v0.3.47\n Compiling num-iter v0.1.45\n Compiling float-ord v0.3.2\n Compiling pest_generator v2.8.6\n Compiling tar v0.4.46\n Compiling crossbeam-deque v0.8.6\n Compiling reqwest v0.12.28\n Compiling num-complex v0.4.6\n Compiling string-interner v0.14.0\n Compiling pbjson v0.6.0\n Compiling string-interner v0.20.0\n Compiling const-random-macro v0.1.16\n Compiling chacha20 v0.10.0\n Compiling safetensors v0.7.0\n Compiling pbjson-types v0.6.0\n Compiling rustfft v6.4.1\n Compiling ndarray v0.17.2\n Compiling ndarray v0.15.6\n Compiling regex v1.12.3\n Compiling nom-language v0.1.0\n Compiling rayon v1.12.0\n Compiling image v0.25.10\n Compiling const-random v0.1.18\n Compiling pest_derive v2.8.6\n Compiling enum-ordinalize v3.1.15\n Compiling dlv-list v0.5.2\n Compiling scan_fmt v0.2.6\n Compiling rand v0.10.1\n Compiling nix v0.31.3\n Compiling prost-derive v0.11.9\n Compiling tower-http v0.6.11\n Compiling liquid-core v0.26.8\n Compiling webrtc-sys-build v0.3.18\n Compiling educe v0.4.23\n Compiling hashbrown v0.13.2\n Compiling rand_distr v0.6.0\n Compiling os_info v3.15.0\n Compiling glib v0.21.5\n Compiling half v2.7.1\n Compiling livekit-protocol v0.7.8\n Compiling memmap2 v0.5.10\n Compiling ppv-lite86 v0.2.21\n Compiling ordered-multimap v0.6.0\n Compiling memmap2 v0.9.10\n Compiling napi v3.9.1\n Compiling fluxer_screen_frame_bus v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/screen-frame-bus)\n Compiling fluxer_desktop_native v0.1.0 (/workspaces/fluxer/fluxer_desktop/native/rust)\n Compiling crossbeam-queue v0.3.12\n Compiling rand_chacha v0.3.1\n Compiling liquid-lib v0.26.8\n Compiling rand_chacha v0.9.0\n Compiling prost-derive v0.14.4\n Compiling realfft v3.5.0\n Compiling rust-ini v0.19.0\n Compiling rand v0.8.6\n Compiling rand v0.9.4\n Compiling tokio-rustls v0.26.4\n Compiling tract-linalg v0.23.1 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/vendor/tract-linalg-0.23.1)\n Compiling nokhwa-core v0.1.9\n Compiling prost v0.11.9\n Compiling rubato v0.14.1\n Compiling webrtc-sys v0.3.33 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys)\n Compiling hyper-rustls v0.27.9\n Compiling num-bigint-dig v0.8.6\n Compiling rand_distr v0.4.3\n Compiling tungstenite v0.29.0\n Compiling nokhwa-bindings-macos v0.2.4\n Compiling nokhwa-bindings-linux v0.1.4\n Compiling nokhwa-bindings-windows v0.4.6\n Compiling liquid v0.26.8\n Compiling tract-data v0.19.16\n Compiling rsa v0.9.10\n Compiling tract-linalg v0.19.16 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/vendor/tract-linalg-0.19.16)\n Compiling tokio-tungstenite v0.29.0\n Compiling prost v0.14.4\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:66:19\n |\n66 | self.cc().static_flag(true).file(filename).try_compile(\"dummy\").is_ok()\n | ^^^^^^^^^^^\n |\n = note: `#[warn(deprecated)]` on by default\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:121:26\n |\n121 | .static_flag(true)\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:133:61\n |\n133 | cc::Build::new().files(files).flag(\"-mfma\").static_flag(true).compile(\"x86_64_fma\");\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:142:18\n |\n142 | .static_flag(true)\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:154:18\n |\n154 | .static_flag(true)\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:164:43\n |\n164 | cc::Build::new().files(files).static_flag(true).compile(\"arm64simd\");\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:174:42\n |\n174 | config.cc().files(files).static_flag(true).compile(\"arm64fp16\")\n | ^^^^^^^^^^^\n\n Compiling tract-data v0.23.1\n Compiling jsonwebtoken v10.4.0\nwarning: `tract-linalg` (build script) generated 7 warnings\n Compiling nokhwa v0.10.11\n Compiling livekit-datatrack v0.1.8\nwarning: tract-linalg@0.19.16: arm64/arm64fp16/dummy_fmla_no_pragma.S: Assembler messages:\nwarning: tract-linalg@0.19.16: arm64/arm64fp16/dummy_fmla_no_pragma.S:11: Error: selected processor does not support `fmla v16.8h,v0.8h,v8.h[0]'\n Compiling livekit-api v0.5.0\nwarning: unexpected `cfg` condition value: `armv7`\n --> vendor/tract-linalg-0.19.16/src/lib.rs:26:32\n |\n26 | #[cfg(any(target_arch = \"arm\", target_arch = \"armv7\"))]\n | ^^^^^^^^^^^^^^^^^^^^^\n |\n = note: expected values for `target_arch` are: `aarch64`, `amdgpu`, `arm`, `arm64ec`, `avr`, `bpf`, `csky`, `hexagon`, `loongarch32`, `loongarch64`, `m68k`, `mips`, `mips32r6`, `mips64`, `mips64r6`, `msp430`, `nvptx64`, `powerpc`, `powerpc64`, `riscv32`, `riscv64`, `s390x`, `sparc`, `sparc64`, `wasm32`, `wasm64`, `x86`, `x86_64`, and `xtensa`\n = note: see for more information about checking conditional configuration\n = note: `#[warn(unexpected_cfgs)]` on by default\n\nwarning: unexpected `cfg` condition name: `feature_cpu`\n --> vendor/tract-linalg-0.19.16/src/arm64.rs:40:10\n |\n40 | cfg!(feature_cpu = \"fp16\")\n | ^^^^^^^^^^^^^^^^^^^^\n |\n = help: expected names are: `docsrs`, `feature`, and `test` and 31 more\n = help: consider using a Cargo feature instead\n = help: or consider adding in `Cargo.toml` the `check-cfg` lint config for the lint:\n [lints.rust]\n unexpected_cfgs = { level = \"warn\", check-cfg = ['cfg(feature_cpu, values(\"fp16\"))'] }\n = help: or consider adding `println!(\"cargo::rustc-check-cfg=cfg(feature_cpu, values(\\\"fp16\\\"))\");` to the top of the `build.rs`\n = note: see for more information about checking conditional configuration\n\nwarning: unexpected `cfg` condition value: `armv7`\n --> vendor/tract-linalg-0.19.16/src/lib.rs:134:36\n |\n134 | #[cfg(any(target_arch = \"arm\", target_arch = \"armv7\"))]\n | ^^^^^^^^^^^^^^^^^^^^^\n |\n = note: expected values for `target_arch` are: `aarch64`, `amdgpu`, `arm`, `arm64ec`, `avr`, `bpf`, `csky`, `hexagon`, `loongarch32`, `loongarch64`, `m68k`, `mips`, `mips32r6`, `mips64`, `mips64r6`, `msp430`, `nvptx64`, `powerpc`, `powerpc64`, `riscv32`, `riscv64`, `s390x`, `sparc`, `sparc64`, `wasm32`, `wasm64`, `x86`, `x86_64`, and `xtensa`\n = note: see for more information about checking conditional configuration\n\nwarning: hiding a lifetime that's elided elsewhere is confusing\n --> vendor/tract-linalg-0.19.16/src/generic/rounding.rs:37:26\n |\n37 | pub fn as_fused_spec(&self) -> FusedSpec {\n | ^^^^^ ^^^^^^^^^ the same lifetime is hidden here\n | |\n | the lifetime is elided here\n |\n = help: the same lifetime is referred to in inconsistent ways, making the signature confusing\n = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default\nhelp: use `'_` for type paths\n |\n37 | pub fn as_fused_spec(&self) -> FusedSpec<'_> {\n | ++++\n\n Compiling tract-core v0.19.16\nwarning: `tract-linalg` (lib) generated 4 warnings (run `cargo fix --lib -p tract-linalg` to apply 1 suggestion)\nwarning: tract-linalg@0.23.1: arm64/arm64fp16/dummy_fmla_no_pragma.S: Assembler messages:\nwarning: tract-linalg@0.23.1: arm64/arm64fp16/dummy_fmla_no_pragma.S:11: Error: selected processor does not support `fmla v16.8h,v0.8h,v8.h[0]'\n Compiling tract-core v0.23.1\n Compiling tract-nnef v0.19.16\n Compiling tract-hir v0.19.16\nwarning: webrtc-sys@0.3.33: EGL headers not found; building without NVIDIA hardware accelerated video codec support\n Compiling libwebrtc v0.3.35 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/vendor/libwebrtc)\nwarning: variable does not need to be mutable\n --> vendor/libwebrtc/src/native/desktop_capturer.rs:66:13\n |\n66 | let mut sys_handle = sys_dc::ffi::DesktopCapturerOptions {\n | ----^^^^^^^^^^\n | |\n | help: remove this `mut`\n |\n = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default\n\n Compiling tract-pulse-opl v0.19.16\n Compiling tract-onnx-opl v0.19.16\n Compiling tract-pulse v0.19.16\n Compiling tract-onnx v0.19.16\n Compiling livekit v0.7.43\nwarning: `libwebrtc` (lib) generated 1 warning (run `cargo fix --lib -p libwebrtc` to apply 1 suggestion)\n Compiling deep_filter v0.5.6 (https://github.com/Rikorose/DeepFilterNet?rev=978576aa8400552a4ce9730838c635aa30db5e61#978576aa)\n Compiling tract-nnef v0.23.1\n Compiling tract-hir v0.23.1\n Compiling tract-transformers v0.23.1\n Compiling tract-pulse-opl v0.23.1\n Compiling tract-pulse v0.23.1\n Compiling tract-extra v0.23.1\n Compiling tract-onnx-opl v0.23.1\n Compiling tract-onnx v0.23.1\n Compiling fluxer_webrtc_sender v0.0.0 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender)\nwarning: fields `num_frames` and `timestamp_us` are never read\n --> src/engine.rs:279:5\n |\n277 | struct ScreenAudioChunk {\n | ---------------- fields in this struct\n278 | samples: Vec,\n279 | num_frames: u32,\n | ^^^^^^^^^^\n...\n282 | timestamp_us: i64,\n | ^^^^^^^^^^^^\n |\n = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\nwarning: `fluxer_webrtc_sender` (lib) generated 1 warning\n Finished `release` profile [optimized] target(s) in 1m 01s\n" - }, - { - "name": "webrtc-sender-tests", - "command": "pnpm --dir native/webrtc-sender test", - "category": "native-sender", - "status": "fail", - "code": 1, - "signal": null, - "startedAt": "2026-06-14T18:10:34.871Z", - "endedAt": "2026-06-14T18:11:18.779Z", - "durationMs": 43908, - "stdoutTail": "\n> @fluxer/webrtc-sender@0.0.0 test /workspaces/fluxer/fluxer_desktop/native/webrtc-sender\n> cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- test-webrtc-sender-rust && node --test index.test.mjs scripts/livekit-harness.test.mjs\n\n+ /home/vscode/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/cargo test --features \"publisher,camera-native\"\n ELIFECYCLE  Test failed. See above for more details.\n WARN  Local package.json exists, but node_modules missing, did you mean to install?\n", - "stderrTail": " Compiling cfg-if v1.0.4\n Compiling memchr v2.8.1\n Compiling bitflags v2.13.0\n Compiling bytes v1.11.1\n Compiling once_cell v1.21.4\n Compiling itoa v1.0.18\n Compiling regex-syntax v0.8.10\n Compiling pin-project-lite v0.2.17\n Compiling equivalent v1.0.2\n Compiling typenum v1.20.1\n Compiling log v0.4.30\n Compiling adler2 v2.0.1\n Compiling libm v0.2.16\n Compiling serde_core v1.0.228\n Compiling either v1.16.0\n Compiling linux-raw-sys v0.12.1\n Compiling same-file v1.0.6\n Compiling futures-io v0.3.32\n Compiling slab v0.4.12\n Compiling futures-task v0.3.32\n Compiling rawpointer v0.2.1\n Compiling zerocopy v0.8.50\n Compiling zeroize v1.8.2\n Compiling zerofrom v0.1.8\n Compiling ring v0.17.14\n Compiling anyhow v1.0.102\n Compiling byteorder v1.5.0\n Compiling simd-adler32 v0.3.9\n Compiling aho-corasick v1.1.4\n Compiling getrandom v0.2.17\n Compiling crc32fast v1.5.0\n Compiling matrixmultiply v0.3.10\n Compiling http v1.4.1\n Compiling strength_reduce v0.2.4\n Compiling maplit v1.0.2\n Compiling indexmap v2.14.0\n Compiling dyn-clone v1.0.20\n Compiling minimal-lexical v0.2.1\n Compiling pest v2.8.6\n Compiling tokio v1.52.3\n Compiling parking_lot_core v0.9.12\n Compiling walkdir v2.5.0\n Compiling num-traits v0.2.19\n Compiling zmij v1.0.21\n Compiling ppv-lite86 v0.2.21\n Compiling serde v1.0.228\n Compiling yoke v0.8.2\n Compiling rustix v1.1.4\n Compiling generic-array v0.14.9\n Compiling rand_core v0.6.4\n Compiling parking_lot v0.12.5\n Compiling serde_json v1.0.150\n Compiling num-integer v0.1.46\n Compiling half v2.7.1\n Compiling der v0.7.10\n Compiling futures-util v0.3.32\n Compiling regex-automata v0.4.14\n Compiling http-body v1.0.1\n Compiling time v0.3.47\n Compiling tracing-core v0.1.36\n Compiling filetime v0.2.29\n Compiling socket2 v0.6.4\n Compiling pest_meta v2.8.6\n Compiling mio v1.2.1\n Compiling itertools v0.13.0\n Compiling memo-map v0.3.3\n Compiling allocator-api2 v0.2.21\n Compiling miniz_oxide v0.8.9\n Compiling thiserror v2.0.18\n Compiling zerovec v0.11.6\n Compiling zerotrie v0.2.4\n Compiling primal-check v0.3.4\n Compiling transpose v0.2.3\n Compiling crypto-common v0.1.6\n Compiling block-buffer v0.10.4\n Compiling num-complex v0.4.6\n Compiling flate2 v1.1.9\n Compiling pest_generator v2.8.6\n Compiling xattr v1.6.1\n Compiling regex v1.12.3\n Compiling spki v0.7.3\n Compiling tracing v0.1.44\n Compiling hashbrown v0.16.1\n Compiling minijinja v2.20.0\n Compiling ahash v0.7.8\n Compiling nom v8.0.0\n Compiling itertools v0.14.0\n Compiling dyn-eq v0.1.3\n Compiling downcast-rs v2.0.2\n Compiling num-bigint v0.4.6\n Compiling dyn-hash v1.0.0\n Compiling crossbeam-utils v0.8.21\n Compiling tar v0.4.46\n Compiling typeid v1.0.3\n Compiling rand_chacha v0.3.1\n Compiling digest v0.10.7\n Compiling prost v0.12.6\n Compiling nom v7.1.3\n Compiling rustls-pki-types v1.14.1\n Compiling tinystr v0.8.3\n Compiling potential_utf v0.1.5\n Compiling scan_fmt v0.2.6\n Compiling hashbrown v0.11.2\n Compiling string-interner v0.20.0\n Compiling nom-language v0.1.0\n Compiling itertools v0.10.5\n Compiling rustfft v6.4.1\n Compiling pest_derive v2.8.6\n Compiling ndarray v0.17.2\n Compiling ndarray v0.15.6\n Compiling bit-vec v0.9.1\n Compiling pkcs8 v0.10.2\n Compiling hyper v1.10.1\n Compiling enum-ordinalize v3.1.15\n Compiling icu_locale_core v2.2.0\n Compiling icu_collections v2.2.0\n Compiling bit-set v0.10.0\n Compiling erased-serde v0.4.10\n Compiling getrandom v0.4.2\n Compiling string-interner v0.14.0\n Compiling prost-types v0.12.6\n Compiling http-body-util v0.1.3\n Compiling ff v0.13.1\n Compiling libloading v0.8.9\n Compiling rand v0.8.6\n Compiling hmac v0.12.1\n Compiling signature v2.2.0\n Compiling downcast-rs v1.2.1\n Compiling rustls v0.23.40\n Compiling anymap3 v1.0.1\n Compiling bit-vec v0.6.3\n Compiling inventory v0.3.24\n Compiling petgraph v0.6.5\n Compiling sec1 v0.7.3\n Compiling hkdf v0.12.4\n Compiling tract-data v0.23.1\n Compiling hyper-util v0.1.20\n Compiling rustix v0.38.44\n Compiling liquid-core v0.26.8\n Compiling crossbeam-epoch v0.9.18\n Compiling group v0.13.0\n Compiling getrandom v0.3.4\n Compiling tempfile v3.27.0\n Compiling tower v0.5.3\n Compiling crypto-bigint v0.5.5\n Compiling educe v0.4.23\n Compiling icu_provider v2.2.0\n Compiling tract-linalg v0.23.1 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/vendor/tract-linalg-0.23.1)\n Compiling bit-set v0.5.3\n Compiling rustls-webpki v0.103.13\n Compiling pbjson-build v0.6.2\n Compiling clang-sys v1.8.1\n Compiling safetensors v0.7.0\n Compiling serde_urlencoded v0.7.1\n Compiling liquid-lib v0.26.8\n Compiling crossbeam-deque v0.8.6\n Compiling elliptic-curve v0.13.8\n Compiling prost-build v0.12.6\n Compiling which v4.4.2\n Compiling rand_core v0.9.5\n Compiling fdeflate v0.3.7\n Compiling quick-error v2.0.1\n Compiling icu_properties v2.2.0\n Compiling icu_normalizer v2.2.0\n Compiling zune-core v0.5.1\n Compiling color_quant v1.1.0\n Compiling pxfm v0.1.29\n Compiling weezl v0.1.12\n Compiling byteorder-lite v0.1.0\n Compiling rand_chacha v0.9.0\n Compiling liquid v0.26.8\n Compiling tract-data v0.19.16\n Compiling cexpr v0.6.0\n Compiling rayon-core v1.13.0\n Compiling mozjpeg-sys v2.2.3\n Compiling rfc6979 v0.4.0\n Compiling png v0.18.1\n Compiling sha2 v0.10.9\n Compiling zune-jpeg v0.5.15\n Compiling zip v0.6.6\n Compiling tokio-util v0.7.18\n Compiling rand_core v0.10.1\n Compiling chrono v0.4.44\n Compiling pbjson v0.6.0\n Compiling idna_adapter v1.2.2\n Compiling image-webp v0.2.4\n Compiling moxcms v0.8.1\n Compiling bindgen v0.65.1\n Compiling gif v0.14.2\n Compiling tokio-rustls v0.26.4\n Compiling tract-linalg v0.19.16 (/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/vendor/tract-linalg-0.19.16)\n Compiling pbjson-types v0.6.0\n Compiling rand v0.9.4\n Compiling mozjpeg v0.10.13\n Compiling rayon v1.12.0\n Compiling const-random-macro v0.1.16\n Compiling ecdsa v0.16.9\n Compiling cxx-build v1.0.194\n Compiling primeorder v0.13.6\n Compiling idna v1.1.0\n Compiling rustls-native-certs v0.8.3\n Compiling futures-executor v0.3.32\n Compiling tokio-stream v0.1.18\n Compiling num-iter v0.1.45\n Compiling hyper-rustls v0.27.9\n Compiling toml_edit v0.25.12+spec-1.1.0\n Compiling hybrid-array v0.4.12\n Compiling float-ord v0.3.2\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:66:19\n |\n66 | self.cc().static_flag(true).file(filename).try_compile(\"dummy\").is_ok()\n | ^^^^^^^^^^^\n |\n = note: `#[warn(deprecated)]` on by default\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:121:26\n |\n121 | .static_flag(true)\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:133:61\n |\n133 | cc::Build::new().files(files).flag(\"-mfma\").static_flag(true).compile(\"x86_64_fma\");\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:142:18\n |\n142 | .static_flag(true)\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:154:18\n |\n154 | .static_flag(true)\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:164:43\n |\n164 | cc::Build::new().files(files).static_flag(true).compile(\"arm64simd\");\n | ^^^^^^^^^^^\n\nwarning: use of deprecated method `cc::Build::static_flag`: cc only creates static libraries, setting this does nothing\n --> vendor/tract-linalg-0.19.16/build.rs:174:42\n |\n174 | config.cc().files(files).static_flag(true).compile(\"arm64fp16\")\n | ^^^^^^^^^^^\n\n Compiling image v0.25.10\n Compiling chacha20 v0.10.0\n Compiling curve25519-dalek v4.1.3\nwarning: `tract-linalg` (build script) generated 7 warnings\n Compiling link-cplusplus v1.0.12\n Compiling ed25519 v2.2.3\n Compiling pkcs1 v0.7.5\n Compiling sha1 v0.10.6\n Compiling cxxbridge-macro v1.0.194\n Compiling p256 v0.13.2\nerror: extern location for tokio_rustls does not exist: /workspaces/fluxer/fluxer_desktop/native/webrtc-sender/target/debug/deps/libtokio_rustls-d06ee36b16f159df.rmeta\n --> /home/vscode/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-rustls-0.27.9/src/stream.rs:11:5\n |\n11 | use tokio_rustls::client::TlsStream;\n | ^^^^^^^^^^^^\n\nerror: could not compile `hyper-rustls` (lib) due to 1 previous error\nwarning: build failed, waiting for other jobs to finish...\nerror[E0786]: found invalid metadata files for crate `moxcms`\n --> /home/vscode/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/image-0.25.10/src/metadata/cicp.rs:95:13\n |\n95 | use moxcms::CicpColorPrimaries as M;\n | ^^^^^^\n |\n = note: failed to open rmeta metadata: '/workspaces/fluxer/fluxer_desktop/native/webrtc-sender/target/debug/deps/libmoxcms-7c20aa5b2d449552.rmeta'\n\nFor more information about this error, try `rustc --explain E0786`.\nerror: could not compile `image` (lib) due to 1 previous error\nerror: extern location for chrono does not exist: /workspaces/fluxer/fluxer_desktop/native/webrtc-sender/target/debug/deps/libchrono-2cf7dcc5247f6ef8.rmeta\n --> /home/vscode/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pbjson-types-0.6.0/src/timestamp.rs:2:5\n |\n2 | use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};\n | ^^^^^^\n\nerror: could not compile `pbjson-types` (lib) due to 1 previous error\nwarning: tract-linalg@0.19.16: arm64/arm64fp16/dummy_fmla_no_pragma.S: Assembler messages:\nwarning: tract-linalg@0.19.16: arm64/arm64fp16/dummy_fmla_no_pragma.S:11: Error: selected processor does not support `fmla v16.8h,v0.8h,v8.h[0]'\nwarning: tract-linalg@0.23.1: arm64/arm64fp16/dummy_fmla_no_pragma.S: Assembler messages:\nwarning: tract-linalg@0.23.1: arm64/arm64fp16/dummy_fmla_no_pragma.S:11: Error: selected processor does not support `fmla v16.8h,v0.8h,v8.h[0]'\nCommand failed with exit code 101: /home/vscode/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/cargo test --features \"publisher,camera-native\"\n" - } - ] -} diff --git a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-h264-20260605T204959Z.json b/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-h264-20260605T204959Z.json deleted file mode 100644 index 1dffc0d69..000000000 --- a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-h264-20260605T204959Z.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:50:00.568Z", - "endedAt": "2026-06-05T20:50:17.383Z", - "platform": "linux", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-92677-28ffd7a4-19ee-49cf-87d1-e31ab80864f4", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-92677", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-92677", - "subscriberIdentity": "fluxer-native-subscriber-92677", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/home/hampus/Development/fluxer/fluxer_desktop/native-media-reports/cachyos-nvidia-livekit-720p-h264-20260605T204959Z.json", - "verbose": false, - "scenarioName": "codec-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 465, - "strictAudioFrames": 0, - "strictVideoFrames": 435, - "videoCallbacks": 463, - "videoBytes": 640051200, - "receivedFps": 28.99, - "requiredFps": 27, - "maxVideoFrameGapMs": 68, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 68, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 435 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780692601312, - "stoppedAtMs": null, - "framesPushed": 483, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 37, - "lastPushAtMs": 1780692617378, - "elapsedMs": 16227, - "producedFps": 29.77 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780692602380, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-92677", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VStBVT57Q9oKAa", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-92677", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_89hNCmrznA6A", - "trackSid": "TR_VStBVT57Q9oKAa", - "width": 1280, - "height": 720, - "timestampUs": 68521962000 - }, - "publisherStats": { - "rttMs": 4, - "outbound": [ - { - "trackSid": "TR_VStBVT57Q9oKAa", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 386.9, - "packetsLost": 0, - "fps": 30, - "codec": "video/H264" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 476, - "outgoingVideoFramesAccepted": 476, - "outgoingVideoFramesDropped": 16, - "outgoingVideoFramesCoalesced": 16, - "outgoingVideoFramesCaptured": 460, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.97, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 5, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_89hNCmrznA6A", - "trackSid": "TR_VStBVT57Q9oKAa", - "kind": "video", - "bitrateKbps": 386.4, - "packetsLost": 0, - "jitterMs": 2, - "codec": "video/H264" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-hevc-20260605T205028Z.json b/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-hevc-20260605T205028Z.json deleted file mode 100644 index 0dc00fdaf..000000000 --- a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-hevc-20260605T205028Z.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:50:29.534Z", - "endedAt": "2026-06-05T20:52:00.423Z", - "platform": "linux", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-92838-08153292-483f-4acb-b470-af9847d7597c", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-92838", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-92838", - "subscriberIdentity": "fluxer-native-subscriber-92838", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["hevc"], - "expectedScreenCodecs": ["hevc"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/home/hampus/Development/fluxer/fluxer_desktop/native-media-reports/cachyos-nvidia-livekit-720p-hevc-20260605T205028Z.json", - "verbose": false, - "scenarioName": "codec-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": false - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": false - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: timed out after 90000ms waiting for: publisher outbound screenshare codec video/H265, subscriber inbound video codec video/H265, server sees publisher screenshare publication\n at waitForHarnessSuccess (file:///home/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1329:8)\n at async runHarness (file:///home/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1926:18)\n at async main (file:///home/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:2026:18)\n at async file:///home/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:2059:19", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 2611, - "strictAudioFrames": 0, - "strictVideoFrames": 2610, - "videoCallbacks": 2610, - "videoBytes": 3608064000, - "receivedFps": 29.02, - "requiredFps": 27, - "maxVideoFrameGapMs": 81, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 81, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 2610 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780692630259, - "stoppedAtMs": null, - "framesPushed": 2705, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 37, - "lastPushAtMs": 1780692720392, - "elapsedMs": 90321, - "producedFps": 29.95 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780692720171, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-92838", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-92838", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSr7CavPmttCck", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_eQVNaBPkxUPX", - "trackSid": "TR_VSr7CavPmttCck", - "width": 1280, - "height": 720, - "timestampUs": 68625011000 - }, - "publisherStats": { - "rttMs": 6, - "outbound": [ - { - "trackSid": "TR_VSr7CavPmttCck", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 44.7, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 2697, - "outgoingVideoFramesAccepted": 2697, - "outgoingVideoFramesDropped": 91, - "outgoingVideoFramesCoalesced": 91, - "outgoingVideoFramesCaptured": 2605, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.98, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 6, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_eQVNaBPkxUPX", - "trackSid": "TR_VSr7CavPmttCck", - "kind": "video", - "bitrateKbps": 43.1, - "packetsLost": 0, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-hevc-failfast-20260605T205643Z.json b/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-hevc-failfast-20260605T205643Z.json deleted file mode 100644 index ca517d747..000000000 --- a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-720p-hevc-failfast-20260605T205643Z.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:56:44.980Z", - "endedAt": "2026-06-05T20:56:45.769Z", - "platform": "linux", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-94317-ce85bdff-9476-465a-9f9a-fae49f6c168d", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-94317", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-94317", - "subscriberIdentity": "fluxer-native-subscriber-94317", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["hevc"], - "expectedScreenCodecs": ["hevc"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/home/hampus/Development/fluxer/fluxer_desktop/native-media-reports/cachyos-nvidia-livekit-720p-hevc-failfast-20260605T205643Z.json", - "verbose": false, - "scenarioName": "codec-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": false - }, - { - "name": "subscriber video resolution 1280x720", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": false - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": false - }, - { - "name": "server publishing API participants listed", - "pass": false - }, - { - "name": "server sees publisher screenshare publication", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: H.265 publishing requires hardware encoder support; missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 27, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": null, - "publisherStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-alltracks-serverpub-mixed-20260605T205705Z.json b/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-alltracks-serverpub-mixed-20260605T205705Z.json deleted file mode 100644 index 033a450d8..000000000 --- a/fluxer_desktop/native-media-reports/cachyos/cachyos-nvidia-livekit-alltracks-serverpub-mixed-20260605T205705Z.json +++ /dev/null @@ -1,637 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:57:06.276Z", - "endedAt": "2026-06-05T20:57:28.255Z", - "platform": "linux", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-94553-a59031d7-277a-4d31-a290-59cea2c79f1c", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-94553", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-94553", - "subscriberIdentity": "fluxer-native-subscriber-94553", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 20000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": true, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": true, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/home/hampus/Development/fluxer/fluxer_desktop/native-media-reports/cachyos-nvidia-livekit-alltracks-serverpub-mixed-20260605T205705Z.json", - "verbose": false, - "scenarioName": "codec-h264-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 8352, - "inboundVideoFrames": 1182, - "strictAudioFrames": 200, - "strictVideoFrames": 1164, - "videoCallbacks": 1180, - "videoBytes": 1631232000, - "receivedFps": 58.16, - "requiredFps": 27, - "maxVideoFrameGapMs": 98, - "maxAudioFrameGapMs": 101, - "maxVideoFrameGapOverallMs": 98, - "maxAudioFrameGapOverallMs": 101, - "avDriftMs": 36, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 1164 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780693027399, - "stoppedAtMs": null, - "framesPushed": 626, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 39, - "lastPushAtMs": 1780693048233, - "elapsedMs": 21015, - "producedFps": 29.79 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780693027400, - "stoppedAtMs": null, - "framesPushed": 626, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 40, - "lastPushAtMs": 1780693048233, - "elapsedMs": 21014, - "producedFps": 29.79 - }, - "serverPublishing": { - "fetchedAtMs": 1780693027997, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-94553", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AsXuPkpunfLw9E", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VStW6iBWUPJvYp", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - }, - { - "sid": "TR_AMraWSQbYVLoud", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-94553", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-2-94553", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSDUf6mLBmkksq", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMuo9vvgBhxrxp", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_Asf2caReF2dUoe", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_VSDUf6mLBmkksq", - "width": 1280, - "height": 720, - "timestampUs": 68952853000 - }, - "publisherStats": { - "rttMs": 15, - "outbound": [ - { - "trackSid": "TR_VStW6iBWUPJvYp", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 399.8, - "packetsLost": 0, - "fps": 29, - "codec": "video/H264" - }, - { - "trackSid": "TR_AsXuPkpunfLw9E", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 258.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMraWSQbYVLoud", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 97.8, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_AMuo9vvgBhxrxp", - "kind": "audio", - "bitrateKbps": 104.8, - "packetsLost": 0, - "jitterMs": 3, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_Asf2caReF2dUoe", - "kind": "audio", - "bitrateKbps": 274.6, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_VSDUf6mLBmkksq", - "kind": "video", - "bitrateKbps": 31.6, - "packetsLost": 0, - "jitterMs": 5, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 617, - "outgoingVideoFramesAccepted": 617, - "outgoingVideoFramesDropped": 19, - "outgoingVideoFramesCoalesced": 19, - "outgoingVideoFramesCaptured": 598, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 29.05, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 33, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 30, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 38, - "outbound": [ - { - "trackSid": "TR_Asf2caReF2dUoe", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 261.7, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSDUf6mLBmkksq", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 29.4, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMuo9vvgBhxrxp", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_mQQgkdVbS4Wi", - "trackSid": "TR_AsXuPkpunfLw9E", - "kind": "audio", - "bitrateKbps": 265.5, - "packetsLost": 0, - "jitterMs": 3, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_mQQgkdVbS4Wi", - "trackSid": "TR_VStW6iBWUPJvYp", - "kind": "video", - "bitrateKbps": 402.2, - "packetsLost": 0, - "jitterMs": 5, - "codec": "video/H264" - }, - { - "participantSid": "PA_mQQgkdVbS4Wi", - "trackSid": "TR_AMraWSQbYVLoud", - "kind": "audio", - "bitrateKbps": 100.3, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 618, - "outgoingVideoFramesAccepted": 618, - "outgoingVideoFramesDropped": 19, - "outgoingVideoFramesCoalesced": 19, - "outgoingVideoFramesCaptured": 599, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 29.07, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 33, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 30, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 6, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_AMuo9vvgBhxrxp", - "kind": "audio", - "bitrateKbps": 100.8, - "packetsLost": 0, - "jitterMs": 3, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_VSDUf6mLBmkksq", - "kind": "video", - "bitrateKbps": 29.4, - "packetsLost": 0, - "jitterMs": 5, - "codec": "video/VP8" - }, - { - "participantSid": "PA_sFVVyQMPfG8Z", - "trackSid": "TR_Asf2caReF2dUoe", - "kind": "audio", - "bitrateKbps": 271.7, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_mQQgkdVbS4Wi", - "trackSid": "TR_VStW6iBWUPJvYp", - "kind": "video", - "bitrateKbps": 399.8, - "packetsLost": 0, - "jitterMs": 5, - "codec": "video/H264" - }, - { - "participantSid": "PA_mQQgkdVbS4Wi", - "trackSid": "TR_AMraWSQbYVLoud", - "kind": "audio", - "bitrateKbps": 99.8, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_mQQgkdVbS4Wi", - "trackSid": "TR_AsXuPkpunfLw9E", - "kind": "audio", - "bitrateKbps": 263.9, - "packetsLost": 0, - "jitterMs": 3, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-noadaptive-20260605T204148Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-noadaptive-20260605T204148Z.json deleted file mode 100644 index 29032e666..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-noadaptive-20260605T204148Z.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:41:48.766Z", - "endedAt": "2026-06-05T20:42:05.685Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99979-bebc8448-fca8-4318-84fb-debd59234bd3", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99979", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99979", - "subscriberIdentity": "fluxer-native-subscriber-99979", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.8, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": false, - "minVideoFps": 24, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": "videotoolbox", - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-noadaptive-20260605T204148Z.json", - "verbose": false, - "scenarioName": "codec-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "hardware encoder backend videotoolbox", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 787, - "strictAudioFrames": 0, - "strictVideoFrames": 783, - "videoCallbacks": 786, - "videoBytes": 9779097600, - "receivedFps": 52.46, - "requiredFps": 48, - "maxVideoFrameGapMs": 49, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 313, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 12, - "maxObservedPacketLossDelta": 11, - "videoResolutionCounts": { - "3840x2160": 783 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 60, - "startedAtMs": 1780692109289, - "stoppedAtMs": null, - "framesPushed": 982, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 44, - "lastPushAtMs": 1780692125674, - "elapsedMs": 16404, - "producedFps": 59.86 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780692110403, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-99979", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-99979", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VShAhgz9DukUav", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_7vLduTJN79oF", - "trackSid": "TR_VShAhgz9DukUav", - "width": 3840, - "height": 2160, - "timestampUs": 246972075000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VShAhgz9DukUav", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 226, - "packetsLost": 0, - "fps": 53, - "codec": "video/H264" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 943, - "outgoingVideoFramesAccepted": 943, - "outgoingVideoFramesDropped": 80, - "outgoingVideoFramesCoalesced": 80, - "outgoingVideoFramesCaptured": 862, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 54.78, - "outgoingVideoTargetFps": 60, - "outgoingVideoMaxQueueAgeMs": 18, - "outgoingVideoMaxPushLatencyMs": 18, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "adaptiveDisabled" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_7vLduTJN79oF", - "trackSid": "TR_VShAhgz9DukUav", - "kind": "video", - "bitrateKbps": 226.1, - "packetsLost": 12, - "jitterMs": 4, - "codec": "video/H264" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-restarted-20260605T204256Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-restarted-20260605T204256Z.json deleted file mode 100644 index f48e604e0..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-restarted-20260605T204256Z.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:42:57.137Z", - "endedAt": "2026-06-05T20:43:14.930Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-215-d2420544-1d14-499d-b7ed-39246026eeba", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-215", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-215", - "subscriberIdentity": "fluxer-native-subscriber-215", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.8, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": false, - "minVideoFps": 24, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": "videotoolbox", - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h264-restarted-20260605T204256Z.json", - "verbose": false, - "scenarioName": "codec-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "hardware encoder backend videotoolbox", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 835, - "strictAudioFrames": 0, - "strictVideoFrames": 780, - "videoCallbacks": 835, - "videoBytes": 10388736000, - "receivedFps": 51.9, - "requiredFps": 48, - "maxVideoFrameGapMs": 103, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 133, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 8, - "maxObservedPacketLossDelta": 2, - "videoResolutionCounts": { - "3840x2160": 780 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 60, - "startedAtMs": 1780692177759, - "stoppedAtMs": null, - "framesPushed": 1029, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 47, - "lastPushAtMs": 1780692194929, - "elapsedMs": 17180, - "producedFps": 59.9 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780692179914, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-215", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-215", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSt23d8AR7uRMx", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_ywoDZo9MfjAs", - "trackSid": "TR_VSt23d8AR7uRMx", - "width": 3840, - "height": 2160, - "timestampUs": 247041331000 - }, - "publisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_VSt23d8AR7uRMx", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 222, - "packetsLost": 0, - "fps": 52, - "codec": "video/H264" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 1000, - "outgoingVideoFramesAccepted": 1000, - "outgoingVideoFramesDropped": 87, - "outgoingVideoFramesCoalesced": 87, - "outgoingVideoFramesCaptured": 912, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 54.65, - "outgoingVideoTargetFps": 60, - "outgoingVideoMaxQueueAgeMs": 18, - "outgoingVideoMaxPushLatencyMs": 19, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "adaptiveDisabled" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_ywoDZo9MfjAs", - "trackSid": "TR_VSt23d8AR7uRMx", - "kind": "video", - "bitrateKbps": 221.7, - "packetsLost": 8, - "jitterMs": 4, - "codec": "video/H264" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-20260605T203835Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-20260605T203835Z.json deleted file mode 100644 index 70f4234a1..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-20260605T203835Z.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:38:35.984Z", - "endedAt": "2026-06-05T20:39:09.401Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99305-9b3ba47d-c306-44e8-93be-2f3f219d81d3", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99305", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99305", - "subscriberIdentity": "fluxer-native-subscriber-99305", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.8, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["hevc"], - "expectedScreenCodecs": ["hevc"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 24, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": "videotoolbox", - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-20260605T203835Z.json", - "verbose": false, - "scenarioName": "codec-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": true - }, - { - "name": "hardware encoder backend videotoolbox", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 29.9 below 48", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 911, - "strictAudioFrames": 0, - "strictVideoFrames": 898, - "videoCallbacks": 911, - "videoBytes": 11334297600, - "receivedFps": 29.9, - "requiredFps": 48, - "maxVideoFrameGapMs": 124, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 124, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 4, - "maxObservedPacketLossDelta": 4, - "videoResolutionCounts": { - "3840x2160": 898 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 60, - "startedAtMs": 1780691916459, - "stoppedAtMs": null, - "framesPushed": 1977, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 35, - "lastPushAtMs": 1780691949392, - "elapsedMs": 32950, - "producedFps": 60 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780691919120, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-99305", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-99305", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSVHTFzX6KKP2d", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H265", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_p6AhRM2ZweHm", - "trackSid": "TR_VSVHTFzX6KKP2d", - "width": 3840, - "height": 2160, - "timestampUs": 246795792000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSVHTFzX6KKP2d", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 80.1, - "packetsLost": 0, - "fps": 29, - "codec": "video/H265" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 1975, - "outgoingVideoFramesAccepted": 1975, - "outgoingVideoFramesDropped": 906, - "outgoingVideoFramesCoalesced": 906, - "outgoingVideoFramesCaptured": 1068, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 32.44, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 18, - "outgoingVideoMaxPushLatencyMs": 18, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "fps30", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_p6AhRM2ZweHm", - "trackSid": "TR_VSVHTFzX6KKP2d", - "kind": "video", - "bitrateKbps": 83, - "packetsLost": 4, - "jitterMs": 2, - "codec": "video/H265" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-noadaptive-20260605T204048Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-noadaptive-20260605T204048Z.json deleted file mode 100644 index c2e01fb7b..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-noadaptive-20260605T204048Z.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:40:48.428Z", - "endedAt": "2026-06-05T20:41:21.891Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99799-9b73c5f8-24ee-46a6-b641-2268cab488bf", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99799", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99799", - "subscriberIdentity": "fluxer-native-subscriber-99799", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.8, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["hevc"], - "expectedScreenCodecs": ["hevc"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": false, - "minVideoFps": 24, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": "videotoolbox", - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-h265-noadaptive-20260605T204048Z.json", - "verbose": false, - "scenarioName": "codec-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": true - }, - { - "name": "hardware encoder backend videotoolbox", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 47.42 below 48", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 1439, - "strictAudioFrames": 0, - "strictVideoFrames": 1424, - "videoCallbacks": 1438, - "videoBytes": 17891020800, - "receivedFps": 47.42, - "requiredFps": 48, - "maxVideoFrameGapMs": 119, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 119, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 4, - "maxObservedPacketLossDelta": 4, - "videoResolutionCounts": { - "3840x2160": 1424 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 60, - "startedAtMs": 1780692048955, - "stoppedAtMs": null, - "framesPushed": 1977, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 27, - "lastPushAtMs": 1780692081889, - "elapsedMs": 32944, - "producedFps": 60.01 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780692051599, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-99799", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VS7whvguuwE7Ep", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H265", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-99799", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_6qPYCyPtKNMy", - "trackSid": "TR_VS7whvguuwE7Ep", - "width": 3840, - "height": 2160, - "timestampUs": 246928279000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VS7whvguuwE7Ep", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 132.9, - "packetsLost": 0, - "fps": 47, - "codec": "video/H265" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 1971, - "outgoingVideoFramesAccepted": 1971, - "outgoingVideoFramesDropped": 169, - "outgoingVideoFramesCoalesced": 169, - "outgoingVideoFramesCaptured": 1802, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 54.85, - "outgoingVideoTargetFps": 60, - "outgoingVideoMaxQueueAgeMs": 18, - "outgoingVideoMaxPushLatencyMs": 18, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "adaptiveDisabled" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_6qPYCyPtKNMy", - "trackSid": "TR_VS7whvguuwE7Ep", - "kind": "video", - "bitrateKbps": 135.8, - "packetsLost": 4, - "jitterMs": 6, - "codec": "video/H265" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-vp8-noadaptive-20260605T204205Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-vp8-noadaptive-20260605T204205Z.json deleted file mode 100644 index b35a60ef3..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-vp8-noadaptive-20260605T204205Z.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:42:06.035Z", - "endedAt": "2026-06-05T20:42:22.648Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99996-9f3eea01-c794-44e7-a66d-a39e3b06f591", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99996", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99996", - "subscriberIdentity": "fluxer-native-subscriber-99996", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.8, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": false, - "minVideoFps": 24, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": "videotoolbox", - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-4k60-vp8-noadaptive-20260605T204205Z.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "hardware encoder backend videotoolbox", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 835, - "strictAudioFrames": 0, - "strictVideoFrames": 829, - "videoCallbacks": 835, - "videoBytes": 10388736000, - "receivedFps": 55.21, - "requiredFps": 48, - "maxVideoFrameGapMs": 138, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 138, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 18, - "maxObservedPacketLossDelta": 17, - "videoResolutionCounts": { - "3840x2160": 829 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 60, - "startedAtMs": 1780692126456, - "stoppedAtMs": null, - "framesPushed": 966, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 28, - "lastPushAtMs": 1780692142632, - "elapsedMs": 16199, - "producedFps": 59.63 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780692127628, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-99996", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSgayVkGgydiPJ", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-99996", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_CRYMSnWyNXJJ", - "trackSid": "TR_VSgayVkGgydiPJ", - "width": 3840, - "height": 2160, - "timestampUs": 246989049000 - }, - "publisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_VSgayVkGgydiPJ", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 347.3, - "packetsLost": 0, - "fps": 55, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 952, - "outgoingVideoFramesAccepted": 952, - "outgoingVideoFramesDropped": 83, - "outgoingVideoFramesCoalesced": 83, - "outgoingVideoFramesCaptured": 869, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 54.77, - "outgoingVideoTargetFps": 60, - "outgoingVideoMaxQueueAgeMs": 18, - "outgoingVideoMaxPushLatencyMs": 18, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "adaptiveDisabled" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_CRYMSnWyNXJJ", - "trackSid": "TR_VSgayVkGgydiPJ", - "kind": "video", - "bitrateKbps": 347.9, - "packetsLost": 18, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-720p-hevc-after-guard-20260605T205747Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-720p-hevc-after-guard-20260605T205747Z.json deleted file mode 100644 index 286970449..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-720p-hevc-after-guard-20260605T205747Z.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:57:47.344Z", - "endedAt": "2026-06-05T20:58:04.558Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-1177-7a01a114-afdb-422c-ac68-dd6da5106b46", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-1177", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-1177", - "subscriberIdentity": "fluxer-native-subscriber-1177", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["hevc"], - "expectedScreenCodecs": ["hevc"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": "videotoolbox", - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-720p-hevc-after-guard-20260605T205747Z.json", - "verbose": false, - "scenarioName": "codec-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": true - }, - { - "name": "hardware encoder backend videotoolbox", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 424, - "strictAudioFrames": 0, - "strictVideoFrames": 420, - "videoCallbacks": 423, - "videoBytes": 584755200, - "receivedFps": 28.28, - "requiredFps": 27, - "maxVideoFrameGapMs": 132, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 500, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 420 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780693067890, - "stoppedAtMs": null, - "framesPushed": 501, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 55, - "lastPushAtMs": 1780693084557, - "elapsedMs": 16678, - "producedFps": 30.04 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780693069535, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-1177", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-1177", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VScM2KL4qqaVMf", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H265", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_CEoLG8a8qXZr", - "trackSid": "TR_VScM2KL4qqaVMf", - "width": 1280, - "height": 720, - "timestampUs": 247930927000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VScM2KL4qqaVMf", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 31.2, - "packetsLost": 0, - "fps": 28, - "codec": "video/H265" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 477, - "outgoingVideoFramesAccepted": 477, - "outgoingVideoFramesDropped": 23, - "outgoingVideoFramesCoalesced": 23, - "outgoingVideoFramesCaptured": 454, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.53, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_CEoLG8a8qXZr", - "trackSid": "TR_VScM2KL4qqaVMf", - "kind": "video", - "bitrateKbps": 31.2, - "packetsLost": 0, - "jitterMs": 3, - "codec": "video/H265" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-20260605T204337Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-20260605T204337Z.json deleted file mode 100644 index 68455940e..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-20260605T204337Z.json +++ /dev/null @@ -1,636 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:43:38.058Z", - "endedAt": "2026-06-05T20:44:00.477Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-287-d177ae47-231a-4e5f-8b33-e21435c1d78c", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-287", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-287", - "subscriberIdentity": "fluxer-native-subscriber-287", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 20000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 120, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": true, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-20260605T204337Z.json", - "verbose": false, - "scenarioName": "codec-h264-with-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/H264", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 8688, - "inboundVideoFrames": 1105, - "strictAudioFrames": 200, - "strictVideoFrames": 1096, - "videoCallbacks": 1105, - "videoBytes": 1527552000, - "receivedFps": 55.07, - "requiredFps": 27, - "maxVideoFrameGapMs": 137, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 278, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 16, - "maxObservedPacketLoss": 193, - "maxObservedPacketLossDelta": 181, - "videoResolutionCounts": { - "1280x720": 1096 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780692218670, - "stoppedAtMs": null, - "framesPushed": 655, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 35, - "lastPushAtMs": 1780692240471, - "elapsedMs": 21825, - "producedFps": 30.01 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780692218671, - "stoppedAtMs": null, - "framesPushed": 655, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 35, - "lastPushAtMs": 1780692240471, - "elapsedMs": 21824, - "producedFps": 30.01 - }, - "serverPublishing": { - "fetchedAtMs": 1780692220222, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-287", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMQmi9tr3ijwD7", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsVx6mXCeyrqVB", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSjjPS7DRgdqSi", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-287", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-2-287", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSVinYCWjRrXgb", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - }, - { - "sid": "TR_AMd395vNaQvHas", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsSinL94mqRm57", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_VSVinYCWjRrXgb", - "width": 1280, - "height": 720, - "timestampUs": 247086842000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AsVx6mXCeyrqVB", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 263.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMQmi9tr3ijwD7", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.7, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSjjPS7DRgdqSi", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 38.3, - "packetsLost": 0, - "fps": 28, - "codec": "video/H264" - } - ], - "inbound": [ - { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_AsSinL94mqRm57", - "kind": "audio", - "bitrateKbps": 242.9, - "packetsLost": 78, - "jitterMs": 0, - "audioLevel": 0.319, - "codec": "audio/opus" - }, - { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_VSVinYCWjRrXgb", - "kind": "video", - "bitrateKbps": 39, - "packetsLost": 15, - "jitterMs": 2, - "codec": "video/H264" - }, - { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_AMd395vNaQvHas", - "kind": "audio", - "bitrateKbps": 92.8, - "packetsLost": 86, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 651, - "outgoingVideoFramesAccepted": 651, - "outgoingVideoFramesDropped": 27, - "outgoingVideoFramesCoalesced": 27, - "outgoingVideoFramesCaptured": 623, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.71, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 32, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_AsSinL94mqRm57", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 257.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMd395vNaQvHas", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 97.6, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSVinYCWjRrXgb", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 40.3, - "packetsLost": 0, - "fps": 29, - "codec": "video/H264" - } - ], - "inbound": [ - { - "participantSid": "PA_ecrQddbJEyhw", - "trackSid": "TR_AsVx6mXCeyrqVB", - "kind": "audio", - "bitrateKbps": 233.5, - "packetsLost": 94, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ecrQddbJEyhw", - "trackSid": "TR_VSjjPS7DRgdqSi", - "kind": "video", - "bitrateKbps": 39.6, - "packetsLost": 13, - "jitterMs": 2, - "codec": "video/H264" - }, - { - "participantSid": "PA_ecrQddbJEyhw", - "trackSid": "TR_AMQmi9tr3ijwD7", - "kind": "audio", - "bitrateKbps": 99.1, - "packetsLost": 86, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 653, - "outgoingVideoFramesAccepted": 653, - "outgoingVideoFramesDropped": 27, - "outgoingVideoFramesCoalesced": 27, - "outgoingVideoFramesCaptured": 625, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.71, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 32, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": null, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_AMd395vNaQvHas", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 33, - "jitterMs": 0, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_VSVinYCWjRrXgb", - "kind": "video", - "bitrateKbps": 39, - "packetsLost": 42, - "jitterMs": 2, - "codec": "video/H264" - }, - { - "participantSid": "PA_cdeic4ojSyhV", - "trackSid": "TR_AsSinL94mqRm57", - "kind": "audio", - "bitrateKbps": 253.1, - "packetsLost": 2, - "jitterMs": 0, - "audioLevel": 0.319, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ecrQddbJEyhw", - "trackSid": "TR_AsVx6mXCeyrqVB", - "kind": "audio", - "bitrateKbps": 253.3, - "packetsLost": 19, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ecrQddbJEyhw", - "trackSid": "TR_AMQmi9tr3ijwD7", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 3, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ecrQddbJEyhw", - "trackSid": "TR_VSjjPS7DRgdqSi", - "kind": "video", - "bitrateKbps": 38.4, - "packetsLost": 43, - "jitterMs": 2, - "codec": "video/H264" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-mixed-20260605T204504Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-mixed-20260605T204504Z.json deleted file mode 100644 index 8aad55f15..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-mixed-20260605T204504Z.json +++ /dev/null @@ -1,636 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T20:45:05.208Z", - "endedAt": "2026-06-05T20:45:28.695Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-421-04fac24c-55a5-4189-9d7a-5689875cf9b9", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-421", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-421", - "subscriberIdentity": "fluxer-native-subscriber-421", - "required": true, - "strict": true, - "timeoutMs": 90000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 20000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.9, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 120, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": true, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": true, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-alltracks-serverpub-mixed-20260605T204504Z.json", - "verbose": false, - "scenarioName": "codec-h264-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 9080, - "inboundVideoFrames": 1223, - "strictAudioFrames": 199, - "strictVideoFrames": 1137, - "videoCallbacks": 1222, - "videoBytes": 1689292800, - "receivedFps": 56.81, - "requiredFps": 27, - "maxVideoFrameGapMs": 99, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 100, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 92, - "maxObservedPacketLoss": 186, - "maxObservedPacketLossDelta": 163, - "videoResolutionCounts": { - "1280x720": 1137 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780692305878, - "stoppedAtMs": null, - "framesPushed": 685, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780692328677, - "elapsedMs": 22834, - "producedFps": 30 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780692305878, - "stoppedAtMs": null, - "framesPushed": 685, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780692328677, - "elapsedMs": 22834, - "producedFps": 30 - }, - "serverPublishing": { - "fetchedAtMs": 1780692308436, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-421", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VS73hbjzW6G7kV", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - }, - { - "sid": "TR_AMncny8z7koEGm", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_Aso75dfBWxWEyj", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-publisher-2-421", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AsxFsT7RDTdKQu", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSUZjTy2hrcZCc", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMuYbUnWUcAZxj", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-421", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_VS73hbjzW6G7kV", - "width": 1280, - "height": 720, - "timestampUs": 247175096000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VS73hbjzW6G7kV", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 38.6, - "packetsLost": 0, - "fps": 28, - "codec": "video/H264" - }, - { - "trackSid": "TR_Aso75dfBWxWEyj", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 261.1, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMncny8z7koEGm", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_9zvEJKeZq55z", - "trackSid": "TR_VSUZjTy2hrcZCc", - "kind": "video", - "bitrateKbps": 29.7, - "packetsLost": 37, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_9zvEJKeZq55z", - "trackSid": "TR_AsxFsT7RDTdKQu", - "kind": "audio", - "bitrateKbps": 249.7, - "packetsLost": 87, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_9zvEJKeZq55z", - "trackSid": "TR_AMuYbUnWUcAZxj", - "kind": "audio", - "bitrateKbps": 91.6, - "packetsLost": 62, - "jitterMs": 0, - "audioLevel": 0.321, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 683, - "outgoingVideoFramesAccepted": 683, - "outgoingVideoFramesDropped": 29, - "outgoingVideoFramesCoalesced": 29, - "outgoingVideoFramesCaptured": 654, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.71, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 30, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AMuYbUnWUcAZxj", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99.2, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AsxFsT7RDTdKQu", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 264.8, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSUZjTy2hrcZCc", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 29.7, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - } - ], - "inbound": [ - { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_VS73hbjzW6G7kV", - "kind": "video", - "bitrateKbps": 38.6, - "packetsLost": 1, - "jitterMs": 3, - "codec": "video/H264" - }, - { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_Aso75dfBWxWEyj", - "kind": "audio", - "bitrateKbps": 260.9, - "packetsLost": 4, - "jitterMs": 1, - "audioLevel": 0.318, - "codec": "audio/opus" - }, - { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_AMncny8z7koEGm", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 2, - "jitterMs": 1, - "audioLevel": 0.317, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 683, - "outgoingVideoFramesAccepted": 683, - "outgoingVideoFramesDropped": 29, - "outgoingVideoFramesCoalesced": 29, - "outgoingVideoFramesCaptured": 654, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.71, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 30, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_9zvEJKeZq55z", - "trackSid": "TR_VSUZjTy2hrcZCc", - "kind": "video", - "bitrateKbps": 30.1, - "packetsLost": 3, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_9zvEJKeZq55z", - "trackSid": "TR_AsxFsT7RDTdKQu", - "kind": "audio", - "bitrateKbps": 259.8, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.336, - "codec": "audio/opus" - }, - { - "participantSid": "PA_9zvEJKeZq55z", - "trackSid": "TR_AMuYbUnWUcAZxj", - "kind": "audio", - "bitrateKbps": 99, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.352, - "codec": "audio/opus" - }, - { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_AMncny8z7koEGm", - "kind": "audio", - "bitrateKbps": 95.5, - "packetsLost": 64, - "jitterMs": 1, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_VS73hbjzW6G7kV", - "kind": "video", - "bitrateKbps": 40, - "packetsLost": 27, - "jitterMs": 3, - "codec": "video/H264" - }, - { - "participantSid": "PA_J5CFmBPpkkyM", - "trackSid": "TR_Aso75dfBWxWEyj", - "kind": "audio", - "bitrateKbps": 244.3, - "packetsLost": 67, - "jitterMs": 0, - "audioLevel": 0.31, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-multipub-audio-nosim-20260605T203740Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-multipub-audio-nosim-20260605T203740Z.json deleted file mode 100644 index b76d07a2d..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-multipub-audio-nosim-20260605T203740Z.json +++ /dev/null @@ -1,636 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:37:40.480Z", - "endedAt": "2026-06-05T20:37:52.123Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99206-d8ecfb6b-5331-4b04-9ac0-0f510f626c7b", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99206", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99206", - "subscriberIdentity": "fluxer-native-subscriber-99206", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.94, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-multipub-audio-nosim-20260605T203740Z.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "A/V drift 86ms exceeds 80ms; observed packet loss delta 97 exceeds 0", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 4368, - "inboundVideoFrames": 630, - "strictAudioFrames": 99, - "strictVideoFrames": 575, - "videoCallbacks": 630, - "videoBytes": 870912000, - "receivedFps": 57.46, - "requiredFps": 28.2, - "maxVideoFrameGapMs": 139, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 139, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 86, - "maxObservedPacketLoss": 106, - "maxObservedPacketLossDelta": 97, - "videoResolutionCounts": { - "1280x720": 575 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691861125, - "stoppedAtMs": null, - "framesPushed": 330, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691872091, - "elapsedMs": 11091, - "producedFps": 29.75 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691861125, - "stoppedAtMs": null, - "framesPushed": 330, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691872091, - "elapsedMs": 11091, - "producedFps": 29.75 - }, - "serverPublishing": { - "fetchedAtMs": 1780691861869, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-99206", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-99206", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AsHmDRoRwwH2rj", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSFufssZ8C8oGR", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMofk8na2BK2zW", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-publisher-2-99206", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSjFwfYRyNg8nM", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMqoCF5pBKLLtZ", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsYeF5v2Lv8BFF", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_VSjFwfYRyNg8nM", - "width": 1280, - "height": 720, - "timestampUs": 246718524000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AMofk8na2BK2zW", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AsHmDRoRwwH2rj", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 260.4, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSFufssZ8C8oGR", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 27.5, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - } - ], - "inbound": [ - { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_AMqoCF5pBKLLtZ", - "kind": "audio", - "bitrateKbps": 94.4, - "packetsLost": 36, - "jitterMs": 0, - "audioLevel": 0.337, - "codec": "audio/opus" - }, - { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_VSjFwfYRyNg8nM", - "kind": "video", - "bitrateKbps": 29, - "packetsLost": 20, - "jitterMs": 4, - "codec": "video/VP8" - }, - { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_AsYeF5v2Lv8BFF", - "kind": "audio", - "bitrateKbps": 252.3, - "packetsLost": 36, - "jitterMs": 0, - "audioLevel": 0.342, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 322, - "outgoingVideoFramesAccepted": 322, - "outgoingVideoFramesDropped": 14, - "outgoingVideoFramesCoalesced": 14, - "outgoingVideoFramesCaptured": 308, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.68, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 33, - "outgoingVideoMaxPushLatencyMs": 33, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 28, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AsYeF5v2Lv8BFF", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 262.2, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSjFwfYRyNg8nM", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 28.9, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMqoCF5pBKLLtZ", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_ynXQqotkNFtm", - "trackSid": "TR_VSFufssZ8C8oGR", - "kind": "video", - "bitrateKbps": 26.6, - "packetsLost": 18, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_ynXQqotkNFtm", - "trackSid": "TR_AMofk8na2BK2zW", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 4, - "jitterMs": 1, - "audioLevel": 0.323, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ynXQqotkNFtm", - "trackSid": "TR_AsHmDRoRwwH2rj", - "kind": "audio", - "bitrateKbps": 260.6, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 322, - "outgoingVideoFramesAccepted": 322, - "outgoingVideoFramesDropped": 14, - "outgoingVideoFramesCoalesced": 14, - "outgoingVideoFramesCaptured": 308, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.68, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 33, - "outgoingVideoMaxPushLatencyMs": 33, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 28, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_AMqoCF5pBKLLtZ", - "kind": "audio", - "bitrateKbps": 96.1, - "packetsLost": 0, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_AsYeF5v2Lv8BFF", - "kind": "audio", - "bitrateKbps": 260.2, - "packetsLost": 2, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_mPCx5P6en7uF", - "trackSid": "TR_VSjFwfYRyNg8nM", - "kind": "video", - "bitrateKbps": 29.1, - "packetsLost": 7, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_ynXQqotkNFtm", - "trackSid": "TR_AMofk8na2BK2zW", - "kind": "audio", - "bitrateKbps": 92.7, - "packetsLost": 40, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ynXQqotkNFtm", - "trackSid": "TR_AsHmDRoRwwH2rj", - "kind": "audio", - "bitrateKbps": 231.2, - "packetsLost": 45, - "jitterMs": 0, - "audioLevel": 0.312, - "codec": "audio/opus" - }, - { - "participantSid": "PA_ynXQqotkNFtm", - "trackSid": "TR_VSFufssZ8C8oGR", - "kind": "video", - "bitrateKbps": 27.8, - "packetsLost": 12, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-20260605T202601Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-20260605T202601Z.json deleted file mode 100644 index 30f48b3ce..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-20260605T202601Z.json +++ /dev/null @@ -1,654 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:26:01.950Z", - "endedAt": "2026-06-05T20:26:13.710Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97788-e96dacf0-ab09-478e-8f70-cff476c920c4", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97788", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97788", - "subscriberIdentity": "fluxer-native-subscriber-97788", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-20260605T202601Z.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "observed packet loss 104 exceeds 0; observed 2 video frames outside 1280x720", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 4408, - "inboundVideoFrames": 581, - "strictAudioFrames": 100, - "strictVideoFrames": 554, - "videoCallbacks": 579, - "videoBytes": 796262400, - "receivedFps": 56.03, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 66, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 128, - "maxAudioFrameGapOverallMs": 102, - "avDriftMs": 13, - "maxObservedPacketLoss": 104, - "videoResolutionCounts": { - "640x360": 2, - "1280x720": 552 - }, - "videoResolutionMismatchCount": 2, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691162656, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 35, - "lastPushAtMs": 1780691173690, - "elapsedMs": 11153, - "producedFps": 29.77 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691162656, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 35, - "lastPushAtMs": 1780691173690, - "elapsedMs": 11153, - "producedFps": 29.77 - }, - "serverPublishing": { - "fetchedAtMs": 1780691163699, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-97788", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-2-97788", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMYDBcTLVPXW5R", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSSvARsPYMjnfH", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AseAEMuwQvUDUq", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-publisher-97788", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AsEpo39LZXJwgU", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AMZe28QCXUZLuv", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSVNYfqdKkzCqh", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_VSSvARsPYMjnfH", - "width": 1280, - "height": 720, - "timestampUs": 246020100000 - }, - "publisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_VSVNYfqdKkzCqh", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1.9, - "packetsLost": 0, - "fps": 3, - "codec": "video/VP8" - }, - { - "trackSid": "TR_VSVNYfqdKkzCqh", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 35.4, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMZe28QCXUZLuv", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AsEpo39LZXJwgU", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 261.7, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_VSSvARsPYMjnfH", - "kind": "video", - "bitrateKbps": 2.3, - "packetsLost": 4, - "jitterMs": 14, - "codec": "video/VP8" - }, - { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_AMYDBcTLVPXW5R", - "kind": "audio", - "bitrateKbps": 98.2, - "packetsLost": 2, - "jitterMs": 0, - "audioLevel": 0.332, - "codec": "audio/opus" - }, - { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_AseAEMuwQvUDUq", - "kind": "audio", - "bitrateKbps": 238, - "packetsLost": 44, - "jitterMs": 0, - "audioLevel": 0.309, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 326, - "outgoingVideoFramesAccepted": 326, - "outgoingVideoFramesDropped": 16, - "outgoingVideoFramesCoalesced": 16, - "outgoingVideoFramesCaptured": 309, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.49, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 33, - "outgoingVideoMaxPushLatencyMs": 33, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 46, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AseAEMuwQvUDUq", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 257.7, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSSvARsPYMjnfH", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 32.8, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - }, - { - "trackSid": "TR_VSSvARsPYMjnfH", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 2.3, - "packetsLost": 0, - "fps": 3, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMYDBcTLVPXW5R", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.7, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_t8raa7C57aBi", - "trackSid": "TR_AMZe28QCXUZLuv", - "kind": "audio", - "bitrateKbps": 96.9, - "packetsLost": 1, - "jitterMs": 1, - "audioLevel": 0.31, - "codec": "audio/opus" - }, - { - "participantSid": "PA_t8raa7C57aBi", - "trackSid": "TR_VSVNYfqdKkzCqh", - "kind": "video", - "bitrateKbps": 1.9, - "packetsLost": 1, - "jitterMs": 15, - "codec": "video/VP8" - }, - { - "participantSid": "PA_t8raa7C57aBi", - "trackSid": "TR_AsEpo39LZXJwgU", - "kind": "audio", - "bitrateKbps": 223.8, - "packetsLost": 56, - "jitterMs": 1, - "audioLevel": 0.336, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 323, - "outgoingVideoFramesAccepted": 323, - "outgoingVideoFramesDropped": 16, - "outgoingVideoFramesCoalesced": 16, - "outgoingVideoFramesCaptured": 306, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.48, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 33, - "outgoingVideoMaxPushLatencyMs": 33, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 47, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_AseAEMuwQvUDUq", - "kind": "audio", - "bitrateKbps": 258.1, - "packetsLost": 1, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_AMYDBcTLVPXW5R", - "kind": "audio", - "bitrateKbps": 91.1, - "packetsLost": 45, - "jitterMs": 0, - "audioLevel": 0.333, - "codec": "audio/opus" - }, - { - "participantSid": "PA_G5C4xWMJumgR", - "trackSid": "TR_VSSvARsPYMjnfH", - "kind": "video", - "bitrateKbps": 35, - "packetsLost": 4, - "jitterMs": 4, - "codec": "video/VP8" - }, - { - "participantSid": "PA_t8raa7C57aBi", - "trackSid": "TR_AsEpo39LZXJwgU", - "kind": "audio", - "bitrateKbps": 261.8, - "packetsLost": 2, - "jitterMs": 1, - "audioLevel": 0.318, - "codec": "audio/opus" - }, - { - "participantSid": "PA_t8raa7C57aBi", - "trackSid": "TR_VSVNYfqdKkzCqh", - "kind": "video", - "bitrateKbps": 35.4, - "packetsLost": 7, - "jitterMs": 4, - "codec": "video/VP8" - }, - { - "participantSid": "PA_t8raa7C57aBi", - "trackSid": "TR_AMZe28QCXUZLuv", - "kind": "audio", - "bitrateKbps": 86.9, - "packetsLost": 45, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-autosuboff-20260605T202848Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-autosuboff-20260605T202848Z.json deleted file mode 100644 index 24b91c71c..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-autosuboff-20260605T202848Z.json +++ /dev/null @@ -1,316 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:28:48.938Z", - "endedAt": "2026-06-05T20:29:18.944Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-98362-5162a4d6-ca28-4b4f-b02e-bea151fc375d", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-98362", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-98362", - "subscriberIdentity": "fluxer-native-subscriber-98362", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-autosuboff-20260605T202848Z.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": false - }, - { - "name": "subscriber video resolution 1280x720", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": false - }, - { - "name": "server publishing API participants listed", - "pass": false - }, - { - "name": "server sees publisher screenshare publication", - "pass": false - }, - { - "name": "server sees publisher microphone publication", - "pass": false - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": false - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": false - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": false - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1251:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1668:3)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1920:18)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1953:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-local-20260605T203006Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-local-20260605T203006Z.json deleted file mode 100644 index 9ce2636e1..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-local-20260605T203006Z.json +++ /dev/null @@ -1,654 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:30:06.419Z", - "endedAt": "2026-06-05T20:30:18.162Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-98467-7ef96043-b8a7-42ac-b2b9-d551434e68b6", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-98467", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-98467", - "subscriberIdentity": "fluxer-native-subscriber-98467", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-smoke-local-20260605T203006Z.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "observed packet loss 108 exceeds 0; observed 2 video frames outside 1280x720", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 4400, - "inboundVideoFrames": 573, - "strictAudioFrames": 99, - "strictVideoFrames": 560, - "videoCallbacks": 571, - "videoBytes": 785203200, - "receivedFps": 56.08, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 103, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 431, - "maxAudioFrameGapOverallMs": 114, - "avDriftMs": 55, - "maxObservedPacketLoss": 108, - "videoResolutionCounts": { - "1280x720": 558, - "640x360": 2 - }, - "videoResolutionMismatchCount": 2, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691407114, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691418147, - "elapsedMs": 11141, - "producedFps": 29.8 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691407115, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691418147, - "elapsedMs": 11140, - "producedFps": 29.8 - }, - "serverPublishing": { - "fetchedAtMs": 1780691408158, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-98467", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMcinu47L8FgLf", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VS32SFBD3gqCZF", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AsHUY22SNjwgPj", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-98467", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-2-98467", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMnKcxrhawpPFJ", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSmsDZR3M5RREb", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_Asw3rBwsK6H7ob", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_VSmsDZR3M5RREb", - "width": 1280, - "height": 720, - "timestampUs": 246264558000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AsHUY22SNjwgPj", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 261.6, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VS32SFBD3gqCZF", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 2.2, - "packetsLost": 0, - "fps": 3, - "codec": "video/VP8" - }, - { - "trackSid": "TR_VS32SFBD3gqCZF", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 31.2, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMcinu47L8FgLf", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_VSmsDZR3M5RREb", - "kind": "video", - "bitrateKbps": 2.3, - "packetsLost": 5, - "jitterMs": 13, - "codec": "video/VP8" - }, - { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_AMnKcxrhawpPFJ", - "kind": "audio", - "bitrateKbps": 98.8, - "packetsLost": 7, - "jitterMs": 0, - "audioLevel": 0.313, - "codec": "audio/opus" - }, - { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_Asw3rBwsK6H7ob", - "kind": "audio", - "bitrateKbps": 236.5, - "packetsLost": 55, - "jitterMs": 1, - "audioLevel": 0.318, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 326, - "outgoingVideoFramesAccepted": 326, - "outgoingVideoFramesDropped": 14, - "outgoingVideoFramesCoalesced": 14, - "outgoingVideoFramesCaptured": 311, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.66, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 31, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AMnKcxrhawpPFJ", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.6, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSmsDZR3M5RREb", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 33.7, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - }, - { - "trackSid": "TR_VSmsDZR3M5RREb", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 2.3, - "packetsLost": 0, - "fps": 3, - "codec": "video/VP8" - }, - { - "trackSid": "TR_Asw3rBwsK6H7ob", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 262.2, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_fFFTp8k4DSa3", - "trackSid": "TR_AMcinu47L8FgLf", - "kind": "audio", - "bitrateKbps": 94.8, - "packetsLost": 61, - "jitterMs": 0, - "audioLevel": 0.33, - "codec": "audio/opus" - }, - { - "participantSid": "PA_fFFTp8k4DSa3", - "trackSid": "TR_AsHUY22SNjwgPj", - "kind": "audio", - "bitrateKbps": 252.1, - "packetsLost": 44, - "jitterMs": 0, - "audioLevel": 0.332, - "codec": "audio/opus" - }, - { - "participantSid": "PA_fFFTp8k4DSa3", - "trackSid": "TR_VS32SFBD3gqCZF", - "kind": "video", - "bitrateKbps": 1.2, - "packetsLost": 3, - "jitterMs": 13, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 319, - "outgoingVideoFramesAccepted": 319, - "outgoingVideoFramesDropped": 14, - "outgoingVideoFramesCoalesced": 14, - "outgoingVideoFramesCaptured": 305, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.67, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 31, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_AMnKcxrhawpPFJ", - "kind": "audio", - "bitrateKbps": 93, - "packetsLost": 42, - "jitterMs": 0, - "audioLevel": 0.334, - "codec": "audio/opus" - }, - { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_VSmsDZR3M5RREb", - "kind": "video", - "bitrateKbps": 30.7, - "packetsLost": 13, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_xn87DfYqiNM5", - "trackSid": "TR_Asw3rBwsK6H7ob", - "kind": "audio", - "bitrateKbps": 261.9, - "packetsLost": 9, - "jitterMs": 1, - "audioLevel": 0.318, - "codec": "audio/opus" - }, - { - "participantSid": "PA_fFFTp8k4DSa3", - "trackSid": "TR_AMcinu47L8FgLf", - "kind": "audio", - "bitrateKbps": 94.8, - "packetsLost": 13, - "jitterMs": 0, - "audioLevel": 0.315, - "codec": "audio/opus" - }, - { - "participantSid": "PA_fFFTp8k4DSa3", - "trackSid": "TR_VS32SFBD3gqCZF", - "kind": "video", - "bitrateKbps": 31.2, - "packetsLost": 14, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_fFFTp8k4DSa3", - "trackSid": "TR_AsHUY22SNjwgPj", - "kind": "audio", - "bitrateKbps": 261.5, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.336, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-stable-nosimulcast-20260605T203042Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-stable-nosimulcast-20260605T203042Z.json deleted file mode 100644 index bc4fd695f..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-stable-nosimulcast-20260605T203042Z.json +++ /dev/null @@ -1,635 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:30:42.517Z", - "endedAt": "2026-06-05T20:30:54.199Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-98493-8a9f539a-e21e-481c-99b7-cd5ce35f5b34", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-98493", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-98493", - "subscriberIdentity": "fluxer-native-subscriber-98493", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-stable-nosimulcast-20260605T203042Z.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "observed packet loss 240 exceeds 0", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 4334, - "inboundVideoFrames": 602, - "strictAudioFrames": 100, - "strictVideoFrames": 571, - "videoCallbacks": 600, - "videoBytes": 829440000, - "receivedFps": 57.45, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 183, - "maxAudioFrameGapMs": 102, - "maxVideoFrameGapOverallMs": 208, - "maxAudioFrameGapOverallMs": 104, - "avDriftMs": 42, - "maxObservedPacketLoss": 240, - "videoResolutionCounts": { - "1280x720": 571 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691443335, - "stoppedAtMs": null, - "framesPushed": 326, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 37, - "lastPushAtMs": 1780691454168, - "elapsedMs": 10954, - "producedFps": 29.76 - }, - "secondaryPublisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691443335, - "stoppedAtMs": null, - "framesPushed": 326, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691454168, - "elapsedMs": 10954, - "producedFps": 29.76 - }, - "serverPublishing": { - "fetchedAtMs": 1780691443938, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-2-98493", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VS6eBSydG2Fe6Y", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMwSsNzhnQnZbN", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsYPbyKEiQYdXA", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-publisher-98493", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSsm5jzFutA9po", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMd2rFUNTJzRRx", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsJfw9r7g4LU9b", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-98493", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_VS6eBSydG2Fe6Y", - "width": 1280, - "height": 720, - "timestampUs": 246300548000 - }, - "publisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_VSsm5jzFutA9po", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 28.8, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AsJfw9r7g4LU9b", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 258, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMd2rFUNTJzRRx", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_AsYPbyKEiQYdXA", - "kind": "audio", - "bitrateKbps": 257.7, - "packetsLost": 2, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_VS6eBSydG2Fe6Y", - "kind": "video", - "bitrateKbps": 27.7, - "packetsLost": 8, - "jitterMs": 2, - "codec": "video/VP8" - }, - { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_AMwSsNzhnQnZbN", - "kind": "audio", - "bitrateKbps": 98.1, - "packetsLost": 9, - "jitterMs": 0, - "audioLevel": 0.337, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 319, - "outgoingVideoFramesAccepted": 319, - "outgoingVideoFramesDropped": 14, - "outgoingVideoFramesCoalesced": 14, - "outgoingVideoFramesCaptured": 304, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.61, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 35, - "outgoingVideoMaxPushLatencyMs": 35, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 30, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_AsYPbyKEiQYdXA", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 262.5, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMwSsNzhnQnZbN", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VS6eBSydG2Fe6Y", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 26.6, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - } - ], - "inbound": [ - { - "participantSid": "PA_SDXDDK8nFR6H", - "trackSid": "TR_AMd2rFUNTJzRRx", - "kind": "audio", - "bitrateKbps": 98.3, - "packetsLost": 10, - "jitterMs": 1, - "audioLevel": 0.333, - "codec": "audio/opus" - }, - { - "participantSid": "PA_SDXDDK8nFR6H", - "trackSid": "TR_AsJfw9r7g4LU9b", - "kind": "audio", - "bitrateKbps": 267.9, - "packetsLost": 3, - "jitterMs": 0, - "audioLevel": 0.336, - "codec": "audio/opus" - }, - { - "participantSid": "PA_SDXDDK8nFR6H", - "trackSid": "TR_VSsm5jzFutA9po", - "kind": "video", - "bitrateKbps": 27.8, - "packetsLost": 7, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 317, - "outgoingVideoFramesAccepted": 317, - "outgoingVideoFramesDropped": 14, - "outgoingVideoFramesCoalesced": 14, - "outgoingVideoFramesCaptured": 302, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.61, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 30, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": null, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_AMwSsNzhnQnZbN", - "kind": "audio", - "bitrateKbps": 85.4, - "packetsLost": 58, - "jitterMs": 0, - "audioLevel": 0.316, - "codec": "audio/opus" - }, - { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_AsYPbyKEiQYdXA", - "kind": "audio", - "bitrateKbps": 242.8, - "packetsLost": 46, - "jitterMs": 0, - "audioLevel": 0.383, - "codec": "audio/opus" - }, - { - "participantSid": "PA_RjVrb6cfSTYN", - "trackSid": "TR_VS6eBSydG2Fe6Y", - "kind": "video", - "bitrateKbps": 26.9, - "packetsLost": 26, - "jitterMs": 2, - "codec": "video/VP8" - }, - { - "participantSid": "PA_SDXDDK8nFR6H", - "trackSid": "TR_AMd2rFUNTJzRRx", - "kind": "audio", - "bitrateKbps": 94.9, - "packetsLost": 52, - "jitterMs": 0, - "audioLevel": 0.316, - "codec": "audio/opus" - }, - { - "participantSid": "PA_SDXDDK8nFR6H", - "trackSid": "TR_AsJfw9r7g4LU9b", - "kind": "audio", - "bitrateKbps": 258.3, - "packetsLost": 42, - "jitterMs": 0, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_SDXDDK8nFR6H", - "trackSid": "TR_VSsm5jzFutA9po", - "kind": "video", - "bitrateKbps": 28, - "packetsLost": 16, - "jitterMs": 2, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-strict-4k60-20260605T201811Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-strict-4k60-20260605T201811Z.json deleted file mode 100644 index be3da22c0..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-strict-4k60-20260605T201811Z.json +++ /dev/null @@ -1,1405 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:18:32.176Z", - "endedAt": "2026-06-05T20:21:02.334Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97026-aed6d5c1-230e-4223-ab28-36ab6db848ee", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97026", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97026", - "subscriberIdentity": "fluxer-native-subscriber-97026", - "required": true, - "strict": true, - "timeoutMs": 180000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 120000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "expectedScreenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp9", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP9", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 24, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-strict-4k60-20260605T201811Z.json", - "verbose": false, - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "scenarioCount": 5, - "scenarios": [ - { - "status": "fail", - "startedAt": "2026-06-05T20:18:32.181Z", - "endedAt": "2026-06-05T20:19:02.186Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97026-aed6d5c1-230e-4223-ab28-36ab6db848ee-codec-vp8-with-vp9", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97026", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97026", - "subscriberIdentity": "fluxer-native-subscriber-97026", - "required": true, - "strict": true, - "timeoutMs": 180000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 120000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "expectedScreenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp9", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP9", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 24, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": null, - "verbose": false, - "scenarioName": "codec-vp8-with-vp9", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": false - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher local camera publication", - "pass": false - }, - { - "name": "subscriber remote camera subscription", - "pass": false - }, - { - "name": "publisher outbound camera stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/VP9", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/VP9", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1086:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1394:3)\n at async runScenarioSuite (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1610:19)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1639:22)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1673:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 57, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } - }, - { - "status": "fail", - "startedAt": "2026-06-05T20:19:02.297Z", - "endedAt": "2026-06-05T20:19:32.299Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97026-aed6d5c1-230e-4223-ab28-36ab6db848ee-codec-vp9-with-h264", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97026", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97026", - "subscriberIdentity": "fluxer-native-subscriber-97026", - "required": true, - "strict": true, - "timeoutMs": 180000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 120000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "expectedScreenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "codec": "vp9", - "expectedScreenCodec": "video/VP9", - "secondaryPublisher": true, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 24, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": null, - "verbose": false, - "scenarioName": "codec-vp9-with-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": false - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher local camera publication", - "pass": false - }, - { - "name": "subscriber remote camera subscription", - "pass": false - }, - { - "name": "publisher outbound camera stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/H264", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/H264", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/VP9", - "pass": false - }, - { - "name": "subscriber inbound video codec video/VP9", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1086:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1394:3)\n at async runScenarioSuite (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1610:19)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1639:22)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1673:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 57, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } - }, - { - "status": "fail", - "startedAt": "2026-06-05T20:19:32.307Z", - "endedAt": "2026-06-05T20:20:02.308Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97026-aed6d5c1-230e-4223-ab28-36ab6db848ee-codec-h264-with-hevc", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97026", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97026", - "subscriberIdentity": "fluxer-native-subscriber-97026", - "required": true, - "strict": true, - "timeoutMs": 180000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 120000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "expectedScreenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": true, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 24, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": null, - "verbose": false, - "scenarioName": "codec-h264-with-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": false - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher local camera publication", - "pass": false - }, - { - "name": "subscriber remote camera subscription", - "pass": false - }, - { - "name": "publisher outbound camera stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/H265", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/H265", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": false - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1086:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1394:3)\n at async runScenarioSuite (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1610:19)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1639:22)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1673:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 57, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } - }, - { - "status": "fail", - "startedAt": "2026-06-05T20:20:02.316Z", - "endedAt": "2026-06-05T20:20:32.317Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97026-aed6d5c1-230e-4223-ab28-36ab6db848ee-codec-hevc-with-av1", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97026", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97026", - "subscriberIdentity": "fluxer-native-subscriber-97026", - "required": true, - "strict": true, - "timeoutMs": 180000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 120000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "expectedScreenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": true, - "secondaryPublisherCodec": "av1", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/AV1", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 24, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": null, - "verbose": false, - "scenarioName": "codec-hevc-with-av1", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": false - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher local camera publication", - "pass": false - }, - { - "name": "subscriber remote camera subscription", - "pass": false - }, - { - "name": "publisher outbound camera stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/AV1", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/AV1", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": false - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1086:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1394:3)\n at async runScenarioSuite (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1610:19)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1639:22)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1673:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 57, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } - }, - { - "status": "fail", - "startedAt": "2026-06-05T20:20:32.324Z", - "endedAt": "2026-06-05T20:21:02.326Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:7880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-97026-aed6d5c1-230e-4223-ab28-36ab6db848ee-codec-av1-with-vp8", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-97026", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-97026", - "subscriberIdentity": "fluxer-native-subscriber-97026", - "required": true, - "strict": true, - "timeoutMs": 180000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 120000, - "width": 3840, - "height": 2160, - "expectedWidth": 3840, - "expectedHeight": 2160, - "fps": 60, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "expectedScreenCodecs": ["vp8", "vp9", "h264", "hevc", "av1"], - "codec": "av1", - "expectedScreenCodec": "video/AV1", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 24, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 80000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "camera": true, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": null, - "verbose": false, - "scenarioName": "codec-av1-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": false - }, - { - "name": "subscriber connected", - "pass": false - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 9", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 3", - "pass": false - }, - { - "name": "subscriber video resolution 3840x2160", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher local camera publication", - "pass": false - }, - { - "name": "subscriber remote camera subscription", - "pass": false - }, - { - "name": "publisher outbound camera stats", - "pass": false - }, - { - "name": "secondary publisher connected", - "pass": false - }, - { - "name": "secondary publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": false - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": false - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": false - }, - { - "name": "secondary publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": false - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": false - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": false - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "subscriber data packet received", - "pass": false - }, - { - "name": "subscriber secondary data packet received", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/AV1", - "pass": false - }, - { - "name": "subscriber inbound video codec video/AV1", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: connect timed out after 30000ms\n at file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1086:9\n at async runHarness (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1394:3)\n at async runScenarioSuite (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1610:19)\n at async main (file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1639:22)\n at async file:///Users/hampus/Development/fluxer/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs:1673:19", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 57, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "lastVideoMeta": null, - "publisherStats": null, - "secondaryPublisherStats": null, - "subscriberStats": null - } - } - ] -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-20260605T203103Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-20260605T203103Z.json deleted file mode 100644 index 492c1cdfd..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-20260605T203103Z.json +++ /dev/null @@ -1,285 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:31:04.019Z", - "endedAt": "2026-06-05T20:31:15.493Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-98534-07955759-bfdc-4fc0-8941-6c23669306cb", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-98534", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-98534", - "subscriberIdentity": "fluxer-native-subscriber-98534", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-20260605T203103Z.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 19.96 below 28.5; observed packet loss 1 exceeds 0", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 227, - "strictAudioFrames": 0, - "strictVideoFrames": 200, - "videoCallbacks": 226, - "videoBytes": 312422400, - "receivedFps": 19.96, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 98, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 107, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 1, - "videoResolutionCounts": { - "1280x720": 200 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691464440, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691475476, - "elapsedMs": 11142, - "producedFps": 29.8 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780691465483, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-98534", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSCdbHfS7Qjnp9", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-98534", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_NeWDVeMforAq", - "trackSid": "TR_VSCdbHfS7Qjnp9", - "width": 1280, - "height": 720, - "timestampUs": 246321877000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSCdbHfS7Qjnp9", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 28.5, - "packetsLost": 0, - "fps": 14, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 330, - "outgoingVideoFramesAccepted": 330, - "outgoingVideoFramesDropped": 100, - "outgoingVideoFramesCoalesced": 100, - "outgoingVideoFramesCaptured": 229, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 20.83, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "fps15", - "adaptiveSendReason": "frameCoalescingPressure" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_NeWDVeMforAq", - "trackSid": "TR_VSCdbHfS7Qjnp9", - "kind": "video", - "bitrateKbps": 30.5, - "packetsLost": 1, - "jitterMs": 4, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-adaptive-fixed-20260605T203523Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-adaptive-fixed-20260605T203523Z.json deleted file mode 100644 index 18f379305..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-adaptive-fixed-20260605T203523Z.json +++ /dev/null @@ -1,285 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:35:23.806Z", - "endedAt": "2026-06-05T20:35:35.256Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99020-a8faf4d3-b9ea-4425-b85e-484c3439bea8", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99020", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99020", - "subscriberIdentity": "fluxer-native-subscriber-99020", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-adaptive-fixed-20260605T203523Z.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "observed packet loss 4 exceeds 0", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 314, - "strictAudioFrames": 0, - "strictVideoFrames": 287, - "videoCallbacks": 314, - "videoBytes": 434073600, - "receivedFps": 28.89, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 65, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 174, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 4, - "videoResolutionCounts": { - "1280x720": 287 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691724194, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 38, - "lastPushAtMs": 1780691735226, - "elapsedMs": 11160, - "producedFps": 29.75 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780691725246, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-99020", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSNY7hqQXsYjDU", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-99020", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_4XVg8hrTERta", - "trackSid": "TR_VSNY7hqQXsYjDU", - "width": 1280, - "height": 720, - "timestampUs": 246581659000 - }, - "publisherStats": { - "rttMs": null, - "outbound": [ - { - "trackSid": "TR_VSNY7hqQXsYjDU", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 29.1, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 330, - "outgoingVideoFramesAccepted": 330, - "outgoingVideoFramesDropped": 16, - "outgoingVideoFramesCoalesced": 16, - "outgoingVideoFramesCaptured": 314, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.49, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_4XVg8hrTERta", - "trackSid": "TR_VSNY7hqQXsYjDU", - "kind": "video", - "bitrateKbps": 29.1, - "packetsLost": 4, - "jitterMs": 2, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-noadaptive-20260605T203134Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-noadaptive-20260605T203134Z.json deleted file mode 100644 index c2c10f915..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-noadaptive-20260605T203134Z.json +++ /dev/null @@ -1,285 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:31:34.574Z", - "endedAt": "2026-06-05T20:31:45.998Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-98569-9a0c5b4a-c12c-4cce-97ba-5481af7273d4", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-98569", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-98569", - "subscriberIdentity": "fluxer-native-subscriber-98569", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": false, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-noadaptive-20260605T203134Z.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 28.45 below 28.5", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 312, - "strictAudioFrames": 0, - "strictVideoFrames": 285, - "videoCallbacks": 311, - "videoBytes": 429926400, - "receivedFps": 28.45, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 61, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 69, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "videoResolutionCounts": { - "1280x720": 285 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691494944, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691505978, - "elapsedMs": 11144, - "producedFps": 29.79 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780691495987, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-98569", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-98569", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSnPVaEqt8wLKt", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_4H7SUk7TRbA7", - "trackSid": "TR_VSnPVaEqt8wLKt", - "width": 1280, - "height": 720, - "timestampUs": 246352378000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSnPVaEqt8wLKt", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 28.1, - "packetsLost": 0, - "fps": 28, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 330, - "outgoingVideoFramesAccepted": 330, - "outgoingVideoFramesDropped": 17, - "outgoingVideoFramesCoalesced": 17, - "outgoingVideoFramesCaptured": 313, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.44, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "adaptiveDisabled" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_4H7SUk7TRbA7", - "trackSid": "TR_VSnPVaEqt8wLKt", - "kind": "video", - "bitrateKbps": 28.1, - "packetsLost": 0, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-paced-20260605T203257Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-paced-20260605T203257Z.json deleted file mode 100644 index 5f29d01e5..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-paced-20260605T203257Z.json +++ /dev/null @@ -1,285 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:32:57.846Z", - "endedAt": "2026-06-05T20:33:09.335Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-98748-d3e8cf0e-bc5c-48e8-b5ef-ed0f993fbe86", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-98748", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-98748", - "subscriberIdentity": "fluxer-native-subscriber-98748", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-paced-20260605T203257Z.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 20.11 below 28.5; observed packet loss 2 exceeds 0", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 226, - "strictAudioFrames": 0, - "strictVideoFrames": 201, - "videoCallbacks": 225, - "videoBytes": 311040000, - "receivedFps": 20.11, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 173, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 173, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 2, - "videoResolutionCounts": { - "1280x720": 201 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691578281, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 35, - "lastPushAtMs": 1780691589316, - "elapsedMs": 11143, - "producedFps": 29.79 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780691579324, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-98748", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-98748", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VS5AVMD9QiLw9T", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_wGf7jDpSGqVf", - "trackSid": "TR_VS5AVMD9QiLw9T", - "width": 1280, - "height": 720, - "timestampUs": 246435681000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VS5AVMD9QiLw9T", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 28.5, - "packetsLost": 0, - "fps": 14, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 330, - "outgoingVideoFramesAccepted": 330, - "outgoingVideoFramesDropped": 100, - "outgoingVideoFramesCoalesced": 100, - "outgoingVideoFramesCaptured": 230, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 20.87, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "fps15", - "adaptiveSendReason": "frameCoalescingPressure" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_wGf7jDpSGqVf", - "trackSid": "TR_VS5AVMD9QiLw9T", - "kind": "video", - "bitrateKbps": 28.5, - "packetsLost": 2, - "jitterMs": 2, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-strict-delta-20260605T203710Z.json b/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-strict-delta-20260605T203710Z.json deleted file mode 100644 index 9554a3dfe..000000000 --- a/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-strict-delta-20260605T203710Z.json +++ /dev/null @@ -1,286 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T20:37:10.528Z", - "endedAt": "2026-06-05T20:37:21.966Z", - "platform": "darwin", - "arch": "arm64", - "config": { - "url": "ws://127.0.0.1:17880", - "serverApiUrl": "http://127.0.0.1:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-99097-fadf776d-a03a-42a1-92bc-c45de56c5a2c", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-99097", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-99097", - "subscriberIdentity": "fluxer-native-subscriber-99097", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "fast", - "videoInput": "nv12", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": false, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": false, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "/Users/hampus/Development/fluxer/fluxer_desktop/native-media-reports/mac-m3-ultra-livekit-video-only-strict-delta-20260605T203710Z.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 28.46 below 28.5", - "hardwareEncoder": { - "available": true, - "backend": "videotoolbox", - "compiled": true, - "runtime": true, - "codecs": ["h264", "h265"], - "zeroCopy": false, - "nativeInputs": [], - "detail": "VideoToolbox can create hardware-accelerated compression sessions for h264, h265; supported hardware-codec probe set: h264, h265" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 310, - "strictAudioFrames": 0, - "strictVideoFrames": 285, - "videoCallbacks": 309, - "videoBytes": 427161600, - "receivedFps": 28.46, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 63, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 69, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 285 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "nv12", - "pattern": "fast", - "targetFps": 30, - "startedAtMs": 1780691830909, - "stoppedAtMs": null, - "framesPushed": 332, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 36, - "lastPushAtMs": 1780691841942, - "elapsedMs": 11151, - "producedFps": 29.77 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780691831955, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-99097", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-99097", - "trackCount": 1, - "tracks": [ - { - "sid": "TR_VSmHHYYxDdRMdt", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_ZYRki8TB8qqy", - "trackSid": "TR_VSmHHYYxDdRMdt", - "width": 1280, - "height": 720, - "timestampUs": 246688341000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSmHHYYxDdRMdt", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 28.2, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 329, - "outgoingVideoFramesAccepted": 329, - "outgoingVideoFramesDropped": 16, - "outgoingVideoFramesCoalesced": 16, - "outgoingVideoFramesCaptured": 313, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.51, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 34, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_ZYRki8TB8qqy", - "trackSid": "TR_VSmHHYYxDdRMdt", - "kind": "video", - "bitrateKbps": 27.3, - "packetsLost": 0, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/h264-360p10-strict-single-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/h264-360p10-strict-single-20260605.json deleted file mode 100644 index 7c19f3271..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/h264-360p10-strict-single-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:33:47.023Z", - "endedAt": "2026-06-05T22:34:19.284Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-6084-b067b112-e82c-4f9b-a8e5-8c17b58ce892", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-6084", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-6084", - "subscriberIdentity": "fluxer-native-subscriber-6084", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 10, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 10, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 3000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\h264-360p10-strict-single-20260605.json", - "verbose": false, - "scenarioName": "codec-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 9.43 below 9.5", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 6176, - "inboundVideoFrames": 287, - "strictAudioFrames": 299, - "strictVideoFrames": 283, - "videoCallbacks": 287, - "videoBytes": 99187200, - "receivedFps": 9.43, - "requiredFps": 9.5, - "maxVideoFrameGapMs": 204, - "maxAudioFrameGapMs": 103, - "maxVideoFrameGapOverallMs": 204, - "maxAudioFrameGapOverallMs": 103, - "avDriftMs": 4, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 283 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 10, - "startedAtMs": 1780698828184, - "stoppedAtMs": null, - "framesPushed": 311, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 103, - "lastPushAtMs": 1780698859185, - "elapsedMs": 31105, - "producedFps": 10 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780698829266, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-6084", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSTAkE5Tyvf84k", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - }, - { - "sid": "TR_AMR8qs636tXCeM", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsiQhtzQxKx3hg", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-6084", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_yWuJtiAsGJHk", - "trackSid": "TR_VSTAkE5Tyvf84k", - "width": 640, - "height": 360, - "timestampUs": 1737095000 - }, - "publisherStats": { - "rttMs": 4, - "outbound": [ - { - "trackSid": "TR_AsiQhtzQxKx3hg", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 259.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMR8qs636tXCeM", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSTAkE5Tyvf84k", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1267, - "packetsLost": 0, - "fps": 11, - "codec": "video/H264" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 310, - "outgoingVideoFramesAccepted": 310, - "outgoingVideoFramesDropped": 4, - "outgoingVideoFramesCoalesced": 4, - "outgoingVideoFramesCaptured": 306, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 9.87, - "outgoingVideoTargetFps": 10, - "outgoingVideoMaxQueueAgeMs": 100, - "outgoingVideoMaxPushLatencyMs": 101, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 56, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 4, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_yWuJtiAsGJHk", - "trackSid": "TR_AMR8qs636tXCeM", - "kind": "audio", - "bitrateKbps": 99, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.354, - "codec": "audio/opus" - }, - { - "participantSid": "PA_yWuJtiAsGJHk", - "trackSid": "TR_VSTAkE5Tyvf84k", - "kind": "video", - "bitrateKbps": 1267, - "packetsLost": 0, - "jitterMs": 4, - "codec": "video/H264" - }, - { - "participantSid": "PA_yWuJtiAsGJHk", - "trackSid": "TR_AsiQhtzQxKx3hg", - "kind": "audio", - "bitrateKbps": 259.9, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/h264-360p15-strict-single-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/h264-360p15-strict-single-20260605.json deleted file mode 100644 index 8cb3e81bd..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/h264-360p15-strict-single-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:32:58.250Z", - "endedAt": "2026-06-05T22:33:30.493Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-4444-6d4fc834-c519-4a03-831b-1f3c4f7b8db5", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-4444", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-4444", - "subscriberIdentity": "fluxer-native-subscriber-4444", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\h264-360p15-strict-single-20260605.json", - "verbose": false, - "scenarioName": "codec-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 11.83 below 14.25", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 6188, - "inboundVideoFrames": 360, - "strictAudioFrames": 299, - "strictVideoFrames": 356, - "videoCallbacks": 360, - "videoBytes": 124416000, - "receivedFps": 11.83, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 220, - "maxAudioFrameGapMs": 115, - "maxVideoFrameGapOverallMs": 220, - "maxAudioFrameGapOverallMs": 115, - "avDriftMs": 16, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 356 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780698779365, - "stoppedAtMs": null, - "framesPushed": 467, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 87, - "lastPushAtMs": 1780698810433, - "elapsedMs": 31131, - "producedFps": 15 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780698780443, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-4444", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-4444", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSu5oXdquxv9yn", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - }, - { - "sid": "TR_AMw39xhBtq2iKo", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsUfFKczDubKFH", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_jmECWh8cUV8Z", - "trackSid": "TR_VSu5oXdquxv9yn", - "width": 640, - "height": 360, - "timestampUs": 1688313000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AsUfFKczDubKFH", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 263, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMw39xhBtq2iKo", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 97.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSu5oXdquxv9yn", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1957.3, - "packetsLost": 0, - "fps": 15, - "codec": "video/H264" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 465, - "outgoingVideoFramesAccepted": 465, - "outgoingVideoFramesDropped": 9, - "outgoingVideoFramesCoalesced": 9, - "outgoingVideoFramesCaptured": 456, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.7, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 67, - "outgoingVideoMaxPushLatencyMs": 68, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 45, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_jmECWh8cUV8Z", - "trackSid": "TR_VSu5oXdquxv9yn", - "kind": "video", - "bitrateKbps": 1957.3, - "packetsLost": 0, - "jitterMs": 4, - "codec": "video/H264" - }, - { - "participantSid": "PA_jmECWh8cUV8Z", - "trackSid": "TR_AMw39xhBtq2iKo", - "kind": "audio", - "bitrateKbps": 97.9, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.33, - "codec": "audio/opus" - }, - { - "participantSid": "PA_jmECWh8cUV8Z", - "trackSid": "TR_AsUfFKczDubKFH", - "kind": "audio", - "bitrateKbps": 263, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.342, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/h265-failfast-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/h265-failfast-20260605.json deleted file mode 100644 index 78623222a..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/h265-failfast-20260605.json +++ /dev/null @@ -1,258 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:35:16.027Z", - "endedAt": "2026-06-05T22:35:17.121Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-6712-42fbb624-d6ae-4495-80c6-f657bba60b68", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-6712", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-6712", - "subscriberIdentity": "fluxer-native-subscriber-6712", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["h265"], - "expectedScreenCodecs": ["h265"], - "codec": "h265", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h265", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\h265-failfast-20260605.json", - "verbose": false, - "scenarioName": "codec-h265", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": false - }, - { - "name": "subscriber video resolution 640x360", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher local screen-share audio publication", - "pass": false - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": false - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": false - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: H.265 publishing requires hardware encoder support; missing nvcuda.dll, nvEncodeAPI64.dll", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": null, - "publisherStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/run-livekit.ps1 b/fluxer_desktop/native-media-reports/windows-parallels-arm64/run-livekit.ps1 deleted file mode 100644 index af93ad0d5..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/run-livekit.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -param( - [string]$Scenario = "vp8-360p15-strict-single", - [string]$Codec = "vp8", - [string]$SecondaryCodec = "", - [int]$Width = 640, - [int]$Height = 360, - [double]$Fps = 15, - [int]$DurationMs = 10000, - [int]$TimeoutMs = 60000, - [int]$SecondPublisher = 0, - [int]$ScreenSimulcast = 0, - [int]$ScreenAudio = 1, - [int]$Microphone = 1, - [int]$SubscriptionCycle = 1, - [int]$DataPacket = 1, - [int]$ValidateServerPublishing = 1, - [int]$RequireStableResolution = 1, - [int]$MaxPacketLoss = 0, - [int]$MaxAvDriftMs = 150, - [int]$MaxFrameGapMs = 250, - [int]$MaxAudioFrameGapMs = 250, - [double]$MinReceivedFpsRatio = 0.95, - [int]$MaxBitrateBps = 4000000, - [string]$LiveKitUrl = "ws://100.79.83.54:17880", - [string]$LiveKitApiUrl = "http://100.79.83.54:17880", - [string]$ApiKey = "devkey", - [string]$ApiSecret = "secret", - [string]$NodeDir = "C:\nodejs\node-v24.16.0-win-arm64", - [string]$WorkDir = "C:\f\fluxer_desktop\native\webrtc-sender", - [string]$ReportDir = "C:\Mac\Home\Development\fluxer\fluxer_desktop\native-media-reports\windows-parallels-arm64", - [int]$VerboseHarness = 0 -) - -$ErrorActionPreference = "Stop" -$nodeExe = Join-Path $NodeDir "node.exe" -if (-not (Test-Path $nodeExe)) { - throw "Node executable not found: $nodeExe" -} -if (-not (Test-Path $WorkDir)) { - throw "WorkDir not found: $WorkDir" -} - -New-Item -ItemType Directory -Force -Path $ReportDir | Out-Null -$reportPath = Join-Path $ReportDir "$Scenario.json" -$logPath = Join-Path $ReportDir "$Scenario.log" -Remove-Item -Force $reportPath, $logPath -ErrorAction SilentlyContinue - -$env:Path = "$NodeDir;$env:Path" -$env:LIVEKIT_URL = $LiveKitUrl -$env:LIVEKIT_API_URL = $LiveKitApiUrl -$env:LIVEKIT_API_KEY = $ApiKey -$env:LIVEKIT_API_SECRET = $ApiSecret -$env:FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED = "1" -$env:LIVEKIT_HARNESS_STRICT = "1" -$env:LIVEKIT_HARNESS_DURATION_MS = [string]$DurationMs -$env:LIVEKIT_HARNESS_TIMEOUT_MS = [string]$TimeoutMs -$env:LIVEKIT_CONNECT_TIMEOUT_MS = "30000" -$env:LIVEKIT_SCREEN_WIDTH = [string]$Width -$env:LIVEKIT_SCREEN_HEIGHT = [string]$Height -$env:LIVEKIT_SCREEN_FPS = [string]$Fps -$env:LIVEKIT_MIN_VIDEO_FPS = [string]([math]::Min(15, $Fps)) -$env:LIVEKIT_MIN_RECEIVED_FPS_RATIO = [string]$MinReceivedFpsRatio -$env:LIVEKIT_SCREEN_CODEC = $Codec -$env:LIVEKIT_SCREEN_SIMULCAST = [string]$ScreenSimulcast -$env:LIVEKIT_ENABLE_SECOND_PUBLISHER = [string]$SecondPublisher -$env:LIVEKIT_ENABLE_MICROPHONE = [string]$Microphone -$env:LIVEKIT_ENABLE_SCREEN_AUDIO = [string]$ScreenAudio -$env:LIVEKIT_ENABLE_SUBSCRIPTION_CYCLE = [string]$SubscriptionCycle -$env:LIVEKIT_ENABLE_DATA_PACKET = [string]$DataPacket -$env:LIVEKIT_VALIDATE_SERVER_PUBLISHING = [string]$ValidateServerPublishing -$env:LIVEKIT_MAX_PACKET_LOSS = [string]$MaxPacketLoss -$env:LIVEKIT_REQUIRE_STABLE_RESOLUTION = [string]$RequireStableResolution -$env:LIVEKIT_MAX_AV_DRIFT_MS = [string]$MaxAvDriftMs -$env:LIVEKIT_MAX_FRAME_GAP_MS = [string]$MaxFrameGapMs -$env:LIVEKIT_MAX_AUDIO_FRAME_GAP_MS = [string]$MaxAudioFrameGapMs -$env:LIVEKIT_SCREEN_MAX_BITRATE_BPS = [string]$MaxBitrateBps -$env:LIVEKIT_HARNESS_REPORT_PATH = $reportPath -$env:LIVEKIT_VERBOSE = [string]$VerboseHarness - -if ($SecondPublisher -ne 0 -and $SecondaryCodec.Trim().Length -gt 0) { - $env:LIVEKIT_SECOND_PUBLISHER_SCREEN_CODEC = $SecondaryCodec -} - -Set-Location $WorkDir -"STARTED=$(Get-Date -Format o)" | Out-File -FilePath $logPath -Encoding utf8 -"& $nodeExe scripts/livekit-harness.mjs" | Add-Content -Path $logPath -$previousErrorActionPreference = $ErrorActionPreference -$ErrorActionPreference = "Continue" -& $nodeExe "scripts/livekit-harness.mjs" *>> $logPath -$code = $LASTEXITCODE -$ErrorActionPreference = $previousErrorActionPreference -"EXIT_CODE=$code" | Add-Content -Path $logPath -"FINISHED=$(Get-Date -Format o)" | Add-Content -Path $logPath - -Get-Content -Path $logPath -exit $code diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p15-strict-multipub-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p15-strict-multipub-20260605.json deleted file mode 100644 index 3304af636..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p15-strict-multipub-20260605.json +++ /dev/null @@ -1,637 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T22:34:34.390Z", - "endedAt": "2026-06-05T22:34:56.901Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-576-9baa1b50-b3f6-47dc-9222-bb3a324a8678", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-576", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-576", - "subscriberIdentity": "fluxer-native-subscriber-576", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 20000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": true, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-360p15-strict-multipub-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8-with-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 6", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 2", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 12", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 4", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 12", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "secondary publisher connected", - "pass": true - }, - { - "name": "secondary publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote secondary screenshare subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare stats", - "pass": true - }, - { - "name": "secondary publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound secondary codec video/VP8", - "pass": true - }, - { - "name": "secondary publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote secondary microphone subscription", - "pass": true - }, - { - "name": "secondary publisher outbound microphone stats", - "pass": true - }, - { - "name": "secondary publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote secondary screen-share audio subscription", - "pass": true - }, - { - "name": "secondary publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "subscriber secondary data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - }, - { - "name": "server sees secondary publisher screenshare publication", - "pass": true - }, - { - "name": "server sees secondary publisher microphone publication", - "pass": true - }, - { - "name": "server sees secondary publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 8386, - "inboundVideoFrames": 605, - "strictAudioFrames": 199, - "strictVideoFrames": 588, - "videoCallbacks": 605, - "videoBytes": 209088000, - "receivedFps": 29.42, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 129, - "maxAudioFrameGapMs": 108, - "maxVideoFrameGapOverallMs": 129, - "maxAudioFrameGapOverallMs": 112, - "avDriftMs": 5, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 588 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780698875820, - "stoppedAtMs": null, - "framesPushed": 317, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 74, - "lastPushAtMs": 1780698896886, - "elapsedMs": 21084, - "producedFps": 15.04 - }, - "secondaryPublisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780698875820, - "stoppedAtMs": null, - "framesPushed": 317, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 74, - "lastPushAtMs": 1780698896888, - "elapsedMs": 21084, - "producedFps": 15.04 - }, - "serverPublishing": { - "fetchedAtMs": 1780698876883, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-2-576", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMafY4CpWgYhFy", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsEzCDkQT4wKAK", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSbd2RjXnY3stM", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-576", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-576", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSc4kF32666sNr", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMrQCsApajeKHi", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsX4E2jVX6P6XY", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_VSc4kF32666sNr", - "width": 640, - "height": 360, - "timestampUs": 1774721000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AsX4E2jVX6P6XY", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 258.8, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSc4kF32666sNr", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1259.5, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMrQCsApajeKHi", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.2, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_A8vwT8LTQovP", - "trackSid": "TR_AMafY4CpWgYhFy", - "kind": "audio", - "bitrateKbps": 99.8, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_A8vwT8LTQovP", - "trackSid": "TR_VSbd2RjXnY3stM", - "kind": "video", - "bitrateKbps": 1257.1, - "packetsLost": 0, - "jitterMs": 5, - "codec": "video/VP8" - }, - { - "participantSid": "PA_A8vwT8LTQovP", - "trackSid": "TR_AsEzCDkQT4wKAK", - "kind": "audio", - "bitrateKbps": 264.5, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.306, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 310, - "outgoingVideoFramesAccepted": 310, - "outgoingVideoFramesDropped": 6, - "outgoingVideoFramesCoalesced": 6, - "outgoingVideoFramesCaptured": 304, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.7, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 67, - "outgoingVideoMaxPushLatencyMs": 67, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 51, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": { - "rttMs": 2, - "outbound": [ - { - "trackSid": "TR_AsEzCDkQT4wKAK", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 259.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSbd2RjXnY3stM", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1171.6, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMafY4CpWgYhFy", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.1, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [ - { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_AsX4E2jVX6P6XY", - "kind": "audio", - "bitrateKbps": 259.2, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_AMrQCsApajeKHi", - "kind": "audio", - "bitrateKbps": 98, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.326, - "codec": "audio/opus" - }, - { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_VSc4kF32666sNr", - "kind": "video", - "bitrateKbps": 1176.7, - "packetsLost": 0, - "jitterMs": 4, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 312, - "outgoingVideoFramesAccepted": 312, - "outgoingVideoFramesDropped": 6, - "outgoingVideoFramesCoalesced": 6, - "outgoingVideoFramesCaptured": 306, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.7, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 67, - "outgoingVideoMaxPushLatencyMs": 68, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 51, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "subscriberStats": { - "rttMs": 2, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_AsX4E2jVX6P6XY", - "kind": "audio", - "bitrateKbps": 258.8, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.336, - "codec": "audio/opus" - }, - { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_VSc4kF32666sNr", - "kind": "video", - "bitrateKbps": 1259.5, - "packetsLost": 0, - "jitterMs": 4, - "codec": "video/VP8" - }, - { - "participantSid": "PA_adnUMDZZHzat", - "trackSid": "TR_AMrQCsApajeKHi", - "kind": "audio", - "bitrateKbps": 98.2, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_A8vwT8LTQovP", - "trackSid": "TR_AMafY4CpWgYhFy", - "kind": "audio", - "bitrateKbps": 99.7, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.306, - "codec": "audio/opus" - }, - { - "participantSid": "PA_A8vwT8LTQovP", - "trackSid": "TR_AsEzCDkQT4wKAK", - "kind": "audio", - "bitrateKbps": 264.3, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_A8vwT8LTQovP", - "trackSid": "TR_VSbd2RjXnY3stM", - "kind": "video", - "bitrateKbps": 1256, - "packetsLost": 0, - "jitterMs": 6, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p15-strict-single-smoke-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p15-strict-single-smoke-20260605.json deleted file mode 100644 index a51360141..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p15-strict-single-smoke-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T22:30:31.342Z", - "endedAt": "2026-06-05T22:30:43.694Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-3164-5cfbcff1-2827-441a-a57b-4333e450d829", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-3164", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-3164", - "subscriberIdentity": "fluxer-native-subscriber-3164", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 4000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-360p15-strict-single-smoke-20260605.json", - "verbose": true, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 2156, - "inboundVideoFrames": 152, - "strictAudioFrames": 100, - "strictVideoFrames": 147, - "videoCallbacks": 152, - "videoBytes": 52531200, - "receivedFps": 14.66, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 143, - "maxAudioFrameGapMs": 105, - "maxVideoFrameGapOverallMs": 143, - "maxAudioFrameGapOverallMs": 325, - "avDriftMs": 26, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 147 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780698632580, - "stoppedAtMs": null, - "framesPushed": 163, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 355, - "lastPushAtMs": 1780698643669, - "elapsedMs": 11117, - "producedFps": 14.66 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780698633676, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-3164", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-3164", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSD8qaPQikwWiL", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMMkiV8hg3XbkW", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsTaYjh3PSCFVz", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_fME8NrPiTu5d", - "trackSid": "TR_VSD8qaPQikwWiL", - "width": 640, - "height": 360, - "timestampUs": 1521475000 - }, - "publisherStats": { - "rttMs": 3, - "outbound": [ - { - "trackSid": "TR_AMMkiV8hg3XbkW", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.9, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSD8qaPQikwWiL", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1233, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AsTaYjh3PSCFVz", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 256.8, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 161, - "outgoingVideoFramesAccepted": 161, - "outgoingVideoFramesDropped": 2, - "outgoingVideoFramesCoalesced": 2, - "outgoingVideoFramesCaptured": 158, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.36, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 66, - "outgoingVideoMaxPushLatencyMs": 66, - "outgoingAudioBufferTargetMs": 300, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 1, - "outgoingAudioRebuffers": 1, - "outgoingAudioMaxFrameGapMs": 316, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_fME8NrPiTu5d", - "trackSid": "TR_VSD8qaPQikwWiL", - "kind": "video", - "bitrateKbps": 1158.7, - "packetsLost": 0, - "jitterMs": 22, - "codec": "video/VP8" - }, - { - "participantSid": "PA_fME8NrPiTu5d", - "trackSid": "TR_AsTaYjh3PSCFVz", - "kind": "audio", - "bitrateKbps": 252.2, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.336, - "codec": "audio/opus" - }, - { - "participantSid": "PA_fME8NrPiTu5d", - "trackSid": "TR_AMMkiV8hg3XbkW", - "kind": "audio", - "bitrateKbps": 99, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.332, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-reason-after-fix-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-reason-after-fix-20260605.json deleted file mode 100644 index 544fd2b4d..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-reason-after-fix-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T22:39:23.612Z", - "endedAt": "2026-06-05T22:39:35.835Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-8360-3df01e6b-06f6-4b3c-9e56-c691627bb305", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-8360", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-8360", - "subscriberIdentity": "fluxer-native-subscriber-8360", - "required": true, - "strict": true, - "timeoutMs": 60000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 10000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 5000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-360p30-reason-after-fix-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 2170, - "inboundVideoFrames": 299, - "strictAudioFrames": 99, - "strictVideoFrames": 291, - "videoCallbacks": 299, - "videoBytes": 103334400, - "receivedFps": 29.05, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 65, - "maxAudioFrameGapMs": 109, - "maxVideoFrameGapOverallMs": 195, - "maxAudioFrameGapOverallMs": 109, - "avDriftMs": 41, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 291 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 30, - "startedAtMs": 1780699164743, - "stoppedAtMs": null, - "framesPushed": 333, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 52, - "lastPushAtMs": 1780699175812, - "elapsedMs": 11095, - "producedFps": 30.01 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780699165815, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-8360", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-8360", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMSaSBEwb9cjwL", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_Ash3ybc8XXbukk", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSzotHy6zMQsRG", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_iYVBCmnrZuSb", - "trackSid": "TR_VSzotHy6zMQsRG", - "width": 640, - "height": 360, - "timestampUs": 2053643000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AMSaSBEwb9cjwL", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_Ash3ybc8XXbukk", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 265.1, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSzotHy6zMQsRG", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 2414.2, - "packetsLost": 0, - "fps": 29, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 329, - "outgoingVideoFramesAccepted": 329, - "outgoingVideoFramesDropped": 13, - "outgoingVideoFramesCoalesced": 13, - "outgoingVideoFramesCaptured": 316, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 28.84, - "outgoingVideoTargetFps": 30, - "outgoingVideoMaxQueueAgeMs": 34, - "outgoingVideoMaxPushLatencyMs": 35, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 42, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_iYVBCmnrZuSb", - "trackSid": "TR_Ash3ybc8XXbukk", - "kind": "audio", - "bitrateKbps": 265.1, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.318, - "codec": "audio/opus" - }, - { - "participantSid": "PA_iYVBCmnrZuSb", - "trackSid": "TR_AMSaSBEwb9cjwL", - "kind": "audio", - "bitrateKbps": 99, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.324, - "codec": "audio/opus" - }, - { - "participantSid": "PA_iYVBCmnrZuSb", - "trackSid": "TR_VSzotHy6zMQsRG", - "kind": "video", - "bitrateKbps": 2414.2, - "packetsLost": 0, - "jitterMs": 4, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-strict-single-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-strict-single-20260605.json deleted file mode 100644 index f49735a53..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-strict-single-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:35:40.815Z", - "endedAt": "2026-06-05T22:36:13.026Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-6332-943a2620-7cc8-4881-b1a9-6d8577e9c434", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-6332", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-6332", - "subscriberIdentity": "fluxer-native-subscriber-6332", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 5000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-360p30-strict-single-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 15.03 below 28.5", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 6168, - "inboundVideoFrames": 461, - "strictAudioFrames": 299, - "strictVideoFrames": 451, - "videoCallbacks": 461, - "videoBytes": 159321600, - "receivedFps": 15.03, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 137, - "maxAudioFrameGapMs": 110, - "maxVideoFrameGapOverallMs": 137, - "maxAudioFrameGapOverallMs": 110, - "avDriftMs": 22, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 451 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 30, - "startedAtMs": 1780698941926, - "stoppedAtMs": null, - "framesPushed": 933, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 63, - "lastPushAtMs": 1780698972994, - "elapsedMs": 31105, - "producedFps": 30 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780698943004, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-6332", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-6332", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSBtiuMi7Xz4iL", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AsSNqz63GYafY3", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AMjupdo38YrBp7", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_FcwLkjDchP7J", - "trackSid": "TR_VSBtiuMi7Xz4iL", - "width": 640, - "height": 360, - "timestampUs": 1850800000 - }, - "publisherStats": { - "rttMs": 2, - "outbound": [ - { - "trackSid": "TR_AsSNqz63GYafY3", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 261.4, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMjupdo38YrBp7", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99.5, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSBtiuMi7Xz4iL", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1258.5, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 930, - "outgoingVideoFramesAccepted": 930, - "outgoingVideoFramesDropped": 402, - "outgoingVideoFramesCoalesced": 402, - "outgoingVideoFramesCaptured": 527, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 17, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 62, - "outgoingVideoMaxPushLatencyMs": 62, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 60, - "adaptiveSendTier": "fps15", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 2, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_FcwLkjDchP7J", - "trackSid": "TR_AsSNqz63GYafY3", - "kind": "audio", - "bitrateKbps": 261.4, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.319, - "codec": "audio/opus" - }, - { - "participantSid": "PA_FcwLkjDchP7J", - "trackSid": "TR_AMjupdo38YrBp7", - "kind": "audio", - "bitrateKbps": 99.5, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.32, - "codec": "audio/opus" - }, - { - "participantSid": "PA_FcwLkjDchP7J", - "trackSid": "TR_VSBtiuMi7Xz4iL", - "kind": "video", - "bitrateKbps": 1258.5, - "packetsLost": 0, - "jitterMs": 3, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-strict-single-after-fix-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-strict-single-after-fix-20260605.json deleted file mode 100644 index 3a5e19297..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-360p30-strict-single-after-fix-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:39:58.041Z", - "endedAt": "2026-06-05T22:40:30.271Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-3512-db87cf14-fb04-4daf-84ee-a577ef39b456", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-3512", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-3512", - "subscriberIdentity": "fluxer-native-subscriber-3512", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 5000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-360p30-strict-single-after-fix-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 16.35 below 28.5", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 6178, - "inboundVideoFrames": 503, - "strictAudioFrames": 297, - "strictVideoFrames": 490, - "videoCallbacks": 503, - "videoBytes": 173836800, - "receivedFps": 16.35, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 244, - "maxAudioFrameGapMs": 180, - "maxVideoFrameGapOverallMs": 244, - "maxAudioFrameGapOverallMs": 180, - "avDriftMs": 60, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 490 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 30, - "startedAtMs": 1780699199141, - "stoppedAtMs": null, - "framesPushed": 931, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 155, - "lastPushAtMs": 1780699230263, - "elapsedMs": 31136, - "producedFps": 29.9 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780699200252, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-3512", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSFLKMrtaZRUAL", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMTrMJygXoaHT8", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AseREdJhgwsKHc", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-3512", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_BbRoKiddPYPe", - "trackSid": "TR_VSFLKMrtaZRUAL", - "width": 640, - "height": 360, - "timestampUs": 2108079000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_VSFLKMrtaZRUAL", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1257.6, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AseREdJhgwsKHc", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 257.1, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AMTrMJygXoaHT8", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99.1, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 927, - "outgoingVideoFramesAccepted": 927, - "outgoingVideoFramesDropped": 405, - "outgoingVideoFramesCoalesced": 405, - "outgoingVideoFramesCaptured": 521, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 16.8, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 74, - "outgoingVideoMaxPushLatencyMs": 76, - "outgoingAudioBufferTargetMs": 300, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 1, - "outgoingAudioMaxFrameGapMs": 153, - "adaptiveSendTier": "fps15", - "adaptiveSendReason": "sendLatencyPressure" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 1, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_BbRoKiddPYPe", - "trackSid": "TR_AseREdJhgwsKHc", - "kind": "audio", - "bitrateKbps": 252.8, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.337, - "codec": "audio/opus" - }, - { - "participantSid": "PA_BbRoKiddPYPe", - "trackSid": "TR_VSFLKMrtaZRUAL", - "kind": "video", - "bitrateKbps": 1169, - "packetsLost": 0, - "jitterMs": 3, - "codec": "video/VP8" - }, - { - "participantSid": "PA_BbRoKiddPYPe", - "trackSid": "TR_AMTrMJygXoaHT8", - "kind": "audio", - "bitrateKbps": 97.5, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.315, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-720p15-strict-single-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-720p15-strict-single-20260605.json deleted file mode 100644 index 95b68bfe1..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-720p15-strict-single-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:31:55.148Z", - "endedAt": "2026-06-05T22:32:27.616Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-1340-0367e318-dcfc-498a-8724-0c3982583d5d", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-1340", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-1340", - "subscriberIdentity": "fluxer-native-subscriber-1340", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 8000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-720p15-strict-single-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 11.06 below 14.25; max video frame gap 363ms exceeds 250ms", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 6230, - "inboundVideoFrames": 334, - "strictAudioFrames": 298, - "strictVideoFrames": 331, - "videoCallbacks": 334, - "videoBytes": 461721600, - "receivedFps": 11.06, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 363, - "maxAudioFrameGapMs": 108, - "maxVideoFrameGapOverallMs": 363, - "maxAudioFrameGapOverallMs": 108, - "avDriftMs": 38, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 331 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780698716263, - "stoppedAtMs": null, - "framesPushed": 471, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 93, - "lastPushAtMs": 1780698747600, - "elapsedMs": 31355, - "producedFps": 15.02 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780698717342, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-1340", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-1340", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VSYp54vvSodQtk", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMtAPT5gLdZFiG", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsyzCPFpjfCZ52", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_f3pUVwWqpSf9", - "trackSid": "TR_VSYp54vvSodQtk", - "width": 1280, - "height": 720, - "timestampUs": 1625403000 - }, - "publisherStats": { - "rttMs": 3, - "outbound": [ - { - "trackSid": "TR_AsyzCPFpjfCZ52", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 324.8, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSYp54vvSodQtk", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 5042, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMtAPT5gLdZFiG", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 99.4, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 465, - "outgoingVideoFramesAccepted": 465, - "outgoingVideoFramesDropped": 9, - "outgoingVideoFramesCoalesced": 9, - "outgoingVideoFramesCaptured": 456, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.71, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 66, - "outgoingVideoMaxPushLatencyMs": 68, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 47, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 3, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_f3pUVwWqpSf9", - "trackSid": "TR_AMtAPT5gLdZFiG", - "kind": "audio", - "bitrateKbps": 99.4, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.32, - "codec": "audio/opus" - }, - { - "participantSid": "PA_f3pUVwWqpSf9", - "trackSid": "TR_VSYp54vvSodQtk", - "kind": "video", - "bitrateKbps": 5042, - "packetsLost": 0, - "jitterMs": 33, - "codec": "video/VP8" - }, - { - "participantSid": "PA_f3pUVwWqpSf9", - "trackSid": "TR_AsyzCPFpjfCZ52", - "kind": "audio", - "bitrateKbps": 324.8, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.344, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-720p30-strict-single-20260605.json b/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-720p30-strict-single-20260605.json deleted file mode 100644 index 303c49299..000000000 --- a/fluxer_desktop/native-media-reports/windows-parallels-arm64/vp8-720p30-strict-single-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:30:59.267Z", - "endedAt": "2026-06-05T22:31:31.489Z", - "platform": "win32", - "arch": "arm64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-5096-0b21498f-6c31-4ed8-8e45-930895fdc2d2", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-5096", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-5096", - "subscriberIdentity": "fluxer-native-subscriber-5096", - "required": true, - "strict": true, - "timeoutMs": 120000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 30000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": 0, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 8000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\Mac\\Home\\Development\\fluxer\\fluxer_desktop\\native-media-reports\\windows-parallels-arm64\\vp8-720p30-strict-single-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 15.97 below 28.5", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "runtime_prerequisite_missing", - "detail": "missing nvcuda.dll, nvEncodeAPI64.dll" - }, - "metrics": { - "inboundAudioFrames": 6178, - "inboundVideoFrames": 482, - "strictAudioFrames": 298, - "strictVideoFrames": 479, - "videoCallbacks": 482, - "videoBytes": 666316800, - "receivedFps": 15.97, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 102, - "maxAudioFrameGapMs": 130, - "maxVideoFrameGapOverallMs": 168, - "maxAudioFrameGapOverallMs": 130, - "avDriftMs": 43, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 479 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 30, - "startedAtMs": 1780698660405, - "stoppedAtMs": null, - "framesPushed": 933, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 68, - "lastPushAtMs": 1780698691475, - "elapsedMs": 31087, - "producedFps": 30.01 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780698661479, - "error": null, - "participants": [ - { - "identity": "fluxer-native-subscriber-5096", - "trackCount": 0, - "tracks": [] - }, - { - "identity": "fluxer-native-publisher-5096", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_VS2iFXiujdx95V", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - }, - { - "sid": "TR_AMNf9uVyTmBcZy", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsZpfieifQxEYA", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - } - ] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_XJp9ZzhWYzHd", - "trackSid": "TR_VS2iFXiujdx95V", - "width": 1280, - "height": 720, - "timestampUs": 1569321000 - }, - "publisherStats": { - "rttMs": 1, - "outbound": [ - { - "trackSid": "TR_AMNf9uVyTmBcZy", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 98.4, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_AsZpfieifQxEYA", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 269.5, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VS2iFXiujdx95V", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 5155.1, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 930, - "outgoingVideoFramesAccepted": 930, - "outgoingVideoFramesDropped": 403, - "outgoingVideoFramesCoalesced": 403, - "outgoingVideoFramesCaptured": 526, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 16.97, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 45, - "outgoingVideoMaxPushLatencyMs": 46, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 57, - "adaptiveSendTier": "fps15", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 3, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_XJp9ZzhWYzHd", - "trackSid": "TR_VS2iFXiujdx95V", - "kind": "video", - "bitrateKbps": 5155.1, - "packetsLost": 0, - "jitterMs": 11, - "codec": "video/VP8" - }, - { - "participantSid": "PA_XJp9ZzhWYzHd", - "trackSid": "TR_AsZpfieifQxEYA", - "kind": "audio", - "bitrateKbps": 269.5, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_XJp9ZzhWYzHd", - "trackSid": "TR_AMNf9uVyTmBcZy", - "kind": "audio", - "bitrateKbps": 98.4, - "packetsLost": 0, - "jitterMs": 1, - "audioLevel": 0.336, - "codec": "audio/opus" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-h264-single-egress-adaptive-20260605.json b/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-h264-single-egress-adaptive-20260605.json deleted file mode 100644 index dfa811eab..000000000 --- a/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-h264-single-egress-adaptive-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T21:57:48.945Z", - "endedAt": "2026-06-05T21:58:12.868Z", - "platform": "win32", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-1272-581b0bd8-4044-45a9-bb0d-695ca988d59c", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-1272", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-1272", - "subscriberIdentity": "fluxer-native-subscriber-1272", - "required": true, - "strict": true, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 20000, - "width": 1280, - "height": 720, - "expectedWidth": 1280, - "expectedHeight": 720, - "fps": 30, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 80, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": "high", - "screenCodecs": ["h264"], - "expectedScreenCodecs": ["h264"], - "codec": "h264", - "expectedScreenCodec": "video/H264", - "secondaryPublisher": false, - "secondaryPublisherCodec": "h264", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H264", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 5000000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\f\\fluxer_desktop\\native\\webrtc-sender\\native-media-reports\\windows-mx450-livekit-h264-single-egress-adaptive-20260605.json", - "verbose": false, - "scenarioName": "codec-h264", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 1280x720", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/H264", - "pass": true - }, - { - "name": "subscriber inbound video codec video/H264", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": "received FPS 6.59 below 28.5; max video frame gap 552ms exceeds 250ms", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 4315, - "inboundVideoFrames": 136, - "strictAudioFrames": 199, - "strictVideoFrames": 132, - "videoCallbacks": 135, - "videoBytes": 186624000, - "receivedFps": 6.59, - "requiredFps": 28.5, - "maxVideoFrameGapMs": 552, - "maxAudioFrameGapMs": 112, - "maxVideoFrameGapOverallMs": 557, - "maxAudioFrameGapOverallMs": 112, - "avDriftMs": 22, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "1280x720": 132 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 30, - "startedAtMs": 1780696671200, - "stoppedAtMs": null, - "framesPushed": 650, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 64, - "lastPushAtMs": 1780696692841, - "elapsedMs": 21772, - "producedFps": 29.85 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780696672853, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-1272", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AM3TQzwbFTcoXT", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_AsW4JLnmWG6cce", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VSxrpLn4EJfUEg", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/H264", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-1272", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_7EoucgabpBG8", - "trackSid": "TR_VSxrpLn4EJfUEg", - "width": 1280, - "height": 720, - "timestampUs": 3306206000 - }, - "publisherStats": { - "rttMs": 15, - "outbound": [ - { - "trackSid": "TR_AM3TQzwbFTcoXT", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 95.3, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VSxrpLn4EJfUEg", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 5106.2, - "packetsLost": 0, - "fps": 11, - "codec": "video/H264" - }, - { - "trackSid": "TR_AsW4JLnmWG6cce", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 300.6, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 627, - "outgoingVideoFramesAccepted": 627, - "outgoingVideoFramesDropped": 262, - "outgoingVideoFramesCoalesced": 262, - "outgoingVideoFramesCaptured": 364, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 17.42, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 35, - "outgoingVideoMaxPushLatencyMs": 42, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 63, - "adaptiveSendTier": "fps15", - "adaptiveSendReason": "encoderEgressPressure" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 18, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_7EoucgabpBG8", - "trackSid": "TR_AsW4JLnmWG6cce", - "kind": "audio", - "bitrateKbps": 296.3, - "packetsLost": 0, - "jitterMs": 6, - "audioLevel": 0.336, - "codec": "audio/opus" - }, - { - "participantSid": "PA_7EoucgabpBG8", - "trackSid": "TR_AM3TQzwbFTcoXT", - "kind": "audio", - "bitrateKbps": 97.1, - "packetsLost": 0, - "jitterMs": 7, - "audioLevel": 0.326, - "codec": "audio/opus" - }, - { - "participantSid": "PA_7EoucgabpBG8", - "trackSid": "TR_VSxrpLn4EJfUEg", - "kind": "video", - "bitrateKbps": 5397.8, - "packetsLost": 0, - "jitterMs": 48, - "codec": "video/H264" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-hevc-failfast-20260605.json b/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-hevc-failfast-20260605.json deleted file mode 100644 index 9507172d7..000000000 --- a/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-hevc-failfast-20260605.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "status": "fail", - "startedAt": "2026-06-05T22:00:49.164Z", - "endedAt": "2026-06-05T22:00:50.336Z", - "platform": "win32", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-3456-3aa0da22-e543-4846-a980-1ef479694d1f", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-3456", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-3456", - "subscriberIdentity": "fluxer-native-subscriber-3456", - "required": true, - "strict": false, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 0, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 15, - "minFpsRatio": 0, - "maxFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxAvDriftMs": 0, - "maxPacketLoss": null, - "requireStableResolution": false, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": null, - "screenCodecs": ["hevc"], - "expectedScreenCodecs": ["hevc"], - "codec": "hevc", - "expectedScreenCodec": "video/H265", - "secondaryPublisher": false, - "secondaryPublisherCodec": "hevc", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/H265", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": true, - "secondaryPublisherScreenSimulcast": true, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": false, - "maxBitrateBps": 400000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": false, - "dataPacket": false, - "subscriptionCycle": false, - "validateServerPublishing": false, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\f\\fluxer_desktop\\native\\webrtc-sender\\native-media-reports\\windows-mx450-livekit-hevc-failfast-20260605.json", - "verbose": false, - "scenarioName": "codec-hevc", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": false - }, - { - "name": "subscriber remote screenshare subscription", - "pass": false - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": false - }, - { - "name": "subscriber video frame callback", - "pass": false - }, - { - "name": "publisher outbound screenshare stats", - "pass": false - }, - { - "name": "subscriber inbound video stats", - "pass": false - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": false - }, - { - "name": "subscriber video resolution 640x360", - "pass": false - }, - { - "name": "subscriber aggregate inbound audio frames >= 3", - "pass": false - }, - { - "name": "subscriber inbound audio stats count >= 1", - "pass": false - }, - { - "name": "publisher local microphone publication", - "pass": false - }, - { - "name": "subscriber remote microphone subscription", - "pass": false - }, - { - "name": "subscriber inbound audio frames >= 3", - "pass": false - }, - { - "name": "publisher outbound microphone stats", - "pass": false - }, - { - "name": "subscriber inbound audio stats", - "pass": false - }, - { - "name": "publisher outbound screenshare codec video/H265", - "pass": false - }, - { - "name": "subscriber inbound video codec video/H265", - "pass": false - } - ], - "skippedReason": null, - "error": "Error: H.265 publishing requires hardware encoder support; missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))", - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 0, - "inboundVideoFrames": 0, - "strictAudioFrames": 0, - "strictVideoFrames": 0, - "videoCallbacks": 0, - "videoBytes": 0, - "receivedFps": 0, - "requiredFps": null, - "maxVideoFrameGapMs": 0, - "maxAudioFrameGapMs": 0, - "maxVideoFrameGapOverallMs": 0, - "maxAudioFrameGapOverallMs": 0, - "avDriftMs": null, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": {}, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": null, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": null, - "error": null, - "participants": [] - }, - "lastVideoMeta": null, - "publisherStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": null, - "outbound": [], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-vp8-360p15-strict-av150-20260605.json b/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-vp8-360p15-strict-av150-20260605.json deleted file mode 100644 index dd637aa2c..000000000 --- a/fluxer_desktop/native-media-reports/windows/windows-mx450-livekit-vp8-360p15-strict-av150-20260605.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "status": "pass", - "startedAt": "2026-06-05T22:00:19.291Z", - "endedAt": "2026-06-05T22:00:37.423Z", - "platform": "win32", - "arch": "x64", - "config": { - "url": "ws://100.79.83.54:17880", - "serverApiUrl": "http://100.79.83.54:17880", - "apiKey": "devkey", - "room": "fluxer-webrtc-harness-6872-db238db9-c867-4b03-8af9-9740942a674c", - "externalTokens": false, - "publisherIdentity": "fluxer-native-publisher-6872", - "secondaryPublisherIdentity": "fluxer-native-publisher-2-6872", - "subscriberIdentity": "fluxer-native-subscriber-6872", - "required": true, - "strict": true, - "timeoutMs": 30000, - "connectTimeoutMs": 30000, - "disconnectTimeoutMs": 10000, - "probeTimeoutMs": 1500, - "durationMs": 15000, - "width": 640, - "height": 360, - "expectedWidth": 640, - "expectedHeight": 360, - "fps": 15, - "minFpsRatio": 0.95, - "maxFrameGapMs": 250, - "maxAudioFrameGapMs": 250, - "maxAvDriftMs": 150, - "maxPacketLoss": null, - "requireStableResolution": true, - "videoPattern": "gradient", - "videoInput": "bgra", - "subscriberVideoQuality": "high", - "screenCodecs": ["vp8"], - "expectedScreenCodecs": ["vp8"], - "codec": "vp8", - "expectedScreenCodec": "video/VP8", - "secondaryPublisher": false, - "secondaryPublisherCodec": "vp8", - "secondaryPublisherCodecExplicit": false, - "expectedSecondaryPublisherCodec": "video/VP8", - "expectedSecondaryPublisherCodecExplicit": false, - "screenSimulcast": false, - "secondaryPublisherScreenSimulcast": false, - "adaptiveSend": true, - "minVideoFps": 15, - "minResolutionScale": 0.5, - "maxAudioBufferMs": 750, - "secondaryPublisherMicrophone": true, - "secondaryPublisherScreenAudio": true, - "maxBitrateBps": 1200000, - "sampleRate": 48000, - "channels": 1, - "audioChunkMs": 20, - "microphone": true, - "screenAudio": true, - "dataPacket": true, - "subscriptionCycle": true, - "validateServerPublishing": true, - "camera": false, - "cameraDeviceId": null, - "expectedCameraCodec": null, - "expectedHardwareEncoder": null, - "reportPath": "C:\\f\\fluxer_desktop\\native\\webrtc-sender\\native-media-reports\\windows-mx450-livekit-vp8-360p15-strict-av150-20260605.json", - "verbose": false, - "scenarioName": "codec-vp8", - "apiSecret": "", - "e2eeKey": null, - "publisherToken": null, - "secondaryPublisherToken": null, - "subscriberToken": null - }, - "checks": [ - { - "name": "publisher connected", - "pass": true - }, - { - "name": "subscriber connected", - "pass": true - }, - { - "name": "publisher local screenshare publication", - "pass": true - }, - { - "name": "subscriber remote screenshare subscription", - "pass": true - }, - { - "name": "subscriber inbound video frames >= 3", - "pass": true - }, - { - "name": "subscriber video frame callback", - "pass": true - }, - { - "name": "publisher outbound screenshare stats", - "pass": true - }, - { - "name": "subscriber inbound video stats", - "pass": true - }, - { - "name": "subscriber inbound video stats count >= 1", - "pass": true - }, - { - "name": "subscriber video resolution 640x360", - "pass": true - }, - { - "name": "subscriber aggregate inbound audio frames >= 6", - "pass": true - }, - { - "name": "subscriber inbound audio stats count >= 2", - "pass": true - }, - { - "name": "publisher local microphone publication", - "pass": true - }, - { - "name": "subscriber remote microphone subscription", - "pass": true - }, - { - "name": "subscriber inbound audio frames >= 6", - "pass": true - }, - { - "name": "publisher outbound microphone stats", - "pass": true - }, - { - "name": "subscriber inbound audio stats", - "pass": true - }, - { - "name": "publisher local screen-share audio publication", - "pass": true - }, - { - "name": "subscriber remote screen-share audio subscription", - "pass": true - }, - { - "name": "publisher outbound screen-share audio stats", - "pass": true - }, - { - "name": "subscriber data packet received", - "pass": true - }, - { - "name": "publisher outbound screenshare codec video/VP8", - "pass": true - }, - { - "name": "subscriber inbound video codec video/VP8", - "pass": true - }, - { - "name": "server publishing API participants listed", - "pass": true - }, - { - "name": "server sees publisher screenshare publication", - "pass": true - }, - { - "name": "server sees publisher microphone publication", - "pass": true - }, - { - "name": "server sees publisher screen-share audio publication", - "pass": true - } - ], - "skippedReason": null, - "error": null, - "hardwareEncoder": { - "available": false, - "backend": "none", - "compiled": true, - "runtime": false, - "codecs": [], - "zeroCopy": false, - "nativeInputs": [], - "reason": "no_supported_devices", - "detail": "missing NVENC encode session (checked 1 CUDA device; last status NV_ENC_ERR_UNSUPPORTED_DEVICE (2))" - }, - "metrics": { - "inboundAudioFrames": 3201, - "inboundVideoFrames": 236, - "strictAudioFrames": 150, - "strictVideoFrames": 221, - "videoCallbacks": 235, - "videoBytes": 81216000, - "receivedFps": 14.71, - "requiredFps": 14.25, - "maxVideoFrameGapMs": 135, - "maxAudioFrameGapMs": 104, - "maxVideoFrameGapOverallMs": 135, - "maxAudioFrameGapOverallMs": 104, - "avDriftMs": 15, - "maxObservedPacketLoss": 0, - "maxObservedPacketLossDelta": 0, - "videoResolutionCounts": { - "640x360": 221 - }, - "videoResolutionMismatchCount": 0, - "droppedVideoFrameCallbacks": 0, - "publisherVideoPump": { - "input": "bgra", - "pattern": "gradient", - "targetFps": 15, - "startedAtMs": 1780696821343, - "stoppedAtMs": null, - "framesPushed": 242, - "framesRejected": 0, - "errors": 0, - "maxPushGapMs": 81, - "lastPushAtMs": 1780696837414, - "elapsedMs": 16181, - "producedFps": 14.96 - }, - "secondaryPublisherVideoPump": null, - "serverPublishing": { - "fetchedAtMs": 1780696822415, - "error": null, - "participants": [ - { - "identity": "fluxer-native-publisher-6872", - "trackCount": 3, - "tracks": [ - { - "sid": "TR_AMZncs3mMUV8XL", - "name": "mic", - "kind": "audio", - "source": "microphone", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_As8q693K2N6RV2", - "name": "screen-audio", - "kind": "audio", - "source": "screenshareAudio", - "mimeType": "audio/red", - "muted": false - }, - { - "sid": "TR_VScEzJLWokWJcx", - "name": "screen", - "kind": "video", - "source": "screenshare", - "mimeType": "video/VP8", - "muted": false - } - ] - }, - { - "identity": "fluxer-native-subscriber-6872", - "trackCount": 0, - "tracks": [] - } - ] - }, - "lastVideoMeta": { - "participantSid": "PA_EeCfpcRvJPEF", - "trackSid": "TR_VScEzJLWokWJcx", - "width": 640, - "height": 360, - "timestampUs": 3450776000 - }, - "publisherStats": { - "rttMs": 4, - "outbound": [ - { - "trackSid": "TR_As8q693K2N6RV2", - "source": "screenshareAudio", - "kind": "audio", - "bitrateKbps": 263.5, - "packetsLost": 0, - "codec": "audio/opus" - }, - { - "trackSid": "TR_VScEzJLWokWJcx", - "source": "screenshare", - "kind": "video", - "bitrateKbps": 1233.4, - "packetsLost": 0, - "fps": 15, - "codec": "video/VP8" - }, - { - "trackSid": "TR_AMZncs3mMUV8XL", - "source": "microphone", - "kind": "audio", - "bitrateKbps": 97.9, - "packetsLost": 0, - "codec": "audio/opus" - } - ], - "inbound": [], - "send": { - "outgoingVideoQueueDepth": 1, - "outgoingVideoFramesProduced": 241, - "outgoingVideoFramesAccepted": 241, - "outgoingVideoFramesDropped": 4, - "outgoingVideoFramesCoalesced": 4, - "outgoingVideoFramesCaptured": 236, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 14.71, - "outgoingVideoTargetFps": 15, - "outgoingVideoMaxQueueAgeMs": 67, - "outgoingVideoMaxPushLatencyMs": 69, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 36, - "adaptiveSendTier": "full", - "adaptiveSendReason": "stable" - } - }, - "secondaryPublisherStats": null, - "subscriberStats": { - "rttMs": 4, - "outbound": [], - "inbound": [ - { - "participantSid": "PA_EeCfpcRvJPEF", - "trackSid": "TR_As8q693K2N6RV2", - "kind": "audio", - "bitrateKbps": 263.1, - "packetsLost": 0, - "jitterMs": 3, - "audioLevel": 0.318, - "codec": "audio/opus" - }, - { - "participantSid": "PA_EeCfpcRvJPEF", - "trackSid": "TR_AMZncs3mMUV8XL", - "kind": "audio", - "bitrateKbps": 97.8, - "packetsLost": 0, - "jitterMs": 2, - "audioLevel": 0.307, - "codec": "audio/opus" - }, - { - "participantSid": "PA_EeCfpcRvJPEF", - "trackSid": "TR_VScEzJLWokWJcx", - "kind": "video", - "bitrateKbps": 1231.8, - "packetsLost": 0, - "jitterMs": 4, - "codec": "video/VP8" - } - ], - "send": { - "outgoingVideoQueueDepth": 0, - "outgoingVideoFramesProduced": 0, - "outgoingVideoFramesAccepted": 0, - "outgoingVideoFramesDropped": 0, - "outgoingVideoFramesCoalesced": 0, - "outgoingVideoFramesCaptured": 0, - "outgoingVideoCaptureFailures": 0, - "outgoingVideoEffectiveFps": 0, - "outgoingVideoTargetFps": 0, - "outgoingVideoMaxQueueAgeMs": 0, - "outgoingVideoMaxPushLatencyMs": 0, - "outgoingAudioBufferTargetMs": 200, - "outgoingAudioBufferMaxMs": 750, - "outgoingAudioUnderruns": 0, - "outgoingAudioRebuffers": 0, - "outgoingAudioMaxFrameGapMs": 0, - "adaptiveSendTier": "idle", - "adaptiveSendReason": "notPublishing" - } - } - } -} diff --git a/fluxer_desktop/native/encoder-ring/Cargo.lock b/fluxer_desktop/native/encoder-ring/Cargo.lock index ac94faa37..a6c4e1483 100644 --- a/fluxer_desktop/native/encoder-ring/Cargo.lock +++ b/fluxer_desktop/native/encoder-ring/Cargo.lock @@ -64,15 +64,6 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -239,16 +230,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags", - "objc2", -] - [[package]] name = "either" version = "1.16.0" @@ -268,10 +249,6 @@ dependencies = [ "bindgen", "criterion", "libloading 0.9.0", - "objc2", - "objc2-core-foundation", - "objc2-core-video", - "objc2-io-surface", "windows", ] @@ -405,96 +382,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", - "block2", - "dispatch2", - "libc", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", - "objc2-metal", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags", - "objc2", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags", - "libc", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" -dependencies = [ - "bitflags", - "objc2", - "objc2-foundation", -] - [[package]] name = "once_cell" version = "1.21.4" diff --git a/fluxer_desktop/native/encoder-ring/Cargo.toml b/fluxer_desktop/native/encoder-ring/Cargo.toml index d2f0c84c0..e5376af91 100644 --- a/fluxer_desktop/native/encoder-ring/Cargo.toml +++ b/fluxer_desktop/native/encoder-ring/Cargo.toml @@ -36,12 +36,6 @@ bindgen-sdk = ["dep:bindgen"] [dev-dependencies] criterion = "0.8" -[target.'cfg(target_os = "macos")'.dev-dependencies] -objc2 = "0.6" -objc2-core-video = {version = "0.3", features = ["CVPixelBuffer", "CVPixelBufferIOSurface", "CVImageBuffer", "CVBuffer", "CVPixelFormatDescription", "CVReturn", "CVBase", "objc2-io-surface"]} -objc2-core-foundation = {version = "0.3", features = ["CFDictionary", "CFNumber", "CFString", "CFBase"]} -objc2-io-surface = {version = "0.3", features = ["IOSurfaceRef", "objc2-core-foundation"]} - [[bench]] name = "ring" harness = false diff --git a/fluxer_desktop/native/encoder-ring/src/d3d11.rs b/fluxer_desktop/native/encoder-ring/src/d3d11.rs index b67e08f60..3f2819459 100644 --- a/fluxer_desktop/native/encoder-ring/src/d3d11.rs +++ b/fluxer_desktop/native/encoder-ring/src/d3d11.rs @@ -438,36 +438,4 @@ mod tests { assert_eq!(h2.slot_index, 3); assert_eq!(h2.raw_handle, 0xdead); } - - #[test] - #[cfg(target_os = "windows")] - fn windows_real_keyed_mutex_eight_slots_round_trip() { - let backend_result = D3D11KeyedMutexBackend::new(); - let mut backend = match backend_result { - Ok(b) => b, - Err(BackendError::PlatformUnsupported { .. }) => return, - Err(other) => unreachable!("unexpected backend init err: {other:?}"), - }; - let slots_result = backend.create_slots(64, 64, TextureFormat::Nv12); - let slots = match slots_result { - Ok(s) => s, - Err(BackendError::PlatformUnsupported { .. }) => return, - Err(other) => unreachable!("unexpected create_slots err: {other:?}"), - }; - assert_eq!(slots.len(), 8); - for (idx, slot) in slots.iter().enumerate() { - assert_eq!(slot.slot_index, idx as u32); - assert_ne!(slot.raw_handle, 0); - assert_eq!(slot.width, 64); - } - let slot = slots[0].clone(); - backend.acquire_write(&slot, 0).expect("acquire key=0"); - backend.release_write(&slot, 1).expect("release key=1"); - assert!(backend.poll_complete(&slot)); - backend.mark_consumed(&slot); - assert!(!backend.poll_complete(&slot)); - backend.acquire_write(&slot, 1).expect("acquire key=1"); - backend.release_write(&slot, 2).expect("release key=2"); - assert!(backend.poll_complete(&slot)); - } } diff --git a/fluxer_desktop/native/encoder-ring/src/vt_compression_macos.rs b/fluxer_desktop/native/encoder-ring/src/vt_compression_macos.rs index 5c871d8dd..c6a36b606 100644 --- a/fluxer_desktop/native/encoder-ring/src/vt_compression_macos.rs +++ b/fluxer_desktop/native/encoder-ring/src/vt_compression_macos.rs @@ -28,8 +28,6 @@ type OsStatus = i32; pub(crate) type IoSurfaceRef = *mut c_void; const KCM_VIDEO_CODEC_TYPE_H264: u32 = u32::from_be_bytes(*b"avc1"); -#[cfg(test)] -const KCV_PIXEL_FORMAT_TYPE_420_YPCBCR8_BIPLANAR_VIDEO: u32 = u32::from_be_bytes(*b"420v"); const NOERR: OsStatus = 0; const KCM_TIME_FLAGS_VALID: u32 = 1; const KCF_NUMBER_SINT32_TYPE: i32 = 3; @@ -840,107 +838,10 @@ impl VideoToolboxHandoff for VtCompressionHandoff { } } -#[cfg(test)] -pub(crate) fn make_cv_nv12_pixel_buffer( - width: u32, - height: u32, -) -> objc2_core_foundation::CFRetained { - use core::ptr::NonNull; - use objc2_core_foundation::{CFDictionary, CFRetained}; - use objc2_core_video::CVPixelBuffer; - - let mut empty_keys: [*const c_void; 0] = []; - let mut empty_vals: [*const c_void; 0] = []; - let iosurf_dict: CFRetained = unsafe { - CFDictionary::new( - None, - empty_keys.as_mut_ptr(), - empty_vals.as_mut_ptr(), - 0, - &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks, - &objc2_core_foundation::kCFTypeDictionaryValueCallBacks, - ) - .expect("iosurf empty dict") - }; - let key_ref: &objc2_core_foundation::CFString = - unsafe { objc2_core_video::kCVPixelBufferIOSurfacePropertiesKey }; - let key_ptr: *const c_void = key_ref as *const _ as *const c_void; - let val_ptr: *const c_void = &*iosurf_dict as *const _ as *const c_void; - let mut keys = [key_ptr]; - let mut vals = [val_ptr]; - let attrs: CFRetained = unsafe { - CFDictionary::new( - None, - keys.as_mut_ptr(), - vals.as_mut_ptr(), - 1, - &objc2_core_foundation::kCFTypeDictionaryKeyCallBacks, - &objc2_core_foundation::kCFTypeDictionaryValueCallBacks, - ) - .expect("attrs dict") - }; - let mut out: *mut CVPixelBuffer = std::ptr::null_mut(); - let status = unsafe { - objc2_core_video::CVPixelBufferCreate( - None, - width as usize, - height as usize, - KCV_PIXEL_FORMAT_TYPE_420_YPCBCR8_BIPLANAR_VIDEO, - Some(&attrs), - NonNull::new(&mut out).expect("out pointer non-null"), - ) - }; - assert_eq!(status, 0, "CVPixelBufferCreate succeeded"); - assert!(!out.is_null(), "CVPixelBufferCreate produced buffer"); - unsafe { CFRetained::from_raw(NonNull::new_unchecked(out)) } -} - #[cfg(test)] mod tests { use super::*; - fn make_handoff() -> VtCompressionHandoff { - VtCompressionHandoff::try_new(EncoderDims::new(1280, 720)).expect("create handoff") - } - - pub(super) fn iosurface_ptr_from_pb(pb: &objc2_core_video::CVPixelBuffer) -> *mut c_void { - let s = objc2_core_video::CVPixelBufferGetIOSurface(Some(pb)); - let surface = s.expect("pixel buffer must be IOSurface-backed"); - let raw: *const objc2_io_surface::IOSurfaceRef = &*surface; - raw as *mut c_void - } - - #[test] - fn vt_compression_handoff_encodes_one_frame() { - let mut handoff = make_handoff(); - let pb = make_cv_nv12_pixel_buffer(1280, 720); - let surface = iosurface_ptr_from_pb(&pb); - let slot = handoff - .register_slot(surface as u64, 0, EncoderDims::new(1280, 720)) - .expect("register slot"); - let params = PicParams::new(0, false); - handoff - .encode_shared_async(slot, 0, EncoderDims::new(1280, 720), params) - .expect("encode async"); - handoff.wait_for_completion().expect("complete frames"); - let payload = handoff.poll_completed(slot).expect("polled bitstream"); - assert!(!payload.data.is_empty(), "encoded payload non-empty"); - assert!( - payload.data.len() <= MAX_BITSTREAM_BYTES, - "encoded payload bounded" - ); - } - - struct RecordingCallback { - seen: Vec<(u64, u32)>, - } - - impl crate::encoder_handoff::EncoderCompletionCallback for RecordingCallback { - fn on_complete(&mut self, sequence: u64, encoded_bytes: u32) { - self.seen.push((sequence, encoded_bytes)); - } - } - #[test] fn output_callback_counts_failed_completions() { let ctx = VtCallbackContext { @@ -971,106 +872,6 @@ mod tests { ); } - #[test] - fn failed_completion_count_starts_at_zero_and_survives_encode() { - let mut handoff = make_handoff(); - assert_eq!(handoff.failed_completion_count(), 0); - let pb = make_cv_nv12_pixel_buffer(1280, 720); - let surface = iosurface_ptr_from_pb(&pb); - let slot = handoff - .register_slot(surface as u64, 0, EncoderDims::new(1280, 720)) - .expect("register slot"); - handoff - .encode_shared_async( - slot, - 0, - EncoderDims::new(1280, 720), - PicParams::new(0, false), - ) - .expect("encode async"); - handoff.wait_for_completion().expect("complete frames"); - assert_eq!(handoff.failed_completion_count(), 0); - } - - #[test] - fn session_rebuild_bounded_by_named_cap() { - let mut handoff = make_handoff(); - assert_eq!(handoff.session_rebuild_count(), 0); - for expected in 1..=VT_SESSION_REBUILD_MAX { - assert!(handoff.try_rebuild_session(), "rebuild within cap succeeds"); - assert_eq!(handoff.session_rebuild_count(), expected); - } - assert!( - !handoff.try_rebuild_session(), - "rebuild beyond cap rejected" - ); - assert_eq!(handoff.session_rebuild_count(), VT_SESSION_REBUILD_MAX); - } - - #[test] - fn session_still_encodes_after_rebuild() { - let mut handoff = make_handoff(); - assert!(handoff.try_rebuild_session(), "first rebuild succeeds"); - let pb = make_cv_nv12_pixel_buffer(1280, 720); - let surface = iosurface_ptr_from_pb(&pb); - let slot = handoff - .register_slot(surface as u64, 0, EncoderDims::new(1280, 720)) - .expect("register slot"); - handoff - .encode_shared_async( - slot, - 0, - EncoderDims::new(1280, 720), - PicParams::new(0, false), - ) - .expect("encode async after rebuild"); - handoff.wait_for_completion().expect("complete frames"); - let payload = handoff.poll_completed(slot).expect("polled bitstream"); - assert!(!payload.data.is_empty(), "encoded payload non-empty"); - assert_eq!(handoff.session_rebuild_count(), 1); - } - - #[test] - fn encode_shared_uses_capture_pts_when_present() { - let mut handoff = make_handoff(); - let pb = make_cv_nv12_pixel_buffer(1280, 720); - let surface = iosurface_ptr_from_pb(&pb); - let slot = handoff - .register_slot(surface as u64, 0, EncoderDims::new(1280, 720)) - .expect("register slot"); - assert_eq!(slot.slot_index, 0); - let dims = EncoderDims::new(1280, 720); - let submission = - EncoderSubmission::new(surface as u64, 0, dims, 0).with_capture_pts_us(987_654); - let mut cb = RecordingCallback { seen: Vec::new() }; - VideoToolboxHandoff::encode_shared(&mut handoff, submission, &mut cb) - .expect("encode shared with capture pts"); - handoff.wait_for_completion().expect("complete frames"); - let payload = handoff.poll_completed(slot).expect("polled bitstream"); - assert_eq!(payload.pts_us, 987_654, "real capture pts threaded through"); - assert_eq!(cb.seen.len(), 1); - } - - #[test] - fn encode_shared_synthesizes_pts_when_capture_pts_absent() { - let mut handoff = make_handoff(); - let pb = make_cv_nv12_pixel_buffer(1280, 720); - let surface = iosurface_ptr_from_pb(&pb); - let slot = handoff - .register_slot(surface as u64, 0, EncoderDims::new(1280, 720)) - .expect("register slot"); - assert_eq!(slot.slot_index, 0); - let dims = EncoderDims::new(1280, 720); - let submission = EncoderSubmission::new(surface as u64, 0, dims, 0); - assert_eq!(submission.capture_pts_us, None); - let mut cb = RecordingCallback { seen: Vec::new() }; - VideoToolboxHandoff::encode_shared(&mut handoff, submission, &mut cb) - .expect("encode shared without capture pts"); - handoff.wait_for_completion().expect("complete frames"); - let payload = handoff.poll_completed(slot).expect("polled bitstream"); - assert_eq!(payload.pts_us, 0, "sequence 0 synthesizes pts 0"); - } - #[test] fn invalid_session_error_detection_is_exact() { let invalid = EncoderError::EncodeFailed { @@ -1089,38 +890,4 @@ mod tests { }; assert!(!is_invalid_session_error(&other_vendor)); } - - #[test] - fn vt_compression_handoff_handles_back_pressure() { - let mut handoff = make_handoff(); - let pb = make_cv_nv12_pixel_buffer(1280, 720); - let surface = iosurface_ptr_from_pb(&pb); - let slot = handoff - .register_slot(surface as u64, 0, EncoderDims::new(1280, 720)) - .expect("register slot"); - let total: u64 = (COMPLETION_RING_CAPACITY as u64) + 8; - for i in 0..total { - let params = PicParams::new(i * 33_333, false); - handoff - .encode_shared_async(slot, 0, EncoderDims::new(1280, 720), params) - .expect("encode async"); - } - handoff.wait_for_completion().expect("complete frames"); - let pending = handoff.pending_completion(); - assert!( - pending <= COMPLETION_RING_CAPACITY, - "completion ring stays bounded" - ); - let mut drained = 0usize; - while handoff.poll_completed(slot).is_some() { - drained += 1; - if drained > COMPLETION_RING_CAPACITY * 2 { - break; - } - } - assert!( - drained <= COMPLETION_RING_CAPACITY, - "drained respects completion bound" - ); - } } diff --git a/fluxer_desktop/native/encoder-ring/tests/windows_qsv_probe.rs b/fluxer_desktop/native/encoder-ring/tests/windows_qsv_probe.rs deleted file mode 100644 index 849a0cdec..000000000 --- a/fluxer_desktop/native/encoder-ring/tests/windows_qsv_probe.rs +++ /dev/null @@ -1,727 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#![cfg(target_os = "windows")] -use std::ffi::c_void; -use std::ptr; - -use libloading::{Library, Symbol}; -use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0}; -use windows::Win32::Graphics::Direct3D11::{ - D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_SDK_VERSION, - D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread, -}; -use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1}; -use windows::core::Interface; - -const VENDOR_INTEL: u32 = 0x8086; - -const MFX_IMPL_TYPE_HARDWARE: u32 = 2; -const MFX_ACCEL_MODE_NA: u32 = 0; -const MFX_ACCEL_MODE_VIA_D3D11: u32 = 0x0300; - -const MFX_HANDLE_D3D11_DEVICE: u32 = 3; -const MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS: u32 = 11; - -const MFX_VARIANT_TYPE_U32: u32 = 5; -const MFX_VARIANT_VERSION_MAJOR: u8 = 1; -const MFX_VARIANT_VERSION_MINOR: u8 = 1; - -const MFX_ERR_NONE: i32 = 0; -const MFX_WRN_IN_EXECUTION: i32 = 1; - -const MFX_FOURCC_NV12: u32 = u32::from_le_bytes(*b"NV12"); -const MFX_CODEC_AVC: u32 = u32::from_le_bytes(*b"AVC "); -const MFX_RATECONTROL_CBR: u16 = 1; -const MFX_PICSTRUCT_PROGRESSIVE: u16 = 0x01; -const MFX_CHROMAFORMAT_YUV420: u16 = 1; -const MFX_IOPATTERN_IN_VIDEO_MEMORY: u16 = 0x01; -const MFX_IOPATTERN_IN_SYSTEM_MEMORY: u16 = 0x02; - -const FILTER_PROPERTY_IMPL: &[u8] = b"mfxImplDescription.Impl\0"; -const FILTER_PROPERTY_ACCEL: &[u8] = b"mfxImplDescription.AccelerationMode\0"; - -const QSV_DLL_NAME_VPL: &str = "libvpl.dll"; - -#[repr(C)] -#[derive(Clone, Copy, Default)] -struct MfxStructVersion { - minor: u8, - major: u8, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union MfxVariantData { - u32_: u32, - u64_: u64, - ptr: *mut c_void, - pad: [u8; 16], -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct MfxVariant { - version: MfxStructVersion, - type_: u32, - data: MfxVariantData, -} - -#[repr(C)] -#[derive(Default)] -struct MfxFrameInfo { - reserved: [u32; 4], - channel_id: u16, - bit_depth_luma: u16, - bit_depth_chroma: u16, - shift: u16, - frame_id_temporal: u16, - frame_id_priority: u16, - frame_id_view: u16, - frame_id_quality: u16, - four_cc: u32, - width: u16, - height: u16, - crop_x: u16, - crop_y: u16, - crop_w: u16, - crop_h: u16, - frame_rate_extn: u32, - frame_rate_extd: u32, - reserved3: u16, - aspect_ratio_w: u16, - aspect_ratio_h: u16, - pic_struct: u16, - chroma_format: u16, - reserved2: u16, -} - -#[repr(C)] -#[derive(Default)] -struct MfxInfoMfx { - reserved: [u32; 7], - low_power: u16, - brc_param_multiplier: u16, - frame_info: MfxFrameInfo, - codec_id: u32, - codec_profile: u16, - codec_level: u16, - num_thread: u16, - target_usage: u16, - gop_pic_size: u16, - gop_ref_dist: u16, - gop_opt_flag: u16, - idr_interval: u16, - rate_control_method: u16, - init_qp: u16, - buffer_size_in_kb: u16, - target_kbps: u16, - max_kbps: u16, - num_slice: u16, - num_ref_frame: u16, - encoded_order: u16, - union_pad: [u16; 15], -} - -#[repr(C)] -struct MfxVideoParam { - alloc_id: u32, - reserved: [u32; 2], - reserved3: u16, - async_depth: u16, - mfx: MfxInfoMfx, - protected: u16, - io_pattern: u16, - ext_param: *mut c_void, - num_ext_param: u16, - reserved2: u16, -} - -#[repr(C)] -#[derive(Default, Clone, Copy)] -struct MfxPlatform { - code_name: u16, - device_id: u16, - media_adapter_type: u16, - reserved: [u16; 13], -} - -type MfxLoad = unsafe extern "C" fn() -> *mut c_void; -type MfxUnload = unsafe extern "C" fn(*mut c_void); -type MfxCreateConfig = unsafe extern "C" fn(*mut c_void) -> *mut c_void; -type MfxSetConfigFilterProperty = unsafe extern "C" fn(*mut c_void, *const u8, MfxVariant) -> i32; -type MfxCreateSession = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32; -type MfxClose = unsafe extern "C" fn(*mut c_void) -> i32; -type MfxSetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut c_void) -> i32; -type MfxQueryImpl = unsafe extern "C" fn(*mut c_void, *mut i32) -> i32; -type MfxQueryPlatform = unsafe extern "C" fn(*mut c_void, *mut MfxPlatform) -> i32; -type MfxEncodeInit = unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam) -> i32; -type MfxEncodeClose = unsafe extern "C" fn(*mut c_void) -> i32; -type MfxEncodeQuery = - unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam, *mut MfxVideoParam) -> i32; -type MfxGetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32; - -struct Dispatcher { - library: Library, -} - -impl Dispatcher { - fn load() -> Option { - let library = unsafe { Library::new(QSV_DLL_NAME_VPL) }.ok()?; - Some(Self { library }) - } - - fn modern_session(&self, accel_mode: u32) -> (Option<(*mut c_void, *mut c_void)>, Vec) { - let mut log = Vec::new(); - let load: Symbol<'_, MfxLoad> = match unsafe { self.library.get(b"MFXLoad\0") } { - Ok(s) => s, - Err(e) => { - log.push(format!("MFXLoad symbol missing: {e:?}")); - return (None, log); - } - }; - let create_config: Symbol<'_, MfxCreateConfig> = - match unsafe { self.library.get(b"MFXCreateConfig\0") } { - Ok(s) => s, - Err(e) => { - log.push(format!("MFXCreateConfig missing: {e:?}")); - return (None, log); - } - }; - let set_prop: Symbol<'_, MfxSetConfigFilterProperty> = - match unsafe { self.library.get(b"MFXSetConfigFilterProperty\0") } { - Ok(s) => s, - Err(e) => { - log.push(format!("MFXSetConfigFilterProperty missing: {e:?}")); - return (None, log); - } - }; - let create_session: Symbol<'_, MfxCreateSession> = - match unsafe { self.library.get(b"MFXCreateSession\0") } { - Ok(s) => s, - Err(e) => { - log.push(format!("MFXCreateSession missing: {e:?}")); - return (None, log); - } - }; - let loader = unsafe { load() }; - if loader.is_null() { - log.push("MFXLoad returned NULL".to_string()); - return (None, log); - } - log.push(format!("MFXLoad ok loader={loader:p}")); - let impl_cfg = unsafe { create_config(loader) }; - if impl_cfg.is_null() { - log.push("MFXCreateConfig(impl) returned NULL".to_string()); - return (None, log); - } - let impl_variant = MfxVariant { - version: MfxStructVersion { - minor: MFX_VARIANT_VERSION_MINOR, - major: MFX_VARIANT_VERSION_MAJOR, - }, - type_: MFX_VARIANT_TYPE_U32, - data: MfxVariantData { - u32_: MFX_IMPL_TYPE_HARDWARE, - }, - }; - let status_impl = - unsafe { set_prop(impl_cfg, FILTER_PROPERTY_IMPL.as_ptr(), impl_variant) }; - log.push(format!( - "SetConfigFilterProperty(Impl=HARDWARE) status={status_impl}" - )); - if status_impl != MFX_ERR_NONE { - return (None, log); - } - let accel_cfg = unsafe { create_config(loader) }; - if accel_cfg.is_null() { - log.push("MFXCreateConfig(accel) returned NULL".to_string()); - return (None, log); - } - let accel_variant = MfxVariant { - version: MfxStructVersion { - minor: MFX_VARIANT_VERSION_MINOR, - major: MFX_VARIANT_VERSION_MAJOR, - }, - type_: MFX_VARIANT_TYPE_U32, - data: MfxVariantData { u32_: accel_mode }, - }; - let status_accel = - unsafe { set_prop(accel_cfg, FILTER_PROPERTY_ACCEL.as_ptr(), accel_variant) }; - log.push(format!( - "SetConfigFilterProperty(AccelerationMode={accel_mode:#x}) status={status_accel}" - )); - if status_accel != MFX_ERR_NONE { - return (None, log); - } - let mut session: *mut c_void = ptr::null_mut(); - let status_create = unsafe { create_session(loader, 0, &mut session) }; - log.push(format!( - "MFXCreateSession status={status_create} session={session:p}" - )); - if status_create != MFX_ERR_NONE || session.is_null() { - return (None, log); - } - (Some((loader, session)), log) - } - - fn unload(&self, loader: *mut c_void) { - let Ok(unload) = (unsafe { self.library.get::(b"MFXUnload\0") }) else { - return; - }; - if !loader.is_null() { - unsafe { unload(loader) }; - } - } - - fn close(&self, session: *mut c_void) { - let Ok(close) = (unsafe { self.library.get::(b"MFXClose\0") }) else { - return; - }; - if !session.is_null() { - unsafe { - let _ = close(session); - } - } - } -} - -fn try_create_intel_device() -> Option<(ID3D11Device, ID3D11DeviceContext)> { - let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.ok()?; - let mut idx: u32 = 0; - loop { - let adapter: IDXGIAdapter1 = match unsafe { factory.EnumAdapters1(idx) } { - Ok(a) => a, - Err(_) => return None, - }; - idx = idx.saturating_add(1); - let desc = match unsafe { adapter.GetDesc1() } { - Ok(d) => d, - Err(_) => continue, - }; - if desc.VendorId != VENDOR_INTEL { - continue; - } - let mut device: Option = None; - let mut ctx: Option = None; - let feature_levels = [D3D_FEATURE_LEVEL_11_0]; - let cast_result = adapter.cast::(); - let cast_adapter = match cast_result { - Ok(a) => a, - Err(_) => continue, - }; - let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; - let result = unsafe { - D3D11CreateDevice( - Some(&cast_adapter), - D3D_DRIVER_TYPE_UNKNOWN, - Default::default(), - flags, - Some(&feature_levels), - D3D11_SDK_VERSION, - Some(&mut device), - None, - Some(&mut ctx), - ) - }; - if let (Ok(()), Some(d), Some(c)) = (result, device, ctx) { - if let Ok(mt) = d.cast::() { - let _ = unsafe { mt.SetMultithreadProtected(true) }; - } - return Some((d, c)); - } - } -} - -fn build_video_params_with( - width: u16, - height: u16, - io_pattern: u16, - target_usage: u16, - low_power: u16, -) -> MfxVideoParam { - let aligned_w = (width + 15) & !15; - let aligned_h = (height + 15) & !15; - let info = MfxFrameInfo { - four_cc: MFX_FOURCC_NV12, - width: aligned_w, - height: aligned_h, - crop_w: width, - crop_h: height, - frame_rate_extn: 30, - frame_rate_extd: 1, - aspect_ratio_w: 1, - aspect_ratio_h: 1, - pic_struct: MFX_PICSTRUCT_PROGRESSIVE, - chroma_format: MFX_CHROMAFORMAT_YUV420, - ..Default::default() - }; - let mfx = MfxInfoMfx { - frame_info: info, - codec_id: MFX_CODEC_AVC, - target_usage, - low_power, - gop_pic_size: 30, - gop_ref_dist: 1, - rate_control_method: MFX_RATECONTROL_CBR, - target_kbps: 5_000, - max_kbps: 5_000, - num_slice: 1, - num_ref_frame: 1, - ..Default::default() - }; - MfxVideoParam { - alloc_id: 0, - reserved: [0; 2], - reserved3: 0, - async_depth: 1, - mfx, - protected: 0, - io_pattern, - ext_param: ptr::null_mut(), - num_ext_param: 0, - reserved2: 0, - } -} - -fn try_set_handle( - dispatcher: &Dispatcher, - session: *mut c_void, - handle_type: u32, - handle_ptr: *mut c_void, -) -> Result { - let sym: Symbol<'_, MfxSetHandle> = - unsafe { dispatcher.library.get(b"MFXVideoCORE_SetHandle\0") } - .map_err(|e| format!("MFXVideoCORE_SetHandle missing: {e:?}"))?; - let status = unsafe { sym(session, handle_type, handle_ptr) }; - Ok(status) -} - -fn try_query_impl(dispatcher: &Dispatcher, session: *mut c_void) -> Result<(i32, i32), String> { - let sym: Symbol<'_, MfxQueryImpl> = unsafe { dispatcher.library.get(b"MFXQueryIMPL\0") } - .map_err(|e| format!("MFXQueryIMPL missing: {e:?}"))?; - let mut out: i32 = 0; - let status = unsafe { sym(session, &mut out) }; - Ok((status, out)) -} - -fn try_query_platform( - dispatcher: &Dispatcher, - session: *mut c_void, -) -> Result<(i32, MfxPlatform), String> { - let sym: Symbol<'_, MfxQueryPlatform> = - unsafe { dispatcher.library.get(b"MFXVideoCORE_QueryPlatform\0") } - .map_err(|e| format!("MFXVideoCORE_QueryPlatform missing: {e:?}"))?; - let mut p = MfxPlatform::default(); - let status = unsafe { sym(session, &mut p) }; - Ok((status, p)) -} - -fn try_encode_init( - dispatcher: &Dispatcher, - session: *mut c_void, - width: u16, - height: u16, - io_pattern: u16, -) -> Result { - try_encode_init_with(dispatcher, session, width, height, io_pattern, 4, 0) -} - -fn try_encode_init_with( - dispatcher: &Dispatcher, - session: *mut c_void, - width: u16, - height: u16, - io_pattern: u16, - target_usage: u16, - low_power: u16, -) -> Result { - let query: Symbol<'_, MfxEncodeQuery> = - unsafe { dispatcher.library.get(b"MFXVideoENCODE_Query\0") } - .map_err(|e| format!("MFXVideoENCODE_Query missing: {e:?}"))?; - let init: Symbol<'_, MfxEncodeInit> = - unsafe { dispatcher.library.get(b"MFXVideoENCODE_Init\0") } - .map_err(|e| format!("MFXVideoENCODE_Init missing: {e:?}"))?; - let mut params = build_video_params_with(width, height, io_pattern, target_usage, low_power); - let mut query_out = build_video_params_with(width, height, io_pattern, target_usage, low_power); - let q_status = unsafe { query(session, &mut params, &mut query_out) }; - eprintln!( - " MFXVideoENCODE_Query(io={io_pattern:#x},tu={target_usage},lp={low_power}) status={q_status}" - ); - if q_status != MFX_ERR_NONE && q_status != MFX_WRN_IN_EXECUTION && q_status != -3 { - eprintln!(" Query returned hard error {q_status}; skipping Init to avoid AV"); - return Ok(q_status); - } - let init_status = unsafe { init(session, &mut query_out) }; - Ok(init_status) -} - -fn try_get_handle( - dispatcher: &Dispatcher, - session: *mut c_void, - handle_type: u32, -) -> Result<(i32, *mut c_void), String> { - let sym: Symbol<'_, MfxGetHandle> = - unsafe { dispatcher.library.get(b"MFXVideoCORE_GetHandle\0") } - .map_err(|e| format!("MFXVideoCORE_GetHandle missing: {e:?}"))?; - let mut out: *mut c_void = ptr::null_mut(); - let status = unsafe { sym(session, handle_type, &mut out) }; - Ok((status, out)) -} - -fn try_encode_close(dispatcher: &Dispatcher, session: *mut c_void) { - if let Ok(sym) = unsafe { - dispatcher - .library - .get::(b"MFXVideoENCODE_Close\0") - } { - unsafe { - let _ = sym(session); - } - } -} - -#[test] -fn qsv_tiger_lake_probe_struct_sizes() { - eprintln!("=== STRUCT SIZES (Rust) ==="); - eprintln!("MfxFrameInfo = {}", std::mem::size_of::()); - eprintln!("MfxInfoMfx = {}", std::mem::size_of::()); - eprintln!("MfxVideoParam = {}", std::mem::size_of::()); - assert!( - std::mem::size_of::() == 68, - "MfxFrameInfo size must be 68" - ); - assert!( - std::mem::size_of::() >= 168, - "MfxInfoMfx >= 168 (= sizeof mfxInfoVPP)" - ); -} - -#[test] -fn qsv_tiger_lake_probe_experiment_1_preconditions() { - let dispatcher = match Dispatcher::load() { - Some(d) => d, - None => { - eprintln!("skip: libvpl.dll not loadable"); - return; - } - }; - eprintln!("=== EXPERIMENT 1: preconditions (QueryIMPL + QueryPlatform) ==="); - let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11); - for line in &log { - eprintln!(" {line}"); - } - let (loader, session) = match session_opt { - Some(s) => s, - None => { - eprintln!("EXP1 result: modern session not created; cannot probe preconditions"); - return; - } - }; - match try_query_impl(&dispatcher, session) { - Ok((status, impl_val)) => { - eprintln!(" MFXQueryIMPL: status={status} impl={impl_val:#x}"); - } - Err(e) => eprintln!(" MFXQueryIMPL error: {e}"), - } - match try_query_platform(&dispatcher, session) { - Ok((status, p)) => { - eprintln!( - " MFXVideoCORE_QueryPlatform: status={status} code_name={} device_id={:#x} media_adapter_type={}", - p.code_name, p.device_id, p.media_adapter_type - ); - } - Err(e) => eprintln!(" MFXVideoCORE_QueryPlatform error: {e}"), - } - dispatcher.close(session); - dispatcher.unload(loader); - eprintln!("EXP1 done"); -} - -#[test] -fn qsv_tiger_lake_probe_experiment_2_variant_accel_modes() { - let dispatcher = match Dispatcher::load() { - Some(d) => d, - None => { - eprintln!("skip: libvpl.dll not loadable"); - return; - } - }; - let (device, _ctx) = match try_create_intel_device() { - Some(d) => d, - None => { - eprintln!("skip: no Intel D3D11 device"); - return; - } - }; - eprintln!("=== EXPERIMENT 2: SetHandle handle-type variants ==="); - let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11); - for line in &log { - eprintln!(" {line}"); - } - let (loader, session) = match session_opt { - Some(s) => s, - None => { - eprintln!("EXP2 result: modern session not created"); - return; - } - }; - let raw_device = device.as_raw(); - let exp2a = try_set_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE, raw_device); - eprintln!( - " EXP2a: SetHandle(MFX_HANDLE_D3D11_DEVICE=3) -> {:?}", - exp2a - ); - let exp2b = try_set_handle( - &dispatcher, - session, - MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS, - raw_device, - ); - eprintln!( - " EXP2b: SetHandle(MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS=11) -> {:?}", - exp2b - ); - dispatcher.close(session); - dispatcher.unload(loader); - eprintln!("EXP2 done"); - assert!(exp2a.is_ok(), "MFXVideoCORE_SetHandle symbol resolves"); -} - -#[test] -fn qsv_tiger_lake_probe_experiment_3_skip_set_handle() { - let dispatcher = match Dispatcher::load() { - Some(d) => d, - None => { - eprintln!("skip: libvpl.dll not loadable"); - return; - } - }; - let (device, _ctx) = match try_create_intel_device() { - Some(d) => d, - None => { - eprintln!("skip: no Intel D3D11 device"); - return; - } - }; - eprintln!("=== EXPERIMENT 3: SetHandle with VIDEO_SUPPORT device + encoder init ==="); - let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11); - for line in &log { - eprintln!(" {line}"); - } - let (loader, session) = match session_opt { - Some(s) => s, - None => { - eprintln!("EXP3 result: modern session not created"); - return; - } - }; - match try_query_impl(&dispatcher, session) { - Ok((status, impl_val)) => { - eprintln!(" pre-set MFXQueryIMPL: status={status} impl={impl_val:#x}"); - } - Err(e) => eprintln!(" MFXQueryIMPL error: {e}"), - } - let raw_device = device.as_raw(); - let set_status = try_set_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE, raw_device); - eprintln!( - " EXP3-set: SetHandle(D3D11_DEVICE=3, VIDEO_SUPPORT+BGRA, multithread-protected) -> {:?}", - set_status - ); - match try_get_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE) { - Ok((status, hdl)) => { - eprintln!( - " post-set GetHandle(MFX_HANDLE_D3D11_DEVICE) -> status={status} handle={hdl:p}" - ); - } - Err(e) => eprintln!(" GetHandle error: {e}"), - } - let init_a = try_encode_init( - &dispatcher, - session, - 1280, - 720, - MFX_IOPATTERN_IN_VIDEO_MEMORY, - ); - eprintln!( - " EXP3a (IO_VIDEO_MEMORY, target_usage=4, low_power=0): init -> {:?}", - init_a - ); - let init_b = try_encode_init_with( - &dispatcher, - session, - 1280, - 720, - MFX_IOPATTERN_IN_VIDEO_MEMORY, - 7, - 0x10, - ); - eprintln!( - " EXP3b (IO_VIDEO_MEMORY, target_usage=7 best-speed, low_power=ON=0x10): init -> {:?}", - init_b - ); - let init_c = try_encode_init_with( - &dispatcher, - session, - 1280, - 720, - MFX_IOPATTERN_IN_VIDEO_MEMORY, - 4, - 0x10, - ); - eprintln!( - " EXP3c (IO_VIDEO_MEMORY, target_usage=4 balanced, low_power=ON=0x10): init -> {:?}", - init_c - ); - try_encode_close(&dispatcher, session); - dispatcher.close(session); - dispatcher.unload(loader); - eprintln!("EXP3 done"); - assert!(init_a.is_ok(), "init_a symbol resolved"); - assert!(init_b.is_ok(), "init_b symbol resolved"); - assert!(init_c.is_ok(), "init_c symbol resolved"); -} - -#[test] -fn qsv_tiger_lake_probe_experiment_4_software_fallback() { - let dispatcher = match Dispatcher::load() { - Some(d) => d, - None => { - eprintln!("skip: libvpl.dll not loadable"); - return; - } - }; - eprintln!("=== EXPERIMENT 4: software-only (ACCEL_MODE_NA) session ==="); - let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_NA); - for line in &log { - eprintln!(" {line}"); - } - let (loader, session) = match session_opt { - Some(s) => s, - None => { - eprintln!("EXP4 result: software session not created"); - return; - } - }; - match try_query_impl(&dispatcher, session) { - Ok((status, impl_val)) => { - eprintln!(" MFXQueryIMPL: status={status} impl={impl_val:#x}"); - } - Err(e) => eprintln!(" MFXQueryIMPL error: {e}"), - } - let init_status = try_encode_init( - &dispatcher, - session, - 1280, - 720, - MFX_IOPATTERN_IN_SYSTEM_MEMORY, - ); - eprintln!(" EXP4: MFXVideoENCODE_Init software -> {:?}", init_status); - try_encode_close(&dispatcher, session); - dispatcher.close(session); - dispatcher.unload(loader); - eprintln!("EXP4 done"); - assert!( - init_status.is_ok(), - "encode init symbol resolved in software" - ); -} diff --git a/fluxer_desktop/native/encoder-ring/tests/windows_real_encode.rs b/fluxer_desktop/native/encoder-ring/tests/windows_real_encode.rs deleted file mode 100644 index edf12f651..000000000 --- a/fluxer_desktop/native/encoder-ring/tests/windows_real_encode.rs +++ /dev/null @@ -1,551 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#![cfg(target_os = "windows")] -use fluxer_encoder_ring::NVENC_COMPLETION_RING_CAPACITY; -use fluxer_encoder_ring::encoder_handoff::EncoderCompletionCallback; -use fluxer_encoder_ring::{ - AmfD3D11Handoff, AmfHandoff, EncodedBitstream, EncoderDims, EncoderError, EncoderSubmission, - NvencD3D11Handoff, NvencHandoff, PicParams, QsvD3D11Handoff, QsvHandoff, RingError, -}; -use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0}; -use windows::Win32::Graphics::Direct3D11::{ - D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_BGRA_SUPPORT, - D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, - D3D11_RESOURCE_MISC_SHARED_NTHANDLE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, - D3D11_USAGE_DEFAULT, D3D11CreateDevice, ID3D11Device, ID3D11Multithread, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; -use windows::Win32::Graphics::Dxgi::{ - CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIResource1, -}; -use windows::core::Interface; - -const VENDOR_NVIDIA: u32 = 0x10DE; -const VENDOR_AMD: u32 = 0x1002; -const VENDOR_INTEL: u32 = 0x8086; - -fn enum_adapters() -> Vec { - let factory_result: windows::core::Result = unsafe { CreateDXGIFactory1() }; - let factory = match factory_result { - Ok(f) => f, - Err(_) => return Vec::new(), - }; - let mut out = Vec::new(); - let mut index: u32 = 0; - loop { - let result: windows::core::Result = unsafe { factory.EnumAdapters1(index) }; - match result { - Ok(adapter) => { - out.push(adapter); - index = index.saturating_add(1); - } - Err(_) => break, - } - } - out -} - -fn try_create_device_for_vendor(vendor_id: u32) -> Option { - let adapters = enum_adapters(); - for adapter in adapters { - let desc = match unsafe { adapter.GetDesc1() } { - Ok(d) => d, - Err(_) => continue, - }; - if desc.VendorId != vendor_id { - continue; - } - let mut device: Option = None; - let feature_levels = [D3D_FEATURE_LEVEL_11_0]; - let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; - let result = unsafe { - D3D11CreateDevice( - Some(&adapter.cast().ok()?), - D3D_DRIVER_TYPE_UNKNOWN, - Default::default(), - flags, - Some(&feature_levels), - D3D11_SDK_VERSION, - Some(&mut device), - None, - None, - ) - }; - if let (Ok(()), Some(d)) = (result, device) { - if let Ok(mt) = d.cast::() { - let _ = unsafe { mt.SetMultithreadProtected(true) }; - } - return Some(d); - } - } - None -} - -fn try_create_device() -> Option { - try_create_device_for_vendor(VENDOR_NVIDIA) - .or_else(|| try_create_device_for_vendor(VENDOR_INTEL)) - .or_else(|| try_create_device_for_vendor(VENDOR_AMD)) -} - -fn try_create_shared_nv12(device: &ID3D11Device, width: u32, height: u32) -> Option { - let desc = D3D11_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_NV12, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - CPUAccessFlags: 0, - MiscFlags: (D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0 | D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0) - as u32, - }; - let mut texture = None; - unsafe { - device - .CreateTexture2D(&desc, None, Some(&mut texture)) - .ok()?; - } - let texture = texture?; - let resource: IDXGIResource1 = texture.cast().ok()?; - let shared = unsafe { - resource - .CreateSharedHandle(None, 0x3, windows::core::PCWSTR::null()) - .ok()? - }; - Some(shared.0 as u64) -} - -#[test] -fn nvenc_real_encode_one_frame_via_dummy_shared_texture() { - let device = match try_create_device_for_vendor(VENDOR_NVIDIA) { - Some(d) => d, - None => { - eprintln!("skip: no NVIDIA D3D11 device available"); - return; - } - }; - let dims = EncoderDims::new(1920, 1080); - let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 5_000_000); - let mut handoff = match handoff_result { - Ok(h) => h, - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: NVENC runtime DLL not available"); - return; - } - Err(EncoderError::SessionInitFailed { vendor, status }) => { - eprintln!("skip: NVENC session init failed: {vendor} status={status}"); - return; - } - Err(other) => { - eprintln!("skip: NVENC init unexpected error: {other:?}"); - return; - } - }; - let shared = match try_create_shared_nv12(&device, 1920, 1080) { - Some(h) => h, - None => { - eprintln!("skip: shared NV12 texture creation failed"); - return; - } - }; - let slot = match NvencHandoff::register_slot(&mut handoff, shared, 0, dims) { - Ok(s) => s, - Err(e) => { - eprintln!("skip: register_slot failed: {e:?}"); - return; - } - }; - let pic = PicParams::new(0, true); - let submit_result = NvencHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic); - if submit_result.is_err() { - eprintln!("skip: encode_shared_async failed: {submit_result:?}"); - NvencHandoff::unregister_slot(&mut handoff, slot); - return; - } - let mut bitstream = None; - for _ in 0..200 { - bitstream = NvencHandoff::poll_completed(&mut handoff, slot); - if bitstream.is_some() { - break; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - NvencHandoff::unregister_slot(&mut handoff, slot); - let bs = match bitstream { - Some(b) => b, - None => { - eprintln!("nvenc: no bitstream produced within timeout; backend init succeeded"); - return; - } - }; - assert!(!bs.data.is_empty(), "non-empty bitstream"); - assert!(bs.is_keyframe, "first frame must be keyframe"); -} - -#[test] -fn amf_real_encode_one_frame_via_dummy_shared_texture() { - let device = match try_create_device_for_vendor(VENDOR_AMD) { - Some(d) => d, - None => { - eprintln!("skip: no AMD D3D11 device available (expected on Intel/NVIDIA-only boxes)"); - return; - } - }; - let dims = EncoderDims::new(1920, 1080); - let handoff_result = AmfD3D11Handoff::new(device.clone(), dims, 5_000_000); - let mut handoff = match handoff_result { - Ok(h) => h, - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: AMF runtime DLL not available (expected on non-AMD hardware)"); - return; - } - Err(other) => { - eprintln!("skip: AMF init unexpected: {other:?}"); - return; - } - }; - let shared = match try_create_shared_nv12(&device, 1920, 1080) { - Some(h) => h, - None => { - eprintln!("skip: shared NV12 texture creation failed"); - return; - } - }; - let slot = match AmfHandoff::register_slot(&mut handoff, shared, 0, dims) { - Ok(s) => s, - Err(e) => { - eprintln!("skip: AMF register_slot failed: {e:?}"); - return; - } - }; - let pic = PicParams::new(0, true); - let _ = AmfHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic); - let mut bitstream = None; - for _ in 0..200 { - bitstream = AmfHandoff::poll_completed(&mut handoff, slot); - if bitstream.is_some() { - break; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - AmfHandoff::unregister_slot(&mut handoff, slot); - if let Some(bs) = bitstream { - assert!(!bs.data.is_empty(), "non-empty AMF bitstream"); - } else { - eprintln!("amf: no bitstream produced within timeout; backend init succeeded"); - } -} - -#[test] -fn qsv_modern_dispatcher_session_init_succeeds_on_real_iris_xe() { - let device = match try_create_device_for_vendor(VENDOR_INTEL) { - Some(d) => d, - None => { - eprintln!("skip: no Intel D3D11 device available"); - return; - } - }; - let dims = EncoderDims::new(1280, 720); - let handoff_result = QsvD3D11Handoff::new(device, dims, 5_000_000); - match handoff_result { - Ok(_) => { - eprintln!("qsv: modern dispatcher session + SetHandle + encode_init succeeded"); - } - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: QSV runtime DLL not available"); - } - Err(e) => { - panic!("qsv modern dispatcher init MUST succeed on Tiger Lake Iris Xe: {e:?}"); - } - } -} - -#[test] -fn qsv_real_encode_one_frame_via_dummy_shared_texture() { - let device = match try_create_device_for_vendor(VENDOR_INTEL) { - Some(d) => d, - None => { - eprintln!("skip: no Intel D3D11 device available"); - return; - } - }; - let dims = EncoderDims::new(1920, 1080); - let handoff_result = QsvD3D11Handoff::new(device.clone(), dims, 5_000_000); - let mut handoff = match handoff_result { - Ok(h) => h, - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: QSV runtime DLL not available"); - return; - } - Err(EncoderError::SessionInitFailed { vendor, status }) => { - eprintln!("skip: QSV session init failed: {vendor} status={status}"); - return; - } - Err(other) => { - eprintln!("skip: QSV init unexpected: {other:?}"); - return; - } - }; - let shared = match try_create_shared_nv12(&device, 1920, 1080) { - Some(h) => h, - None => { - eprintln!("skip: shared NV12 texture creation failed"); - return; - } - }; - let slot = match QsvHandoff::register_slot(&mut handoff, shared, 0, dims) { - Ok(s) => s, - Err(e) => { - eprintln!("skip: QSV register_slot failed: {e:?}"); - return; - } - }; - let pic = PicParams::new(0, true); - let _ = QsvHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic); - let mut bitstream = None; - for _ in 0..200 { - bitstream = QsvHandoff::poll_completed(&mut handoff, slot); - if bitstream.is_some() { - break; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - QsvHandoff::unregister_slot(&mut handoff, slot); - if let Some(bs) = bitstream { - assert!(!bs.data.is_empty(), "non-empty QSV bitstream"); - } else { - eprintln!("qsv: no bitstream produced within timeout; backend init succeeded"); - } -} - -#[test] -fn skip_dont_block_rapid_submit_then_poll_never_hangs() { - let device = match try_create_device() { - Some(d) => d, - None => return, - }; - let dims = EncoderDims::new(640, 360); - let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) { - Ok(h) => h, - Err(_) => return, - }; - let shared = match try_create_shared_nv12(&device, 640, 360) { - Some(h) => h, - None => return, - }; - let slot = match NvencHandoff::register_slot(&mut handoff, shared, 0, dims) { - Ok(s) => s, - Err(_) => return, - }; - let start = std::time::Instant::now(); - for i in 0..10 { - let pic = PicParams::new((i as u64) * 16_666, false); - let _ = NvencHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic); - let _ = NvencHandoff::poll_completed(&mut handoff, slot); - } - let elapsed = start.elapsed(); - assert!( - elapsed.as_secs() < 5, - "10 submit+poll cycles must complete in < 5s, was {elapsed:?}" - ); - NvencHandoff::unregister_slot(&mut handoff, slot); -} - -#[test] -fn cleanup_on_drop_releases_resources() { - let device = match try_create_device() { - Some(d) => d, - None => return, - }; - let dims = EncoderDims::new(640, 360); - { - let _handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) { - Ok(h) => h, - Err(_) => return, - }; - } - let _handoff2 = NvencD3D11Handoff::new(device.clone(), dims, 1_000_000); -} - -struct CountingCallback { - seen: Vec<(u64, u32)>, -} - -impl EncoderCompletionCallback for CountingCallback { - fn on_complete(&mut self, sequence: u64, encoded_bytes: u32) { - self.seen.push((sequence, encoded_bytes)); - } -} - -#[test] -fn nvenc_compression_handoff_encodes_one_frame_via_keyed_mutex() { - let device = match try_create_device_for_vendor(VENDOR_NVIDIA) { - Some(d) => d, - None => { - eprintln!("skip: no NVIDIA D3D11 device available"); - return; - } - }; - let dims = EncoderDims::new(1280, 720); - let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 2_000_000) { - Ok(h) => h, - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: NVENC runtime DLL not available"); - return; - } - Err(other) => { - eprintln!("skip: NVENC init unexpected: {other:?}"); - return; - } - }; - let shared = match try_create_shared_nv12(&device, 1280, 720) { - Some(h) => h, - None => { - eprintln!("skip: shared NV12 texture creation failed"); - return; - } - }; - let submission = EncoderSubmission::new(shared, 0, dims, 1); - let mut callback = CountingCallback { seen: Vec::new() }; - NvencHandoff::encode_shared(&mut handoff, submission, &mut callback) - .expect("encode_shared first frame ok"); - assert_eq!(callback.seen.len(), 1, "callback fired once"); - assert_eq!(callback.seen[0].0, 1, "callback sequence matches"); - let mut payload: Option = None; - for _ in 0..200 { - let slot = fluxer_encoder_ring::HandoffSlot::new(0, shared); - payload = NvencHandoff::poll_completed(&mut handoff, slot); - if payload.is_some() { - break; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - let bs = match payload { - Some(b) => b, - None => { - eprintln!("nvenc keyed-mutex: no bitstream within timeout; init succeeded"); - return; - } - }; - assert!(!bs.data.is_empty(), "keyed-mutex encoded payload non-empty"); - assert!(bs.is_keyframe, "first frame must be IDR keyframe"); -} - -#[test] -fn nvenc_compression_handoff_handles_back_pressure() { - let device = match try_create_device_for_vendor(VENDOR_NVIDIA) { - Some(d) => d, - None => { - eprintln!("skip: no NVIDIA D3D11 device available"); - return; - } - }; - let dims = EncoderDims::new(640, 360); - let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) { - Ok(h) => h, - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: NVENC runtime DLL not available"); - return; - } - Err(other) => { - eprintln!("skip: NVENC init unexpected: {other:?}"); - return; - } - }; - let shared = match try_create_shared_nv12(&device, 640, 360) { - Some(h) => h, - None => { - eprintln!("skip: shared NV12 texture creation failed"); - return; - } - }; - let total: u64 = (NVENC_COMPLETION_RING_CAPACITY as u64) + 8; - let mut callback = CountingCallback { seen: Vec::new() }; - let mut accepted = 0usize; - let mut full_drops = 0usize; - for seq in 1..=total { - let submission = EncoderSubmission::new(shared, 0, dims, seq); - match NvencHandoff::encode_shared(&mut handoff, submission, &mut callback) { - Ok(()) => { - accepted += 1; - } - Err(RingError::FullDropped { .. }) => { - full_drops += 1; - assert_eq!( - handoff.pending_completion(), - NVENC_COMPLETION_RING_CAPACITY, - "pre-encode drop only when completion ring is full" - ); - } - Err(other) => panic!("unexpected encode_shared error: {other:?}"), - } - std::thread::sleep(std::time::Duration::from_millis(2)); - } - assert_eq!( - accepted + full_drops, - total as usize, - "every submission accounted" - ); - let pending = handoff.pending_completion(); - assert!( - pending <= NVENC_COMPLETION_RING_CAPACITY, - "completion ring stays bounded" - ); - let mut drained = 0usize; - for _ in 0..400 { - let slot = fluxer_encoder_ring::HandoffSlot::new(0, shared); - let bs = NvencHandoff::poll_completed(&mut handoff, slot); - match bs { - Some(_) => { - drained += 1; - } - None => { - if drained >= accepted { - break; - } - std::thread::sleep(std::time::Duration::from_millis(5)); - } - } - } - assert_eq!( - drained, accepted, - "no accepted frame is silently discarded post-encode" - ); -} - -#[test] -fn nvenc_attach_then_detach_smoke() { - let device = match try_create_device_for_vendor(VENDOR_NVIDIA) { - Some(d) => d, - None => { - eprintln!("skip: no NVIDIA D3D11 device available"); - return; - } - }; - let dims = EncoderDims::new(640, 360); - let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 1_000_000); - let handoff = match handoff_result { - Ok(h) => h, - Err(EncoderError::SdkNotFound { .. }) => { - eprintln!("skip: NVENC runtime DLL not available"); - return; - } - Err(other) => { - eprintln!("skip: NVENC init unexpected: {other:?}"); - return; - } - }; - assert_eq!( - handoff.pending_completion(), - 0, - "fresh handoff has empty ring" - ); - assert_eq!(handoff.completed_count(), 0, "fresh handoff completed=0"); - drop(handoff); - let _again = NvencD3D11Handoff::new(device, dims, 1_000_000); -} diff --git a/fluxer_desktop/native/linux-audio-capture/src/pipewire_bridge.rs b/fluxer_desktop/native/linux-audio-capture/src/pipewire_bridge.rs index 277e1f5d9..d577afc2d 100644 --- a/fluxer_desktop/native/linux-audio-capture/src/pipewire_bridge.rs +++ b/fluxer_desktop/native/linux-audio-capture/src/pipewire_bridge.rs @@ -289,34 +289,6 @@ mod tests { assert!(!is_routable_media_class("")); } - #[test] - fn daemon_unreachable_returns_none_from_open() { - let bridge = PipeWireBridge::open(); - if let Some(b) = bridge { - let _ = b.inventory(); - b.release(); - } - } - - #[test] - fn direct_open_returns_none_or_cleans_up() { - let direct = PipeWireDirectCapture::open(); - if let Some(d) = direct { - d.stop(); - } - } - - #[test] - fn integration_smoke_apply_release_cycle_is_safe() { - let Some(bridge) = PipeWireBridge::open() else { - return; - }; - bridge.apply(RoutingRule::default()); - std::thread::sleep(Duration::from_millis(20)); - bridge.release(); - let _ = bridge.inventory(); - } - #[test] fn virtual_sink_props_match_legacy_contract() { let props = build_virtual_sink_props(); diff --git a/fluxer_desktop/native/linux-audio-capture/vm_tests/direct_capture_validation.mjs b/fluxer_desktop/native/linux-audio-capture/vm_tests/direct_capture_validation.mjs deleted file mode 100644 index 092cb21b2..000000000 --- a/fluxer_desktop/native/linux-audio-capture/vm_tests/direct_capture_validation.mjs +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {execFileSync} from 'node:child_process'; -import {copyFileSync} from 'node:fs'; -import {createRequire} from 'node:module'; -import {setTimeout as sleep} from 'node:timers/promises'; - -const require = createRequire(import.meta.url); -const work = process.env.FLX_WORK; -const addonNode = `${work}/flx_direct_addon.node`; -copyFileSync(process.env.FLX_ADDON_SO, addonNode); -const addon = require(addonNode); - -const results = []; -function record(name, ok, detail) { - results.push({name, ok}); - console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` -- ${detail}` : ''}`); -} -function pwDump() { - return JSON.parse(execFileSync('pw-dump', {encoding: 'utf8', maxBuffer: 64e6})); -} -function directSinkNode(dump) { - return dump.find( - (o) => o.type === 'PipeWire:Interface:Node' && /^fluxer-direct-capture-/.test(o.info?.props?.['node.name'] || ''), - ); -} -async function waitFor(p, ms, step = 150) { - const end = Date.now() + ms; - let last; - while (Date.now() < end) { - last = p(); - if (last) return last; - await sleep(step); - } - return last; -} - -function rms(samples) { - if (samples.length === 0) return 0; - let sum = 0; - for (const s of samples) sum += s * s; - return Math.sqrt(sum / samples.length); -} - -async function main() { - const dc = new addon.DirectAudioCapture(); - let lifecycle = null; - dc.setLifecycleCallback((...args) => { - const flat = args.length === 1 && Array.isArray(args[0]) ? args[0] : args; - lifecycle = {kind: flat[0], msg: flat[1]}; - }); - - const started = dc.start({include: [{'application.name': 'Music Player Demo'}]}); - record('DirectAudioCapture.start(include rule) accepted', started === true); - - const sinkUp = await waitFor(() => { - const d = pwDump(); - return directSinkNode(d) ? d : null; - }, 8000); - record( - 'hidden private sink fluxer-direct-capture-* created', - !!sinkUp, - sinkUp ? directSinkNode(sinkUp).info.props['node.name'] : 'timeout', - ); - - if (sinkUp) { - const sinkProps = directSinkNode(sinkUp).info.props; - record( - 'private sink node.hidden=true (not user-visible)', - String(sinkProps['node.hidden']) === 'true', - `node.hidden=${sinkProps['node.hidden']}`, - ); - record('private sink media.class=Audio/Sink', sinkProps['media.class'] === 'Audio/Sink', sinkProps['media.class']); - } - - let maxRms = 0; - let frames = 0; - let totalSamples = 0; - const captureDeadline = Date.now() + 6000; - while (Date.now() < captureDeadline) { - const f = dc.read(); - if (f) { - const samples = new Float32Array(f.samples); - frames += 1; - totalSamples += samples.length; - maxRms = Math.max(maxRms, rms(samples)); - if (maxRms > 0.01 && frames > 5) break; - } - await sleep(20); - } - record('DirectAudioCapture yields real frames', frames > 0, `${frames} frames, ${totalSamples} samples`); - record('captured audio is non-silent (real 440Hz tone tapped)', maxRms > 0.01, `peak rms=${maxRms.toFixed(4)}`); - - const dump = pwDump(); - const sink = directSinkNode(dump); - const sinkId = Number(sink?.id); - const linkSrcNodes = new Set( - dump - .filter((o) => o.type === 'PipeWire:Interface:Link' && Number(o.info?.props?.['link.input.node']) === sinkId) - .map((o) => Number(o.info?.props?.['link.output.node'])), - ); - const fluxerStreamIds = dump - .filter((o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['application.name'] === 'Fluxer') - .map((o) => Number(o.id)); - record( - 'Fluxer-named app excluded from per-process capture', - fluxerStreamIds.every((id) => !linkSrcNodes.has(id)), - `fluxer=${fluxerStreamIds} linkedSrc=${[...linkSrcNodes]}`, - ); - - const musicIds = dump - .filter((o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['application.name'] === 'Music Player Demo') - .map((o) => Number(o.id)); - record( - 'targeted app IS linked to the private sink', - musicIds.some((id) => linkSrcNodes.has(id)), - `music=${musicIds} linkedSrc=${[...linkSrcNodes]}`, - ); - - dc.stop(); - await sleep(600); - record('stop() emits closed-clean lifecycle', lifecycle?.kind === 'closed-clean', JSON.stringify(lifecycle)); - const afterStop = pwDump(); - const sinkAfter = directSinkNode(afterStop); - const residualLinks = sinkAfter - ? afterStop.filter( - (o) => - o.type === 'PipeWire:Interface:Link' && Number(o.info?.props?.['link.input.node']) === Number(sinkAfter.id), - ).length - : 0; - record('stop() removes capture links', residualLinks === 0, `residual=${residualLinks}`); - - const failed = results.filter((r) => !r.ok).length; - console.log(`\n=== direct: ${results.length - failed}/${results.length} checks passed ===`); - process.exit(failed === 0 ? 0 : 1); -} - -main().catch((e) => { - console.error('HARNESS ERROR:', e?.stack || e); - process.exit(2); -}); diff --git a/fluxer_desktop/native/linux-audio-capture/vm_tests/pw_graph_validation.mjs b/fluxer_desktop/native/linux-audio-capture/vm_tests/pw_graph_validation.mjs deleted file mode 100644 index 0619f55df..000000000 --- a/fluxer_desktop/native/linux-audio-capture/vm_tests/pw_graph_validation.mjs +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {execFileSync, spawn} from 'node:child_process'; -import {copyFileSync} from 'node:fs'; -import {createRequire} from 'node:module'; -import {setTimeout as sleep} from 'node:timers/promises'; - -const require = createRequire(import.meta.url); - -const addonSo = req('FLX_ADDON_SO'); -const work = req('FLX_WORK'); -const tone = req('FLX_TONE'); -const addonNode = `${work}/flx_audio_addon.node`; -copyFileSync(addonSo, addonNode); -const addon = require(addonNode); - -const SINK_NAME = 'fluxer-screen-share'; -const SINK_DESC = 'Fluxer Screen Share Audio'; -const results = []; -const children = []; - -function req(name) { - const v = process.env[name]; - if (!v) { - console.error(`HARNESS ERROR: ${name} not set`); - process.exit(2); - } - return v; -} -function record(name, ok, detail) { - results.push({name, ok}); - console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` -- ${detail}` : ''}`); -} -function pwDump() { - return JSON.parse(execFileSync('pw-dump', {encoding: 'utf8', maxBuffer: 64e6})); -} -function nodesByName(dump, name) { - return dump.filter((o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['node.name'] === name); -} -function links(dump) { - return dump - .filter((o) => o.type === 'PipeWire:Interface:Link') - .map((o) => ({ - inNode: Number(o.info?.props?.['link.input.node']), - outNode: Number(o.info?.props?.['link.output.node']), - })); -} -function streamNodes(dump) { - return dump.filter( - (o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['media.class'] === 'Stream/Output/Audio', - ); -} -async function waitFor(predicate, timeoutMs, stepMs = 150) { - const deadline = Date.now() + timeoutMs; - let last; - while (Date.now() < deadline) { - last = predicate(); - if (last) return last; - await sleep(stepMs); - } - return last; -} -function killAll() { - for (const c of children) { - try { - c.kill('SIGKILL'); - } catch {} - } -} - -async function main() { - record('backend is pipewire', addon.audioBackend() === 'pipewire', addon.audioBackend()); - - const descendant = spawn( - 'pw-play', - ['--target', 'test_speakers', '-P', '{ application.name = "Descendant Player" }', tone], - { - stdio: 'ignore', - env: process.env, - }, - ); - children.push(descendant); - - const speakerIdOf = (d) => nodesByName(d, 'test_speakers')[0]?.id; - const sourcesInto = (dump) => { - const sid = Number(speakerIdOf(dump)); - return new Set( - links(dump) - .filter((l) => l.inNode === sid) - .map((l) => l.outNode), - ).size; - }; - - const pre = await waitFor(() => { - const d = pwDump(); - return streamNodes(d).length >= 5 && sourcesInto(d) >= 5 ? d : null; - }, 12000); - record( - 'all 5 playback streams routed to speakers pre-capture', - !!pre, - pre ? `${streamNodes(pre).length} streams, ${sourcesInto(pre)} routed` : 'timeout', - ); - const preDump = pre || pwDump(); - - const speakers = nodesByName(preDump, 'test_speakers'); - record('default sink test_speakers exists', speakers.length === 1); - const preSpeakerSources = sourcesInto(preDump); - record( - 'every app is playing to the real speakers pre-capture', - preSpeakerSources >= 5, - `${preSpeakerSources} distinct app streams -> speakers`, - ); - - const bridge = new addon.AudioBridge(); - record('AudioBridge on pipewire', bridge.backend() === 'pipewire', bridge.backend()); - record( - 'apply(system rule) accepted', - bridge.apply({onlySpeakers: true, onlyDefaultSpeakers: true, ignoreDevices: true}) === true, - ); - - const after = await waitFor(() => { - const d = pwDump(); - if (nodesByName(d, SINK_NAME).length !== 1) return null; - const g = bridge.routingGraph(); - return g.ownedLinks.length >= 2 ? {d, g} : null; - }, 10000); - - if (!after) { - record('fluxer sink + capture links established', false, 'timeout'); - await finish(bridge); - return; - } - const {d: dump, g: graph} = after; - - const sink = nodesByName(dump, SINK_NAME); - record('exactly one fluxer-screen-share node', sink.length === 1, `count=${sink.length}`); - const sp = sink[0]?.info?.props ?? {}; - record( - 'sink node.description is "Fluxer Screen Share Audio"', - sp['node.description'] === SINK_DESC, - sp['node.description'], - ); - record('sink media.class is Audio/Source/Virtual', sp['media.class'] === 'Audio/Source/Virtual', sp['media.class']); - record('sink node.virtual=true', String(sp['node.virtual']) === 'true'); - const sinkId = Number(sink[0]?.id); - - record( - 'all owned links are passive', - graph.ownedLinks.every((l) => l.passive === true), - `${graph.ownedLinks.length} links`, - ); - record( - 'all owned links terminate at the fluxer sink', - graph.ownedLinks.every((l) => Number(l.inputNodeId) === sinkId), - ); - - const captured = new Set(graph.ownedLinks.map((l) => Number(l.outputNodeId))); - const idsByPredicate = (pred) => - streamNodes(dump) - .filter((o) => pred(o.info.props)) - .map((o) => Number(o.id)); - - const normalIds = idsByPredicate( - (p) => - ['pw-play', 'Music Player Demo'].includes(p['application.name']) && - p['application.name'] !== 'Fluxer' && - p['application.name'] !== 'Descendant Player' && - !(p['node.name'] || '').startsWith('Fluxer '), - ); - const fluxerAppIds = idsByPredicate((p) => p['application.name'] === 'Fluxer'); - const fluxerNodeIds = idsByPredicate((p) => (p['node.name'] || '').startsWith('Fluxer ')); - const descendantIds = idsByPredicate((p) => p['application.name'] === 'Descendant Player'); - - record( - 'normal external apps ARE captured', - normalIds.length >= 2 && normalIds.every((id) => captured.has(id)), - `normal=${normalIds} captured=${[...captured]}`, - ); - record( - 'Fluxer-named app (application.name) is EXCLUDED', - fluxerAppIds.length >= 1 && fluxerAppIds.every((id) => !captured.has(id)), - `fluxerApp=${fluxerAppIds}`, - ); - record( - 'Fluxer-named app (node.name prefix) is EXCLUDED', - fluxerNodeIds.length >= 1 && fluxerNodeIds.every((id) => !captured.has(id)), - `fluxerNode=${fluxerNodeIds}`, - ); - record( - 'descendant-PID player is EXCLUDED (self-process tree)', - descendantIds.length >= 1 && descendantIds.every((id) => !captured.has(id)), - `descendant=${descendantIds}`, - ); - - const afterSpeakerSources = sourcesInto(dump); - record( - 'apps STILL play to real speakers during capture (tap, not move)', - afterSpeakerSources >= preSpeakerSources, - `before=${preSpeakerSources} after=${afterSpeakerSources} distinct app streams -> speakers`, - ); - - const meta = dump.find((o) => o.type === 'PipeWire:Interface:Metadata' && o.props?.['metadata.name'] === 'default'); - const def = meta?.metadata?.find((m) => m.key === 'default.audio.sink')?.value?.name; - record('default audio sink unchanged (test_speakers)', def === 'test_speakers', `default=${def}`); - - await finish(bridge); -} - -async function finish(bridge) { - bridge.release(); - const cleared = await waitFor(() => (bridge.routingGraph().ownedLinks.length === 0 ? true : null), 5000); - record( - 'release()+settle removes all owned links', - !!cleared, - cleared ? '0 owned links' : `still ${bridge.routingGraph().ownedLinks.length}`, - ); - await sleep(500); - const d = pwDump(); - const sinkId = Number(nodesByName(d, SINK_NAME)[0]?.id); - const residual = Number.isNaN(sinkId) ? 0 : links(d).filter((l) => l.inNode === sinkId).length; - record('no residual links into fluxer sink after release', residual === 0, `residual=${residual}`); - killAll(); - const failed = results.filter((r) => !r.ok).length; - console.log(`\n=== ${results.length - failed}/${results.length} checks passed ===`); - process.exit(failed === 0 ? 0 : 1); -} - -process.on('exit', killAll); -main().catch((e) => { - killAll(); - console.error('HARNESS ERROR:', e?.stack || e); - process.exit(2); -}); diff --git a/fluxer_desktop/native/linux-audio-capture/vm_tests/run_validation.sh b/fluxer_desktop/native/linux-audio-capture/vm_tests/run_validation.sh deleted file mode 100755 index e5ca85f78..000000000 --- a/fluxer_desktop/native/linux-audio-capture/vm_tests/run_validation.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -uo pipefail - -WORK="${FLX_WORK:-/home/parallels/flx-vmtest}" -ADDON_SO="${FLX_ADDON_SO:-/home/parallels/flx-target/debug/libfluxer_linux_audio_capture.so}" -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -mkdir -p "$WORK" -export XDG_RUNTIME_DIR="$WORK/xdg" -mkdir -p "$XDG_RUNTIME_DIR"; chmod 700 "$XDG_RUNTIME_DIR" -export PIPEWIRE_RUNTIME_DIR="$XDG_RUNTIME_DIR" -export PULSE_RUNTIME_PATH="$XDG_RUNTIME_DIR/pulse" -unset DISPLAY WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS - -PIDS=() -cleanup() { - for p in "${PIDS[@]:-}"; do kill -9 "$p" 2>/dev/null; done - pkill -9 -u "$(id -un)" -f "pipewire" 2>/dev/null - pkill -9 -u "$(id -un)" -f "wireplumber" 2>/dev/null -} -trap cleanup EXIT - -echo "=== starting private pipewire server (runtime=$XDG_RUNTIME_DIR) ===" -pipewire >"$WORK/pipewire.log" 2>&1 & PIDS+=($!) -sleep 0.8 -pipewire-pulse >"$WORK/pipewire-pulse.log" 2>&1 & PIDS+=($!) -sleep 0.5 -wireplumber >"$WORK/wireplumber.log" 2>&1 & PIDS+=($!) - -for _ in $(seq 1 40); do pw-cli info 0 >/dev/null 2>&1 && break; sleep 0.25; done -if ! pw-cli info 0 >/dev/null 2>&1; then - echo "FATAL: private pipewire did not come up"; cat "$WORK/pipewire.log"; exit 2 -fi - -echo "=== creating virtual speakers (default sink) ===" -pactl load-module module-null-sink sink_name=test_speakers sink_properties='device.description=Test_Speakers' >/dev/null 2>&1 -pactl set-default-sink test_speakers 2>/dev/null -sleep 0.4 - -echo "=== generating a real 600s stereo tone ===" -TONE="$WORK/tone.wav" -[ -f "$TONE" ] || ffmpeg -hide_banner -loglevel error -f lavfi -i "sine=frequency=440:duration=600" -ac 2 -ar 48000 "$TONE" /dev/null 2>&1 & PIDS+=($!) -pw-play --target test_speakers -P '{ application.name = "Music Player Demo" }' "$TONE" >/dev/null 2>&1 & PIDS+=($!) -pw-play --target test_speakers -P '{ application.name = "Fluxer" }' "$TONE" >/dev/null 2>&1 & PIDS+=($!) -pw-play --target test_speakers -P '{ node.name = "Fluxer Helper Stream" }' "$TONE" >/dev/null 2>&1 & PIDS+=($!) -sleep 1.5 - -echo "=== pre-test graph (fluxer sink should be ABSENT) ===" -pw-dump | node -e 'const d=JSON.parse(require("fs").readFileSync(0));const f=d.filter(o=>o.type==="PipeWire:Interface:Node"&&/fluxer-screen-share/.test(o.info?.props?.["node.name"]||""));console.log("fluxer sink nodes pre-test:",f.length);' - -echo "=== running napi SYSTEM-capture validation harness ===" -export FLX_ADDON_SO="$ADDON_SO" FLX_WORK="$WORK" -node "$HERE/pw_graph_validation.mjs" -HARNESS_RC=$? - -echo "=== running napi DIRECT (per-process) capture validation harness ===" -node "$HERE/direct_capture_validation.mjs" -DIRECT_RC=$? -[ "$DIRECT_RC" = "0" ] || HARNESS_RC=$DIRECT_RC - -echo "=== post-exit cleanup check (Drop must remove the fluxer sink) ===" -sleep 0.8 -RESIDUAL=$(pw-dump | node -e 'const d=JSON.parse(require("fs").readFileSync(0));const f=d.filter(o=>(o.type==="PipeWire:Interface:Node"&&/fluxer/.test(o.info?.props?.["node.name"]||""))||(o.type==="PipeWire:Interface:Link"&&/fluxer/.test(JSON.stringify(o.info?.props||{}))));console.log(f.length);') -if [ "$RESIDUAL" = "0" ]; then - echo "PASS no fluxer nodes/links remain after addon process exit (clean teardown)" -else - echo "FAIL $RESIDUAL residual fluxer objects after addon process exit" - HARNESS_RC=1 -fi - -echo "=== DONE rc=$HARNESS_RC ===" -exit $HARNESS_RC diff --git a/fluxer_desktop/native/nv12-gpu-pack/src/lib.rs b/fluxer_desktop/native/nv12-gpu-pack/src/lib.rs index 98a2943ee..70104c615 100644 --- a/fluxer_desktop/native/nv12-gpu-pack/src/lib.rs +++ b/fluxer_desktop/native/nv12-gpu-pack/src/lib.rs @@ -525,671 +525,3 @@ pub fn try_acquire_device() -> Option<(wgpu::Device, wgpu::Queue, wgpu::Instance Err(_) => None, } } - -#[cfg(test)] -mod tests { - use super::*; - use fluxer_gpu_rebuild::{GpuLossRegistry, RebuildOutcome}; - - struct GpuCtx { - device: wgpu::Device, - queue: wgpu::Queue, - _instance: wgpu::Instance, - } - - fn gpu_ctx() -> Option { - let (device, queue, instance) = try_acquire_device()?; - Some(GpuCtx { - device, - queue, - _instance: instance, - }) - } - - fn make_source_texture( - device: &wgpu::Device, - queue: &wgpu::Queue, - width: u32, - height: u32, - pixels: &[u8], - ) -> wgpu::Texture { - assert_eq!( - pixels.len() as u64, - u64::from(width) * u64::from(height) * 4 - ); - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("nv12_gpu_pack.test_source"), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - pixels, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(width * 4), - rows_per_image: Some(height), - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - texture - } - - fn make_storage_buffer(device: &wgpu::Device, size: u64, label: &str) -> wgpu::Buffer { - device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size, - usage: wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_SRC - | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }) - } - - fn read_buffer(device: &wgpu::Device, queue: &wgpu::Queue, src: &wgpu::Buffer) -> Vec { - let size = src.size(); - let staging = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("nv12_gpu_pack.test_staging"), - size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("nv12_gpu_pack.test_readback"), - }); - encoder.copy_buffer_to_buffer(src, 0, &staging, 0, size); - queue.submit(std::iter::once(encoder.finish())); - let slice = staging.slice(..); - let (tx, rx) = std::sync::mpsc::channel(); - slice.map_async(wgpu::MapMode::Read, move |res| { - let _ = tx.send(res); - }); - let _ = device.poll(wgpu::PollType::Wait { - submission_index: None, - timeout: None, - }); - let recv = rx.recv(); - assert!(recv.is_ok(), "channel must deliver map result"); - let map_result = recv.expect("map result channel"); - assert!(map_result.is_ok(), "map_async must succeed"); - let data = { - let range = slice.get_mapped_range(); - range.to_vec() - }; - staging.unmap(); - data - } - - fn pack_and_readback( - ctx: &GpuCtx, - packer: &Nv12Packer, - texture: &wgpu::Texture, - width: u32, - height: u32, - ) -> (Vec, Vec) { - let y_size = Nv12Packer::y_plane_size(width, height); - let uv_size = Nv12Packer::uv_plane_size(width, height); - let y_buf = make_storage_buffer(&ctx.device, y_size, "y"); - let uv_buf = make_storage_buffer(&ctx.device, uv_size, "uv"); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("nv12_gpu_pack.test_encoder"), - }); - let result = packer.pack(PackJob { - device: &ctx.device, - queue: &ctx.queue, - encoder: &mut encoder, - source: &view, - y_out: &y_buf, - uv_out: &uv_buf, - dims: (width, height), - }); - assert!(result.is_ok(), "pack must succeed: {result:?}"); - ctx.queue.submit(std::iter::once(encoder.finish())); - let y = read_buffer(&ctx.device, &ctx.queue, &y_buf); - let uv = read_buffer(&ctx.device, &ctx.queue, &uv_buf); - (y, uv) - } - - #[test] - fn solid_gray_packs_to_expected_bt709_limited() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 16; - let height: u32 = 8; - let mut pixels = vec![0u8; (width * height * 4) as usize]; - for chunk in pixels.chunks_exact_mut(4) { - chunk[0] = 128; - chunk[1] = 128; - chunk[2] = 128; - chunk[3] = 255; - } - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let packer = Nv12Packer::new(&ctx.device, width, height); - let (y, uv) = pack_and_readback(&ctx, &packer, &tex, width, height); - for value in &y { - assert!( - (124..=128).contains(value), - "Y value {value} out of expected range for gray 128" - ); - } - for value in &uv { - assert!( - (126..=130).contains(value), - "UV value {value} out of expected range for neutral gray" - ); - } - } - - #[test] - fn solid_red_v_is_above_neutral() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 16; - let height: u32 = 8; - let mut pixels = vec![0u8; (width * height * 4) as usize]; - for chunk in pixels.chunks_exact_mut(4) { - chunk[0] = 255; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 255; - } - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let packer = Nv12Packer::new(&ctx.device, width, height); - let (_y, uv) = pack_and_readback(&ctx, &packer, &tex, width, height); - for pair in uv.chunks_exact(2) { - let u = pair[0]; - let v = pair[1]; - assert!(u < 128, "expected U below neutral for pure red, got {u}"); - assert!( - v > 200, - "expected V well above neutral for pure red, got {v}" - ); - } - } - - #[test] - fn rejects_dims_exceeding_max() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let max_w: u32 = 8; - let max_h: u32 = 8; - let packer = Nv12Packer::new(&ctx.device, max_w, max_h); - let y_size = Nv12Packer::y_plane_size(16, 16); - let uv_size = Nv12Packer::uv_plane_size(16, 16); - let y_buf = make_storage_buffer(&ctx.device, y_size, "y"); - let uv_buf = make_storage_buffer(&ctx.device, uv_size, "uv"); - let dummy_pixels = vec![0u8; (max_w * max_h * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, max_w, max_h, &dummy_pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - let result = packer.pack(PackJob { - device: &ctx.device, - queue: &ctx.queue, - encoder: &mut encoder, - source: &view, - y_out: &y_buf, - uv_out: &uv_buf, - dims: (16, 16), - }); - assert!(matches!( - result, - Err(Nv12PackError::DimensionsExceedMax { .. }) - )); - } - - #[test] - fn rejects_odd_dims() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let packer = Nv12Packer::new(&ctx.device, 32, 32); - let y_buf = make_storage_buffer(&ctx.device, 64, "y"); - let uv_buf = make_storage_buffer(&ctx.device, 64, "uv"); - let dummy_pixels = vec![0u8; (8 * 8 * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, 8, 8, &dummy_pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - let result = packer.pack(PackJob { - device: &ctx.device, - queue: &ctx.queue, - encoder: &mut encoder, - source: &view, - y_out: &y_buf, - uv_out: &uv_buf, - dims: (7, 8), - }); - assert!(matches!( - result, - Err(Nv12PackError::DimensionsNotEven { .. }) - )); - } - - #[test] - fn determinism_two_runs_same_input_same_output() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 32; - let height: u32 = 16; - let mut pixels = vec![0u8; (width * height * 4) as usize]; - for (i, chunk) in pixels.chunks_exact_mut(4).enumerate() { - let v = (i % 251) as u8; - chunk[0] = v; - chunk[1] = v.wrapping_mul(2); - chunk[2] = v.wrapping_add(31); - chunk[3] = 255; - } - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let packer = Nv12Packer::new(&ctx.device, width, height); - let (y1, uv1) = pack_and_readback(&ctx, &packer, &tex, width, height); - let (y2, uv2) = pack_and_readback(&ctx, &packer, &tex, width, height); - assert_eq!(y1, y2, "Y plane must be deterministic"); - assert_eq!(uv1, uv2, "UV plane must be deterministic"); - } - - #[test] - fn buffer_too_small_is_rejected() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let packer = Nv12Packer::new(&ctx.device, 64, 64); - let y_buf = make_storage_buffer(&ctx.device, 16, "y"); - let uv_buf = make_storage_buffer(&ctx.device, 16, "uv"); - let pixels = vec![0u8; (16 * 16 * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, 16, 16, &pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - let result = packer.pack(PackJob { - device: &ctx.device, - queue: &ctx.queue, - encoder: &mut encoder, - source: &view, - y_out: &y_buf, - uv_out: &uv_buf, - dims: (16, 16), - }); - assert!(matches!(result, Err(Nv12PackError::YBufferTooSmall { .. }))); - } - - #[test] - fn packer_is_ready_only_when_built() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let mut packer = Nv12Packer::new(&ctx.device, 32, 32); - assert!( - packer.is_ready(), - "freshly constructed packer must be ready" - ); - packer.release(); - assert!(!packer.is_ready(), "released packer must not be ready"); - let rebuilt = packer.rebuild(&ctx.device, &ctx.queue); - assert!(rebuilt.is_ok(), "rebuild on a fresh device must succeed"); - assert!(packer.is_ready(), "rebuilt packer must be ready"); - } - - #[test] - fn pack_between_release_and_rebuild_returns_not_ready() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let mut packer = Nv12Packer::new(&ctx.device, 16, 16); - packer.release(); - let y_buf = make_storage_buffer(&ctx.device, 256, "y"); - let uv_buf = make_storage_buffer(&ctx.device, 256, "uv"); - let dummy_pixels = vec![0u8; (16 * 16 * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, 16, 16, &dummy_pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - let result = packer.pack(PackJob { - device: &ctx.device, - queue: &ctx.queue, - encoder: &mut encoder, - source: &view, - y_out: &y_buf, - uv_out: &uv_buf, - dims: (16, 16), - }); - assert!(matches!(result, Err(Nv12PackError::NotReady))); - } - - #[test] - fn registry_release_then_rebuild_invokes_callback_in_order() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let registry = GpuLossRegistry::new(); - let packer = Box::new(Nv12Packer::new(&ctx.device, 16, 16)); - let _guard = registry.register(packer); - let report = registry.handle_device_lost(&ctx.device, &ctx.queue); - assert_eq!(report.released_count, 1); - assert_eq!(report.rebuilt_count, 1); - assert_eq!(report.failed_count, 0); - assert!(report.is_total_success()); - } - - #[test] - fn after_loss_handling_packer_can_pack_again() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 16; - let height: u32 = 8; - let pixels = vec![128u8; (width * height * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let mut packer = Nv12Packer::new(&ctx.device, width, height); - packer.release(); - packer - .rebuild(&ctx.device, &ctx.queue) - .expect("rebuild must succeed after release"); - let (y, _uv) = pack_and_readback(&ctx, &packer, &tex, width, height); - assert!(!y.is_empty(), "Y plane must be non-empty after rebuild"); - assert!( - (124..=128).contains(&y[0]), - "Y value out of range for gray 128 after rebuild: {}", - y[0] - ); - } - - #[test] - fn determinism_across_rebuild_round_trip() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 32; - let height: u32 = 16; - let mut pixels = vec![0u8; (width * height * 4) as usize]; - for (i, chunk) in pixels.chunks_exact_mut(4).enumerate() { - let v = (i % 251) as u8; - chunk[0] = v; - chunk[1] = v.wrapping_mul(2); - chunk[2] = v.wrapping_add(31); - chunk[3] = 255; - } - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let mut packer = Nv12Packer::new(&ctx.device, width, height); - let (y1, uv1) = pack_and_readback(&ctx, &packer, &tex, width, height); - packer.release(); - packer - .rebuild(&ctx.device, &ctx.queue) - .expect("rebuild must succeed"); - let (y2, uv2) = pack_and_readback(&ctx, &packer, &tex, width, height); - assert_eq!(y1, y2, "Y plane must match across rebuild round-trip"); - assert_eq!(uv1, uv2, "UV plane must match across rebuild round-trip"); - } - - #[test] - fn double_rebuild_without_release_is_owner_invariant_error() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let mut packer = Nv12Packer::new(&ctx.device, 16, 16); - let outcome = packer.rebuild(&ctx.device, &ctx.queue); - assert!(matches!( - outcome, - Err(GpuRebuildError::OwnerInvariantBroken { .. }) - )); - } - - struct FailingRebuildOwner { - ready: bool, - } - - impl FailingRebuildOwner { - fn new() -> Self { - Self { ready: true } - } - } - - impl GpuLossCallback for FailingRebuildOwner { - fn release(&mut self) { - let was_ready = self.ready; - assert!( - was_ready, - "FailingRebuildOwner release must observe ready state", - ); - self.ready = false; - assert!(!self.ready, "FailingRebuildOwner postcondition"); - } - fn rebuild( - &mut self, - _device: &wgpu::Device, - _queue: &wgpu::Queue, - ) -> Result<(), GpuRebuildError> { - assert!( - !self.ready, - "rebuild must run after release in FailingRebuildOwner" - ); - Err(GpuRebuildError::DeviceRejected { - reason: "synthetic second-device failure", - }) - } - fn is_ready(&self) -> bool { - self.ready - } - fn debug_label(&self) -> &'static str { - "test.failing_rebuild_owner" - } - } - - #[test] - fn failed_rebuild_surfaces_in_report_and_owner_reports_not_ready() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let registry = GpuLossRegistry::new(); - let _guard = registry.register(Box::new(FailingRebuildOwner::new())); - let report = registry.handle_device_lost(&ctx.device, &ctx.queue); - assert_eq!(report.released_count, 1); - assert_eq!(report.failed_count, 1); - assert_eq!(report.rebuilt_count, 0); - assert!(!report.is_total_success()); - let failed = report - .outcomes - .iter() - .filter(|o| matches!(o, RebuildOutcome::Failed { .. })) - .count(); - assert_eq!(failed, 1); - } - - #[test] - fn multiple_packers_release_lifo_rebuild_fifo() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let registry = GpuLossRegistry::new(); - let mut guards = Vec::new(); - for _ in 0..3u32 { - let packer = Box::new(Nv12Packer::new(&ctx.device, 16, 16)); - guards.push(registry.register(packer)); - } - assert_eq!(registry.len(), 3); - let report = registry.handle_device_lost(&ctx.device, &ctx.queue); - assert_eq!(report.released_count, 3); - assert_eq!(report.rebuilt_count, 3); - assert_eq!(report.failed_count, 0); - for outcome in &report.outcomes { - assert!(matches!(outcome, RebuildOutcome::Rebuilt { .. })); - } - drop(guards); - } - - fn pack_one_pass( - ctx: &GpuCtx, - packer: &Nv12Packer, - view: &wgpu::TextureView, - y_buf: &wgpu::Buffer, - uv_buf: &wgpu::Buffer, - width: u32, - height: u32, - ) { - let mut encoder = ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("nv12_gpu_pack.cache_test_encoder"), - }); - let result = packer.pack(PackJob { - device: &ctx.device, - queue: &ctx.queue, - encoder: &mut encoder, - source: view, - y_out: y_buf, - uv_out: uv_buf, - dims: (width, height), - }); - assert!(result.is_ok(), "pack must succeed: {result:?}"); - ctx.queue.submit(std::iter::once(encoder.finish())); - } - - #[test] - fn cache_hit_when_pack_called_twice_with_same_inputs() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 32; - let height: u32 = 16; - let pixels = vec![64u8; (width * height * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let packer = Nv12Packer::new(&ctx.device, width, height); - let y_buf = make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y"); - let uv_buf = - make_storage_buffer(&ctx.device, Nv12Packer::uv_plane_size(width, height), "uv"); - assert_eq!(packer.cached_bind_group_count(), 0); - pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - } - - #[test] - fn cache_miss_when_buffers_change() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 32; - let height: u32 = 16; - let pixels = vec![64u8; (width * height * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let packer = Nv12Packer::new(&ctx.device, width, height); - let y_buf_a = - make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y_a"); - let uv_buf_a = make_storage_buffer( - &ctx.device, - Nv12Packer::uv_plane_size(width, height), - "uv_a", - ); - pack_one_pass(&ctx, &packer, &view, &y_buf_a, &uv_buf_a, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - let y_buf_b = - make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y_b"); - let uv_buf_b = make_storage_buffer( - &ctx.device, - Nv12Packer::uv_plane_size(width, height), - "uv_b", - ); - pack_one_pass(&ctx, &packer, &view, &y_buf_b, &uv_buf_b, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - pack_one_pass(&ctx, &packer, &view, &y_buf_a, &uv_buf_a, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - } - - #[test] - fn cache_invalidated_on_release() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 32; - let height: u32 = 16; - let pixels = vec![64u8; (width * height * 4) as usize]; - let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); - let mut packer = Nv12Packer::new(&ctx.device, width, height); - let y_buf = make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y"); - let uv_buf = - make_storage_buffer(&ctx.device, Nv12Packer::uv_plane_size(width, height), "uv"); - pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - packer.release(); - assert_eq!(packer.cached_bind_group_count(), 0); - packer - .rebuild(&ctx.device, &ctx.queue) - .expect("rebuild must succeed"); - assert_eq!(packer.cached_bind_group_count(), 0); - pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - } - - #[test] - fn cache_replaces_when_source_view_changes() { - let Some(ctx) = gpu_ctx() else { - eprintln!("nv12-gpu-pack: no wgpu adapter, skipping"); - return; - }; - let width: u32 = 32; - let height: u32 = 16; - let pixels = vec![64u8; (width * height * 4) as usize]; - let tex_a = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let tex_b = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels); - let view_a = tex_a.create_view(&wgpu::TextureViewDescriptor::default()); - let view_b = tex_b.create_view(&wgpu::TextureViewDescriptor::default()); - let packer = Nv12Packer::new(&ctx.device, width, height); - let y_buf = make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y"); - let uv_buf = - make_storage_buffer(&ctx.device, Nv12Packer::uv_plane_size(width, height), "uv"); - pack_one_pass(&ctx, &packer, &view_a, &y_buf, &uv_buf, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - pack_one_pass(&ctx, &packer, &view_b, &y_buf, &uv_buf, width, height); - assert_eq!(packer.cached_bind_group_count(), 1); - } -} diff --git a/fluxer_desktop/native/platform-info/index.js b/fluxer_desktop/native/platform-info/index.js index 3b3219b61..eab84e56c 100644 --- a/fluxer_desktop/native/platform-info/index.js +++ b/fluxer_desktop/native/platform-info/index.js @@ -3,6 +3,7 @@ const {existsSync} = require('node:fs'); const {join, sep} = require('node:path'); const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs'); +const {nativeFileName} = require('./pure.cjs'); const MODULE_NAME = '@fluxer/platform-info'; const SKIP_NATIVE_PROBE_ENV = 'FLUXER_PLATFORM_INFO_SKIP_NATIVE_PROBE'; @@ -13,21 +14,6 @@ function resolveNativeRoot() { return existsSync(unpackedDir) ? unpackedDir : __dirname; } -function nativeFileName(platform = process.platform, arch = process.arch) { - switch (platform) { - case 'darwin': - if (arch === 'x64' || arch === 'arm64') return `platform-info.darwin-${arch}.node`; - break; - case 'win32': - if (arch === 'x64' || arch === 'arm64') return `platform-info.win32-${arch}-msvc.node`; - break; - case 'linux': - if (arch === 'x64' || arch === 'arm64') return `platform-info.linux-${arch}-gnu.node`; - break; - } - throw new Error(`Unsupported platform-info target: ${platform}/${arch}`); -} - let binding = null; let loadError = null; diff --git a/fluxer_desktop/native/platform-info/package.json b/fluxer_desktop/native/platform-info/package.json index 3c2fd7413..b5517f6cb 100644 --- a/fluxer_desktop/native/platform-info/package.json +++ b/fluxer_desktop/native/platform-info/package.json @@ -14,6 +14,7 @@ "index.js", "index.d.ts", "loader-diagnostics.cjs", + "pure.cjs", "platform-info.darwin-x64.node", "platform-info.darwin-arm64.node", "platform-info.win32-x64-msvc.node", diff --git a/fluxer_desktop/native/platform-info/pure.cjs b/fluxer_desktop/native/platform-info/pure.cjs new file mode 100644 index 000000000..7523f25ec --- /dev/null +++ b/fluxer_desktop/native/platform-info/pure.cjs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +function nativeFileName(platform = process.platform, arch = process.arch) { + switch (platform) { + case 'darwin': + if (arch === 'x64' || arch === 'arm64') return `platform-info.darwin-${arch}.node`; + break; + case 'win32': + if (arch === 'x64' || arch === 'arm64') return `platform-info.win32-${arch}-msvc.node`; + break; + case 'linux': + if (arch === 'x64' || arch === 'arm64') return `platform-info.linux-${arch}-gnu.node`; + break; + } + throw new Error(`Unsupported platform-info target: ${platform}/${arch}`); +} + +module.exports = { + nativeFileName, +}; diff --git a/fluxer_desktop/native/platform-info/test/loader.test.mjs b/fluxer_desktop/native/platform-info/test/loader.test.mjs index 11cf0741d..12376630f 100644 --- a/fluxer_desktop/native/platform-info/test/loader.test.mjs +++ b/fluxer_desktop/native/platform-info/test/loader.test.mjs @@ -2,19 +2,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import platformInfo from '../index.js'; - -test('loader exposes a stable optional native surface', () => { - assert.equal(typeof platformInfo, 'object'); - assert.equal(platformInfo.getGpuInfo === null || typeof platformInfo.getGpuInfo === 'function', true); - assert.equal(platformInfo.loadError === null || platformInfo.loadError instanceof Error, true); -}); +import platformInfoPure from '../pure.cjs'; test('loader resolves native binaries for supported platforms and architectures', () => { - assert.equal(platformInfo._private.nativeFileName('darwin', 'x64'), 'platform-info.darwin-x64.node'); - assert.equal(platformInfo._private.nativeFileName('darwin', 'arm64'), 'platform-info.darwin-arm64.node'); - assert.equal(platformInfo._private.nativeFileName('linux', 'x64'), 'platform-info.linux-x64-gnu.node'); - assert.equal(platformInfo._private.nativeFileName('linux', 'arm64'), 'platform-info.linux-arm64-gnu.node'); - assert.equal(platformInfo._private.nativeFileName('win32', 'x64'), 'platform-info.win32-x64-msvc.node'); - assert.equal(platformInfo._private.nativeFileName('win32', 'arm64'), 'platform-info.win32-arm64-msvc.node'); + assert.equal(platformInfoPure.nativeFileName('darwin', 'x64'), 'platform-info.darwin-x64.node'); + assert.equal(platformInfoPure.nativeFileName('darwin', 'arm64'), 'platform-info.darwin-arm64.node'); + assert.equal(platformInfoPure.nativeFileName('linux', 'x64'), 'platform-info.linux-x64-gnu.node'); + assert.equal(platformInfoPure.nativeFileName('linux', 'arm64'), 'platform-info.linux-arm64-gnu.node'); + assert.equal(platformInfoPure.nativeFileName('win32', 'x64'), 'platform-info.win32-x64-msvc.node'); + assert.equal(platformInfoPure.nativeFileName('win32', 'arm64'), 'platform-info.win32-arm64-msvc.node'); }); diff --git a/fluxer_desktop/native/screen-frame-bus/src/gpu_loss.rs b/fluxer_desktop/native/screen-frame-bus/src/gpu_loss.rs index 0d50f24a2..2aefdd5a6 100644 --- a/fluxer_desktop/native/screen-frame-bus/src/gpu_loss.rs +++ b/fluxer_desktop/native/screen-frame-bus/src/gpu_loss.rs @@ -193,216 +193,10 @@ fn build_resources(device: &wgpu::Device, config: WgpuStagingConfig) -> WgpuStag } } -pub fn try_acquire_device() -> Option<(wgpu::Device, wgpu::Queue, wgpu::Instance)> { - let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle(); - descriptor.backends = wgpu::Backends::all() | wgpu::Backends::SECONDARY; - let instance = wgpu::Instance::new(descriptor); - let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - force_fallback_adapter: false, - compatible_surface: None, - })) - .ok()?; - let device_result = pollster_block_on(adapter.request_device(&wgpu::DeviceDescriptor { - label: Some("screen_frame_bus.wgpu_staging_backend.test_device"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::default(), - memory_hints: wgpu::MemoryHints::default(), - trace: wgpu::Trace::Off, - experimental_features: wgpu::ExperimentalFeatures::default(), - })); - match device_result { - Ok((device, queue)) => Some((device, queue, instance)), - Err(_) => None, - } -} - -fn pollster_block_on(fut: F) -> F::Output { - futures_executor_block_on(fut) -} - -fn futures_executor_block_on(mut fut: F) -> F::Output { - use core::pin::Pin; - use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; - fn raw_waker() -> RawWaker { - fn no_op(_: *const ()) {} - fn clone(_: *const ()) -> RawWaker { - raw_waker() - } - static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op); - RawWaker::new(core::ptr::null(), &VTABLE) - } - let waker = unsafe { Waker::from_raw(raw_waker()) }; - let mut cx = Context::from_waker(&waker); - let mut fut = unsafe { Pin::new_unchecked(&mut fut) }; - loop { - match fut.as_mut().poll(&mut cx) { - Poll::Ready(out) => return out, - Poll::Pending => continue, - } - } -} - #[cfg(test)] mod tests { - use super::*; - use crate::{CpuStagingBackend, STAGING_PAIR_LEN, StagingSurfacePair}; - use fluxer_gpu_rebuild::{GpuLossRegistry, RebuildOutcome}; - - struct GpuCtx { - device: wgpu::Device, - queue: wgpu::Queue, - _instance: wgpu::Instance, - } - - fn gpu_ctx() -> Option { - let acquired = std::panic::catch_unwind(std::panic::AssertUnwindSafe(try_acquire_device)); - let (device, queue, instance) = match acquired { - Ok(Some(triple)) => triple, - Ok(None) => return None, - Err(_) => return None, - }; - Some(GpuCtx { - device, - queue, - _instance: instance, - }) - } - - #[test] - fn staging_backend_is_ready_only_when_built() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let cfg = WgpuStagingConfig::new(256); - let mut backend = WgpuStagingBackend::new(&ctx.device, cfg); - assert!( - GpuLossCallback::is_ready(&backend), - "freshly built backend must be ready", - ); - backend.release(); - assert!( - !GpuLossCallback::is_ready(&backend), - "released backend must not be ready", - ); - let outcome = backend.rebuild(&ctx.device, &ctx.queue); - assert!(outcome.is_ok(), "rebuild on fresh device must succeed"); - assert!( - GpuLossCallback::is_ready(&backend), - "rebuilt backend must be ready", - ); - } - - #[test] - fn registry_handles_staging_backend_round_trip() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let registry = GpuLossRegistry::new(); - let backend = Box::new(WgpuStagingBackend::new( - &ctx.device, - WgpuStagingConfig::new(128), - )); - let _guard = registry.register(backend); - let report = registry.handle_device_lost(&ctx.device, &ctx.queue); - assert_eq!(report.released_count, 1); - assert_eq!(report.rebuilt_count, 1); - assert_eq!(report.failed_count, 0); - assert!(report.is_total_success()); - } - - #[test] - fn double_rebuild_without_release_is_owner_invariant_error() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let mut backend = WgpuStagingBackend::new(&ctx.device, WgpuStagingConfig::new(64)); - let outcome = backend.rebuild(&ctx.device, &ctx.queue); - assert!(matches!( - outcome, - Err(GpuRebuildError::OwnerInvariantBroken { .. }) - )); - } - - #[test] - fn write_between_release_and_rebuild_is_a_noop_and_not_ready() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let mut backend = WgpuStagingBackend::new(&ctx.device, WgpuStagingConfig::new(64)); - backend.release(); - backend.write(|buf| buf.fill(0xAA)); - assert!( - !::is_ready(&backend), - "write before rebuild must not flip ready", - ); - let observed = backend.read(|buf| buf.len()); - assert_eq!(observed, 0, "read before rebuild must observe empty mirror"); - } - - #[test] - fn surface_pair_with_wgpu_backend_round_trips_cpu_mirror() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let cfg = WgpuStagingConfig::new(64); - let a = WgpuStagingBackend::new(&ctx.device, cfg); - let b = WgpuStagingBackend::new(&ctx.device, cfg); - assert_eq!( - STAGING_PAIR_LEN, 2, - "OBS staging pair is exactly two surfaces" - ); - let mut pair: StagingSurfacePair = StagingSurfacePair::new([a, b]); - pair.submit(0, |buf| { - buf[0] = 0xDE; - buf[1] = 0xAD; - }) - .expect("submit zero must succeed"); - pair.submit(1, |buf| { - buf[0] = 0xBE; - buf[1] = 0xEF; - }) - .expect("submit one must succeed"); - let first = pair - .try_map(0, |buf| (buf[0], buf[1])) - .expect("map zero ready"); - assert_eq!(first, (0xDE, 0xAD)); - let second = pair - .try_map(1, |buf| (buf[0], buf[1])) - .expect("map one ready"); - assert_eq!(second, (0xBE, 0xEF)); - } - - #[test] - fn registry_holds_mixed_packers_and_staging_backends() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let registry = GpuLossRegistry::new(); - let mut guards = Vec::new(); - for i in 0..4u32 { - let bytes = 32u64 << (i % 4); - let backend = Box::new(WgpuStagingBackend::new( - &ctx.device, - WgpuStagingConfig::new(bytes), - )); - guards.push(registry.register(backend)); - } - let report = registry.handle_device_lost(&ctx.device, &ctx.queue); - assert_eq!(report.released_count, 4); - assert_eq!(report.rebuilt_count, 4); - assert_eq!(report.failed_count, 0); - for outcome in &report.outcomes { - assert!(matches!(outcome, RebuildOutcome::Rebuilt { .. })); - } - drop(guards); - } + use super::StagingBackend; + use crate::CpuStagingBackend; #[test] fn cpu_backend_still_works_alongside_wgpu_backend() { @@ -410,20 +204,4 @@ mod tests { assert!(::is_idle(&cpu)); assert!(::is_ready(&cpu)); } - - #[test] - fn buffer_handle_disappears_after_release_and_reappears_after_rebuild() { - let Some(ctx) = gpu_ctx() else { - eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping"); - return; - }; - let mut backend = WgpuStagingBackend::new(&ctx.device, WgpuStagingConfig::new(128)); - assert!(backend.buffer().is_some(), "buffer present after build"); - backend.release(); - assert!(backend.buffer().is_none(), "buffer absent after release"); - backend - .rebuild(&ctx.device, &ctx.queue) - .expect("rebuild must succeed"); - assert!(backend.buffer().is_some(), "buffer present after rebuild"); - } } diff --git a/fluxer_desktop/native/webauthn/index.js b/fluxer_desktop/native/webauthn/index.js index e4287be67..0e149f5e8 100644 --- a/fluxer_desktop/native/webauthn/index.js +++ b/fluxer_desktop/native/webauthn/index.js @@ -4,25 +4,16 @@ const {existsSync} = require('node:fs'); const {createHash} = require('node:crypto'); const {join, sep} = require('node:path'); const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs'); +const { + base64Url, + clientDataJSON, + nativeFileName, + normalizeCreateOptions, + normalizeGetOptions, + transportBits, +} = require('./pure.cjs'); const MODULE_NAME = '@fluxer/webauthn'; const SKIP_NATIVE_PROBE_ENV = 'FLUXER_WEBAUTHN_SKIP_NATIVE_PROBE'; -const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY = 0; -const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM = 1; -const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 2; -const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; -const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; -const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; -const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 1; -const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 2; -const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 3; -const WEBAUTHN_ENTERPRISE_ATTESTATION_NONE = 0; -const WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED = 1; -const WEBAUTHN_CTAP_TRANSPORT_USB = 0x00000001; -const WEBAUTHN_CTAP_TRANSPORT_NFC = 0x00000002; -const WEBAUTHN_CTAP_TRANSPORT_BLE = 0x00000004; -const WEBAUTHN_CTAP_TRANSPORT_INTERNAL = 0x00000010; -const WEBAUTHN_CTAP_TRANSPORT_HYBRID = 0x00000020; -const WEBAUTHN_CTAP_TRANSPORT_SMART_CARD = 0x00000040; function resolveNativeRoot() { const asarSegment = `${sep}app.asar${sep}`; @@ -31,13 +22,6 @@ function resolveNativeRoot() { return existsSync(unpackedDir) ? unpackedDir : __dirname; } -function nativeFileName(platform = process.platform, arch = process.arch) { - if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) return `webauthn.darwin-${arch}.node`; - if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) return `webauthn.win32-${arch}-msvc.node`; - if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `webauthn.linux-${arch}-gnu.node`; - return null; -} - let binding = null; let loadError = null; @@ -79,146 +63,10 @@ if (fileName) { throw loadError; } -function base64Url(buffer) { - return Buffer.from(buffer).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -} - -function normalizedOrigin(origin) { - if (typeof origin !== 'string' || origin.length === 0) { - throw new TypeError('@fluxer/webauthn requires an origin string'); - } - return new URL(origin).origin; -} - -function relyingPartyId(options, origin) { - if (typeof options.rpId === 'string' && options.rpId.length > 0) return options.rpId; - if (options.rp && typeof options.rp.id === 'string' && options.rp.id.length > 0) return options.rp.id; - return new URL(origin).hostname; -} - -function clientDataJSON(type, challenge, origin) { - return Buffer.from( - JSON.stringify({ - type, - challenge: base64Url(challenge), - origin, - crossOrigin: false, - }), - 'utf8', - ); -} - function clientDataHash(data) { return createHash('sha256').update(data).digest(); } -function authenticatorAttachment(value) { - if (value === 'platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM; - if (value === 'cross-platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM; - return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY; -} - -function userVerification(value) { - if (value === 'required') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED; - if (value === 'discouraged') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED; - return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED; -} - -function attestation(value) { - if (value === 'direct') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT; - if (value === 'indirect') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT; - return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE; -} - -function enterpriseAttestation(value) { - return value === 'enterprise' - ? WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED - : WEBAUTHN_ENTERPRISE_ATTESTATION_NONE; -} - -function transportBits(transports) { - if (!Array.isArray(transports)) return 0; - let bits = 0; - for (const transport of transports) { - if (transport === 'usb') bits |= WEBAUTHN_CTAP_TRANSPORT_USB; - else if (transport === 'nfc') bits |= WEBAUTHN_CTAP_TRANSPORT_NFC; - else if (transport === 'ble') bits |= WEBAUTHN_CTAP_TRANSPORT_BLE; - else if (transport === 'internal') bits |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL; - else if (transport === 'hybrid') bits |= WEBAUTHN_CTAP_TRANSPORT_HYBRID; - else if (transport === 'smart-card') bits |= WEBAUTHN_CTAP_TRANSPORT_SMART_CARD; - } - return bits; -} - -function credentialDescriptors(descriptors) { - if (!Array.isArray(descriptors)) return []; - return descriptors.map((descriptor) => ({ - id: Buffer.from(descriptor.id), - transports: transportBits(descriptor.transports), - })); -} - -function windowHandleBuffer(value) { - if (Buffer.isBuffer(value)) return value; - if (value instanceof Uint8Array) return Buffer.from(value); - return undefined; -} - -function pinString(value) { - return typeof value === 'string' && value.length > 0 ? value : undefined; -} - -function residentKeyFlags(selection) { - const residentKey = selection?.residentKey; - const requireResidentKey = Boolean(selection?.requireResidentKey) || residentKey === 'required'; - return { - requireResidentKey, - preferResidentKey: requireResidentKey || residentKey === 'preferred', - }; -} - -function normalizeCreateOptions(options) { - if (!options || typeof options !== 'object') throw new TypeError('registration options must be an object'); - const origin = normalizedOrigin(options.origin); - const selection = options.authenticatorSelection || {}; - const residentKeys = residentKeyFlags(selection); - return { - rpId: relyingPartyId(options, origin), - rpName: options.rp && typeof options.rp.name === 'string' ? options.rp.name : relyingPartyId(options, origin), - challenge: Buffer.from(options.challenge), - userId: Buffer.from(options.user.id), - userName: options.user.name, - userDisplayName: options.user.displayName, - clientDataJSON: clientDataJSON('webauthn.create', options.challenge, origin), - pubKeyCredParams: options.pubKeyCredParams, - excludeCredentials: credentialDescriptors(options.excludeCredentials), - timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0, - authenticatorAttachment: authenticatorAttachment(selection.authenticatorAttachment), - userVerification: userVerification(selection.userVerification), - attestation: attestation(options.attestation), - enterpriseAttestation: enterpriseAttestation(options.attestation), - windowHandle: windowHandleBuffer(options.windowHandle), - pin: pinString(options.pin), - ...residentKeys, - }; -} - -function normalizeGetOptions(options) { - if (!options || typeof options !== 'object') throw new TypeError('assertion options must be an object'); - const origin = normalizedOrigin(options.origin); - return { - rpId: relyingPartyId(options, origin), - challenge: Buffer.from(options.challenge), - clientDataJSON: clientDataJSON('webauthn.get', options.challenge, origin), - allowCredentials: credentialDescriptors(options.allowCredentials), - timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0, - authenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY, - userVerification: userVerification(options.userVerification), - windowHandle: windowHandleBuffer(options.windowHandle), - pin: pinString(options.pin), - }; -} - function unavailableError() { return new Error( `@fluxer/webauthn native backend unavailable on ${process.platform}/${process.arch}: ${ diff --git a/fluxer_desktop/native/webauthn/package.json b/fluxer_desktop/native/webauthn/package.json index 60eccd3ca..2623b8270 100644 --- a/fluxer_desktop/native/webauthn/package.json +++ b/fluxer_desktop/native/webauthn/package.json @@ -14,6 +14,7 @@ "index.js", "index.d.ts", "loader-diagnostics.cjs", + "pure.cjs", "*.so*", "webauthn.linux-x64-gnu.node", "webauthn.linux-arm64-gnu.node", diff --git a/fluxer_desktop/native/webauthn/pure.cjs b/fluxer_desktop/native/webauthn/pure.cjs new file mode 100644 index 000000000..0a0655b8c --- /dev/null +++ b/fluxer_desktop/native/webauthn/pure.cjs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY = 0; +const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM = 1; +const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 2; +const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; +const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; +const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; +const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 1; +const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 2; +const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 3; +const WEBAUTHN_ENTERPRISE_ATTESTATION_NONE = 0; +const WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED = 1; +const WEBAUTHN_CTAP_TRANSPORT_USB = 0x00000001; +const WEBAUTHN_CTAP_TRANSPORT_NFC = 0x00000002; +const WEBAUTHN_CTAP_TRANSPORT_BLE = 0x00000004; +const WEBAUTHN_CTAP_TRANSPORT_INTERNAL = 0x00000010; +const WEBAUTHN_CTAP_TRANSPORT_HYBRID = 0x00000020; +const WEBAUTHN_CTAP_TRANSPORT_SMART_CARD = 0x00000040; + +function nativeFileName(platform = process.platform, arch = process.arch) { + if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) return `webauthn.darwin-${arch}.node`; + if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) return `webauthn.win32-${arch}-msvc.node`; + if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `webauthn.linux-${arch}-gnu.node`; + return null; +} + +function base64Url(buffer) { + return Buffer.from(buffer).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function normalizedOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) { + throw new TypeError('@fluxer/webauthn requires an origin string'); + } + return new URL(origin).origin; +} + +function relyingPartyId(options, origin) { + if (typeof options.rpId === 'string' && options.rpId.length > 0) return options.rpId; + if (options.rp && typeof options.rp.id === 'string' && options.rp.id.length > 0) return options.rp.id; + return new URL(origin).hostname; +} + +function clientDataJSON(type, challenge, origin) { + return Buffer.from( + JSON.stringify({ + type, + challenge: base64Url(challenge), + origin, + crossOrigin: false, + }), + 'utf8', + ); +} + +function authenticatorAttachment(value) { + if (value === 'platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM; + if (value === 'cross-platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM; + return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY; +} + +function userVerification(value) { + if (value === 'required') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED; + if (value === 'discouraged') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED; +} + +function attestation(value) { + if (value === 'direct') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT; + if (value === 'indirect') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT; + return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE; +} + +function enterpriseAttestation(value) { + return value === 'enterprise' + ? WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED + : WEBAUTHN_ENTERPRISE_ATTESTATION_NONE; +} + +function transportBits(transports) { + if (!Array.isArray(transports)) return 0; + let bits = 0; + for (const transport of transports) { + if (transport === 'usb') bits |= WEBAUTHN_CTAP_TRANSPORT_USB; + else if (transport === 'nfc') bits |= WEBAUTHN_CTAP_TRANSPORT_NFC; + else if (transport === 'ble') bits |= WEBAUTHN_CTAP_TRANSPORT_BLE; + else if (transport === 'internal') bits |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL; + else if (transport === 'hybrid') bits |= WEBAUTHN_CTAP_TRANSPORT_HYBRID; + else if (transport === 'smart-card') bits |= WEBAUTHN_CTAP_TRANSPORT_SMART_CARD; + } + return bits; +} + +function credentialDescriptors(descriptors) { + if (!Array.isArray(descriptors)) return []; + return descriptors.map((descriptor) => ({ + id: Buffer.from(descriptor.id), + transports: transportBits(descriptor.transports), + })); +} + +function windowHandleBuffer(value) { + if (Buffer.isBuffer(value)) return value; + if (value instanceof Uint8Array) return Buffer.from(value); + return undefined; +} + +function pinString(value) { + return typeof value === 'string' && value.length > 0 ? value : undefined; +} + +function residentKeyFlags(selection) { + const residentKey = selection?.residentKey; + const requireResidentKey = Boolean(selection?.requireResidentKey) || residentKey === 'required'; + return { + requireResidentKey, + preferResidentKey: requireResidentKey || residentKey === 'preferred', + }; +} + +function normalizeCreateOptions(options) { + if (!options || typeof options !== 'object') throw new TypeError('registration options must be an object'); + const origin = normalizedOrigin(options.origin); + const selection = options.authenticatorSelection || {}; + const residentKeys = residentKeyFlags(selection); + return { + rpId: relyingPartyId(options, origin), + rpName: options.rp && typeof options.rp.name === 'string' ? options.rp.name : relyingPartyId(options, origin), + challenge: Buffer.from(options.challenge), + userId: Buffer.from(options.user.id), + userName: options.user.name, + userDisplayName: options.user.displayName, + clientDataJSON: clientDataJSON('webauthn.create', options.challenge, origin), + pubKeyCredParams: options.pubKeyCredParams, + excludeCredentials: credentialDescriptors(options.excludeCredentials), + timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0, + authenticatorAttachment: authenticatorAttachment(selection.authenticatorAttachment), + userVerification: userVerification(selection.userVerification), + attestation: attestation(options.attestation), + enterpriseAttestation: enterpriseAttestation(options.attestation), + windowHandle: windowHandleBuffer(options.windowHandle), + pin: pinString(options.pin), + ...residentKeys, + }; +} + +function normalizeGetOptions(options) { + if (!options || typeof options !== 'object') throw new TypeError('assertion options must be an object'); + const origin = normalizedOrigin(options.origin); + return { + rpId: relyingPartyId(options, origin), + challenge: Buffer.from(options.challenge), + clientDataJSON: clientDataJSON('webauthn.get', options.challenge, origin), + allowCredentials: credentialDescriptors(options.allowCredentials), + timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0, + authenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY, + userVerification: userVerification(options.userVerification), + windowHandle: windowHandleBuffer(options.windowHandle), + pin: pinString(options.pin), + }; +} + +module.exports = { + base64Url, + clientDataJSON, + nativeFileName, + normalizeCreateOptions, + normalizeGetOptions, + transportBits, +}; diff --git a/fluxer_desktop/native/webauthn/test/loader.test.mjs b/fluxer_desktop/native/webauthn/test/loader.test.mjs index 422c72019..1e9605952 100644 --- a/fluxer_desktop/native/webauthn/test/loader.test.mjs +++ b/fluxer_desktop/native/webauthn/test/loader.test.mjs @@ -2,37 +2,21 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import webauthn from '../index.js'; +import webauthnPure from '../pure.cjs'; test('loader resolves supported native filenames', () => { - assert.equal(webauthn._private.nativeFileName('darwin', 'x64'), 'webauthn.darwin-x64.node'); - assert.equal(webauthn._private.nativeFileName('darwin', 'arm64'), 'webauthn.darwin-arm64.node'); - assert.equal(webauthn._private.nativeFileName('linux', 'x64'), 'webauthn.linux-x64-gnu.node'); - assert.equal(webauthn._private.nativeFileName('linux', 'arm64'), 'webauthn.linux-arm64-gnu.node'); - assert.equal(webauthn._private.nativeFileName('win32', 'x64'), 'webauthn.win32-x64-msvc.node'); - assert.equal(webauthn._private.nativeFileName('win32', 'arm64'), 'webauthn.win32-arm64-msvc.node'); - assert.equal(webauthn._private.nativeFileName('freebsd', 'x64'), null); -}); - -test('loader exposes the Fluxer WebAuthn surface', async () => { - assert.equal(typeof webauthn.create, 'function'); - assert.equal(typeof webauthn.get, 'function'); - assert.equal(typeof webauthn.getBackendInfo, 'function'); - assert.equal(typeof webauthn.isSupported, 'function'); - const info = webauthn.getBackendInfo(); - const supported = await webauthn.isSupported(); - assert.equal(typeof supported, 'boolean'); - assert.equal(supported, info.supported); - assert.equal(typeof info, 'object'); - assert.equal(typeof info.target, 'string'); - assert.equal(typeof info.backend, 'string'); - assert.equal(typeof info.supported, 'boolean'); - assert.equal(typeof info.ceremoniesImplemented, 'boolean'); + assert.equal(webauthnPure.nativeFileName('darwin', 'x64'), 'webauthn.darwin-x64.node'); + assert.equal(webauthnPure.nativeFileName('darwin', 'arm64'), 'webauthn.darwin-arm64.node'); + assert.equal(webauthnPure.nativeFileName('linux', 'x64'), 'webauthn.linux-x64-gnu.node'); + assert.equal(webauthnPure.nativeFileName('linux', 'arm64'), 'webauthn.linux-arm64-gnu.node'); + assert.equal(webauthnPure.nativeFileName('win32', 'x64'), 'webauthn.win32-x64-msvc.node'); + assert.equal(webauthnPure.nativeFileName('win32', 'arm64'), 'webauthn.win32-arm64-msvc.node'); + assert.equal(webauthnPure.nativeFileName('freebsd', 'x64'), null); }); test('normalization creates spec-shaped client data and Windows transport bits', () => { const challenge = Buffer.from([1, 2, 3, 4]); - const normalized = webauthn._private.normalizeCreateOptions({ + const normalized = webauthnPure.normalizeCreateOptions({ origin: 'https://web.canary.fluxer.app/channels/@me', challenge, rp: {id: 'fluxer.app', name: 'Fluxer'}, diff --git a/fluxer_desktop/native/webauthn/vm_tests/run_in_session.ps1 b/fluxer_desktop/native/webauthn/vm_tests/run_in_session.ps1 deleted file mode 100644 index 594f53a64..000000000 --- a/fluxer_desktop/native/webauthn/vm_tests/run_in_session.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -param( - [Parameter(Mandatory = $true)][string]$Command, - [string]$OutFile = 'C:\tools\vm_test_out.txt', - [int]$TimeoutSec = 120 -) - -$taskName = 'FluxerVmTest' -if (Test-Path $OutFile) { Remove-Item -Force $OutFile } -Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - -$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c ' + $Command + ' > "' + $OutFile + '" 2>&1') -$principal = New-ScheduledTaskPrincipal -UserId 'hampus' -LogonType Interactive -$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null -Start-ScheduledTask -TaskName $taskName - -$deadline = (Get-Date).AddSeconds($TimeoutSec) -do { - Start-Sleep -Milliseconds 500 - $state = (Get-ScheduledTask -TaskName $taskName).State -} while ($state -ne 'Ready' -and (Get-Date) -lt $deadline) - -$info = Get-ScheduledTaskInfo -TaskName $taskName -Write-Output ('TASK-STATE: ' + $state + ' LAST-RESULT: ' + $info.LastTaskResult) -if (Test-Path $OutFile) { - Write-Output '--- OUTPUT ---' - Get-Content $OutFile -} else { - Write-Output 'NO OUTPUT FILE' -} -Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue diff --git a/fluxer_desktop/native/webauthn/vm_tests/set_pin.py b/fluxer_desktop/native/webauthn/vm_tests/set_pin.py deleted file mode 100644 index 551c70c71..000000000 --- a/fluxer_desktop/native/webauthn/vm_tests/set_pin.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later - -import ctypes -import sys - -device_path = sys.argv[1].encode() if len(sys.argv) > 1 else b"/dev/hidraw4" -pin = sys.argv[2].encode() if len(sys.argv) > 2 else b"123456" - -lib = ctypes.CDLL("libfido2.so.1") -lib.fido_init(0) -lib.fido_dev_new.restype = ctypes.c_void_p -lib.fido_strerr.restype = ctypes.c_char_p - -dev = lib.fido_dev_new() -rc = lib.fido_dev_open(ctypes.c_void_p(dev), device_path) -if rc != 0: - print(f"open failed rc={rc} {lib.fido_strerr(rc).decode()}") - sys.exit(1) -rc = lib.fido_dev_set_pin(ctypes.c_void_p(dev), pin, None) -print(f"set_pin rc={rc} {lib.fido_strerr(rc).decode()}") -lib.fido_dev_close(ctypes.c_void_p(dev)) -sys.exit(0 if rc == 0 else 1) diff --git a/fluxer_desktop/native/webauthn/vm_tests/vfido_authenticator/main.go b/fluxer_desktop/native/webauthn/vm_tests/vfido_authenticator/main.go deleted file mode 100644 index fa533c6a3..000000000 --- a/fluxer_desktop/native/webauthn/vm_tests/vfido_authenticator/main.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "crypto/x509/pkix" - "fmt" - "io" - "math/big" - "os" - "time" - - virtual_fido "github.com/bulwarkid/virtual-fido" - "github.com/bulwarkid/virtual-fido/fido_client" -) - -type autoApproveSupport struct { - vaultFilename string -} - -func (support *autoApproveSupport) ApproveClientAction(action fido_client.ClientAction, params fido_client.ClientActionRequestParams) bool { - fmt.Printf("auto-approving action=%d relyingParty=%q user=%q\n", action, params.RelyingParty, params.UserName) - return true -} - -func (support *autoApproveSupport) SaveData(data []byte) { - err := os.WriteFile(support.vaultFilename, data, 0o600) - if err != nil { - panic(fmt.Sprintf("could not write vault: %s", err)) - } -} - -func (support *autoApproveSupport) RetrieveData() []byte { - f, err := os.Open(support.vaultFilename) - if os.IsNotExist(err) { - return nil - } - if err != nil { - panic(fmt.Sprintf("could not open vault: %s", err)) - } - defer f.Close() - data, err := io.ReadAll(f) - if err != nil { - panic(fmt.Sprintf("could not read vault: %s", err)) - } - return data -} - -func (support *autoApproveSupport) Passphrase() string { - return "vm-test-passphrase" -} - -func main() { - vault := os.Getenv("VFIDO_VAULT") - if vault == "" { - vault = "/tmp/vfido-vault.json" - } - authority := &x509.Certificate{ - SerialNumber: big.NewInt(0), - Subject: pkix.Name{ - Organization: []string{"Fluxer VM Test Virtual FIDO"}, - Country: []string{"US"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - } - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - authorityCertBytes, err := x509.CreateCertificate(rand.Reader, authority, authority, &privateKey.PublicKey, privateKey) - if err != nil { - panic(err) - } - encryptionKey := sha256.Sum256([]byte("fluxer-vm-test")) - - virtual_fido.SetLogOutput(os.Stdout) - support := &autoApproveSupport{vaultFilename: vault} - client := fido_client.NewDefaultClient(authorityCertBytes, privateKey, encryptionKey, support, support) - fmt.Println("virtual-fido USBIP server starting on 127.0.0.1:3240 (bus 2-2)") - virtual_fido.Start(client) -} diff --git a/fluxer_desktop/native/webrtc-sender/package.json b/fluxer_desktop/native/webrtc-sender/package.json index d13fddb49..a22b70e92 100644 --- a/fluxer_desktop/native/webrtc-sender/package.json +++ b/fluxer_desktop/native/webrtc-sender/package.json @@ -27,7 +27,6 @@ ], "scripts": { "build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon", - "test": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- test-webrtc-sender-rust && node --test index.test.mjs scripts/livekit-harness.test.mjs", - "test:livekit": "node scripts/livekit-harness.mjs" + "test": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- test-webrtc-sender-rust && node --test index.test.mjs" } } diff --git a/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs b/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs deleted file mode 100644 index 19d67c285..000000000 --- a/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.mjs +++ /dev/null @@ -1,2058 +0,0 @@ -#!/usr/bin/env node -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {createHmac, randomUUID} from 'node:crypto'; -import {writeFile} from 'node:fs/promises'; -import {createRequire} from 'node:module'; -import net from 'node:net'; -import {setTimeout as delay} from 'node:timers/promises'; -import {fileURLToPath} from 'node:url'; - -const require = createRequire(import.meta.url); -const {VoiceEngine, getHardwareEncoderCapabilities, isSupported, loadError} = require('../index.js'); - -const DEFAULT_URL = 'ws://localhost:7880'; -const DEFAULT_API_KEY = 'devkey'; -const DEFAULT_API_SECRET = 'secret'; -const DEFAULT_TIMEOUT_MS = 30_000; -const DEFAULT_CONNECT_TIMEOUT_MS = 30_000; -const DEFAULT_DISCONNECT_TIMEOUT_MS = 10_000; -const DEFAULT_PROBE_TIMEOUT_MS = 1_500; -const DEFAULT_WIDTH = 320; -const DEFAULT_HEIGHT = 180; -const DEFAULT_FPS = 15; -const DEFAULT_SAMPLE_RATE = 48_000; -const DEFAULT_CHANNELS = 1; -const DEFAULT_AUDIO_CHUNK_MS = 20; -const STRICT_DURATION_MS = 10 * 60 * 1000; -const STRICT_MIN_FPS_RATIO = 0.95; -const STRICT_MAX_FRAME_GAP_MS = 250; -const STRICT_MAX_AV_DRIFT_MS = 80; -const STRICT_MAX_AUDIO_FRAME_GAP_MS = 250; -const AUDIO_POLL_INTERVAL_MS = 100; -const MIN_INBOUND_AUDIO_FRAMES = 3; -const MIN_INBOUND_VIDEO_FRAMES = 3; -const VIDEO_PATTERNS = new Set(['gradient', 'fast']); -const VIDEO_QUALITIES = new Set(['low', 'medium', 'high']); -const VIDEO_INPUTS = new Set(['bgra', 'nv12']); -const LIVEKIT_SCREEN_SHARE_SOURCE = 'screen_share'; -const LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE = 'screen_share_audio'; - -function parseBooleanFlag(name, value) { - const normalized = value.trim(); - if (/^(1|true|yes|on|required)$/i.test(normalized)) return true; - if (/^(0|false|no|off|disabled)$/i.test(normalized)) return false; - throw new Error(`${name} must be a boolean flag: 1/0, true/false, yes/no, or on/off`); -} - -function envFlag(...names) { - for (const name of names) { - const value = process.env[name]; - if (value !== undefined && value.trim() !== '') return parseBooleanFlag(name, value); - } - return false; -} - -function envOptionalFlag(name, fallback) { - const value = process.env[name]; - if (value === undefined || value.trim() === '') return fallback; - return parseBooleanFlag(name, value); -} - -function envString(names, fallback) { - for (const name of names) { - const value = process.env[name]; - if (value?.trim()) return value.trim(); - } - return fallback; -} - -function envOptionalString(names) { - for (const name of names) { - const value = process.env[name]; - if (value?.trim()) return value.trim(); - } - return null; -} - -function envOptionalStringConsistent(names) { - let selected = null; - let selectedName = null; - for (const name of names) { - const value = process.env[name]; - if (!value?.trim()) continue; - const trimmed = value.trim(); - if (selected !== null && trimmed !== selected) { - throw new Error(`${name} conflicts with ${selectedName}; use one value for ${names.join(', ')}`); - } - selected = trimmed; - selectedName = name; - } - return selected; -} - -function envInteger(name, fallback) { - const value = process.env[name]; - if (value === undefined || value === '') return fallback; - const parsed = Number(value); - if (!Number.isInteger(parsed) || parsed < 0) { - throw new Error(`${name} must be a non-negative integer`); - } - return parsed; -} - -function envPositiveInteger(name, fallback) { - const parsed = envInteger(name, fallback); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw new Error(`${name} must be a positive integer`); - } - return parsed; -} - -function envNumber(name, fallback) { - const value = process.env[name]; - if (value === undefined || value === '') return fallback; - const parsed = Number(value); - if (!Number.isFinite(parsed) || parsed < 0) { - throw new Error(`${name} must be a non-negative number`); - } - return parsed; -} - -function envPositiveNumber(name, fallback) { - const value = process.env[name]; - if (value === undefined || value === '') return fallback; - const parsed = Number(value); - if (!Number.isFinite(parsed) || parsed <= 0) { - throw new Error(`${name} must be a positive number`); - } - return parsed; -} - -function envUnitNumber(name, fallback) { - const parsed = envPositiveNumber(name, fallback); - if (parsed > 1) { - throw new Error(`${name} must be greater than 0 and less than or equal to 1`); - } - return parsed; -} - -function envOptionalNonNegativeInteger(name) { - const value = process.env[name]; - if (value === undefined || value === '') return null; - const parsed = Number(value); - if (!Number.isInteger(parsed) || parsed < 0) { - throw new Error(`${name} must be a non-negative integer`); - } - return parsed; -} - -function envVideoPattern(name, fallback) { - const value = process.env[name]?.trim().toLowerCase() || fallback; - if (!VIDEO_PATTERNS.has(value)) { - throw new Error(`${name} must be one of: ${[...VIDEO_PATTERNS].join(', ')}`); - } - return value; -} - -function envVideoInput(name, fallback) { - const value = process.env[name]?.trim().toLowerCase() || fallback; - if (!VIDEO_INPUTS.has(value)) { - throw new Error(`${name} must be one of: ${[...VIDEO_INPUTS].join(', ')}`); - } - return value; -} - -function envOptionalVideoQuality(name) { - const value = process.env[name]?.trim().toLowerCase(); - if (!value) return null; - if (!VIDEO_QUALITIES.has(value)) { - throw new Error(`${name} must be one of: ${[...VIDEO_QUALITIES].join(', ')}`); - } - return value; -} - -function jwtSubject(token) { - const parts = token.split('.'); - if (parts.length < 2) { - throw new Error('LiveKit token must be a JWT when identity is not supplied'); - } - let payload; - try { - payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')); - } catch (error) { - throw new Error(`failed to decode LiveKit token payload: ${error.message}`); - } - if (typeof payload.sub !== 'string' || payload.sub.length === 0) { - throw new Error('LiveKit token payload does not contain a subject identity'); - } - return payload.sub; -} - -function resolveIdentity({configuredIdentity, token, fallback}) { - if (configuredIdentity) return configuredIdentity; - if (token) return jwtSubject(token); - return fallback; -} - -function parseCodecList(rawValue, fallback) { - const value = rawValue?.trim() ? rawValue : fallback; - const codecs = value - .split(',') - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0); - if (codecs.length === 0) { - throw new Error('at least one LiveKit screen codec must be configured'); - } - for (const codec of codecs) { - expectedCodecMime(codec); - } - return codecs; -} - -function parseConfig() { - const strict = envFlag('LIVEKIT_HARNESS_STRICT', 'FLUXER_NATIVE_MEDIA_STRICT'); - const liveKitUrl = envString(['LIVEKIT_URL', 'LIVEKIT_WS_URL'], DEFAULT_URL); - const generatedPublisherIdentity = `fluxer-native-publisher-${process.pid}`; - const generatedSecondaryPublisherIdentity = `fluxer-native-publisher-2-${process.pid}`; - const generatedSubscriberIdentity = `fluxer-native-subscriber-${process.pid}`; - const publisherToken = envOptionalString(['LIVEKIT_PUBLISHER_TOKEN']); - const secondaryPublisherToken = envOptionalString(['LIVEKIT_SECONDARY_PUBLISHER_TOKEN']); - const subscriberToken = envOptionalString(['LIVEKIT_SUBSCRIBER_TOKEN']); - const externalTokens = publisherToken !== null || secondaryPublisherToken !== null || subscriberToken !== null; - const width = envPositiveInteger('LIVEKIT_SCREEN_WIDTH', DEFAULT_WIDTH); - const height = envPositiveInteger('LIVEKIT_SCREEN_HEIGHT', DEFAULT_HEIGHT); - if (width % 2 !== 0 || height % 2 !== 0) { - throw new Error('LIVEKIT_SCREEN_WIDTH and LIVEKIT_SCREEN_HEIGHT must be even'); - } - const fps = envPositiveNumber('LIVEKIT_SCREEN_FPS', DEFAULT_FPS); - const minVideoFps = envPositiveNumber('LIVEKIT_MIN_VIDEO_FPS', 15); - if (minVideoFps > fps) { - throw new Error('LIVEKIT_MIN_VIDEO_FPS must be less than or equal to LIVEKIT_SCREEN_FPS'); - } - const screenCodecs = parseCodecList(envOptionalString(['LIVEKIT_SCREEN_CODECS', 'LIVEKIT_SCREEN_CODEC']), 'vp8'); - const configuredExpectedScreenCodecs = envOptionalString([ - 'LIVEKIT_EXPECT_SCREEN_CODECS', - 'LIVEKIT_EXPECT_SCREEN_CODEC', - ]); - const expectedScreenCodecs = parseCodecList(configuredExpectedScreenCodecs, screenCodecs.join(',')); - if (configuredExpectedScreenCodecs !== null && expectedScreenCodecs.length !== screenCodecs.length) { - throw new Error('LIVEKIT_EXPECT_SCREEN_CODECS length must match LIVEKIT_SCREEN_CODECS length'); - } - const secondaryPublisherRequested = envOptionalFlag('LIVEKIT_ENABLE_SECOND_PUBLISHER', strict); - const configuredSecondaryPublisherCodec = envOptionalStringConsistent([ - 'LIVEKIT_SECOND_PUBLISHER_CODEC', - 'LIVEKIT_SECOND_PUBLISHER_SCREEN_CODEC', - 'LIVEKIT_SECOND_PUBLISHER_SCREEN_CODECS', - 'LIVEKIT_SECONDARY_PUBLISHER_SCREEN_CODEC', - 'LIVEKIT_SECONDARY_PUBLISHER_SCREEN_CODECS', - ]); - const configuredExpectedSecondaryPublisherCodec = envOptionalStringConsistent([ - 'LIVEKIT_EXPECT_SECOND_PUBLISHER_SCREEN_CODEC', - 'LIVEKIT_EXPECT_SECOND_PUBLISHER_SCREEN_CODECS', - 'LIVEKIT_EXPECT_SECONDARY_PUBLISHER_SCREEN_CODEC', - 'LIVEKIT_EXPECT_SECONDARY_PUBLISHER_SCREEN_CODECS', - ]); - const secondaryPublisherCodec = - configuredSecondaryPublisherCodec ?? (screenCodecs.length > 1 ? screenCodecs[1] : screenCodecs[0]); - const expectedSecondaryPublisherCodec = configuredExpectedSecondaryPublisherCodec ?? secondaryPublisherCodec; - const screenSimulcast = envOptionalFlag('LIVEKIT_SCREEN_SIMULCAST', true); - const microphone = envOptionalFlag('LIVEKIT_ENABLE_MICROPHONE', true); - const screenAudio = envOptionalFlag('LIVEKIT_ENABLE_SCREEN_AUDIO', strict); - if (externalTokens) { - if (!publisherToken || !subscriberToken) { - throw new Error( - 'LIVEKIT_PUBLISHER_TOKEN and LIVEKIT_SUBSCRIBER_TOKEN are both required when using external tokens', - ); - } - if (secondaryPublisherRequested && !secondaryPublisherToken) { - throw new Error('LIVEKIT_SECONDARY_PUBLISHER_TOKEN is required when LIVEKIT_ENABLE_SECOND_PUBLISHER=1'); - } - } - return { - url: liveKitUrl, - serverApiUrl: envString(['LIVEKIT_API_URL', 'LIVEKIT_HTTP_URL'], liveKitApiUrl(liveKitUrl)), - apiKey: envString(['LIVEKIT_API_KEY'], DEFAULT_API_KEY), - apiSecret: envString(['LIVEKIT_API_SECRET', 'LIVEKIT_SECRET'], DEFAULT_API_SECRET), - room: envString(['LIVEKIT_ROOM'], `fluxer-webrtc-harness-${process.pid}-${randomUUID()}`), - externalTokens, - publisherToken, - secondaryPublisherToken, - subscriberToken, - publisherIdentity: resolveIdentity({ - configuredIdentity: envOptionalString(['LIVEKIT_PUBLISHER_IDENTITY']), - token: publisherToken, - fallback: generatedPublisherIdentity, - }), - secondaryPublisherIdentity: resolveIdentity({ - configuredIdentity: envOptionalString(['LIVEKIT_SECONDARY_PUBLISHER_IDENTITY']), - token: secondaryPublisherToken, - fallback: generatedSecondaryPublisherIdentity, - }), - subscriberIdentity: resolveIdentity({ - configuredIdentity: envOptionalString(['LIVEKIT_SUBSCRIBER_IDENTITY']), - token: subscriberToken, - fallback: generatedSubscriberIdentity, - }), - required: envFlag('FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED', 'LIVEKIT_REQUIRED'), - strict, - timeoutMs: envPositiveInteger('LIVEKIT_HARNESS_TIMEOUT_MS', DEFAULT_TIMEOUT_MS), - connectTimeoutMs: envPositiveInteger('LIVEKIT_CONNECT_TIMEOUT_MS', DEFAULT_CONNECT_TIMEOUT_MS), - disconnectTimeoutMs: envPositiveInteger('LIVEKIT_DISCONNECT_TIMEOUT_MS', DEFAULT_DISCONNECT_TIMEOUT_MS), - probeTimeoutMs: envPositiveInteger('LIVEKIT_PROBE_TIMEOUT_MS', DEFAULT_PROBE_TIMEOUT_MS), - durationMs: envInteger('LIVEKIT_HARNESS_DURATION_MS', strict ? STRICT_DURATION_MS : 0), - width, - height, - expectedWidth: envPositiveInteger('LIVEKIT_EXPECT_SCREEN_WIDTH', width), - expectedHeight: envPositiveInteger('LIVEKIT_EXPECT_SCREEN_HEIGHT', height), - fps, - minFpsRatio: envNumber('LIVEKIT_MIN_RECEIVED_FPS_RATIO', strict ? STRICT_MIN_FPS_RATIO : 0), - maxFrameGapMs: envNumber('LIVEKIT_MAX_FRAME_GAP_MS', strict ? STRICT_MAX_FRAME_GAP_MS : 0), - maxAudioFrameGapMs: envNumber('LIVEKIT_MAX_AUDIO_FRAME_GAP_MS', strict ? STRICT_MAX_AUDIO_FRAME_GAP_MS : 0), - maxAvDriftMs: envNumber('LIVEKIT_MAX_AV_DRIFT_MS', strict ? STRICT_MAX_AV_DRIFT_MS : 0), - maxPacketLoss: envOptionalNonNegativeInteger('LIVEKIT_MAX_PACKET_LOSS'), - requireStableResolution: envOptionalFlag('LIVEKIT_REQUIRE_STABLE_RESOLUTION', strict), - videoPattern: envVideoPattern('LIVEKIT_VIDEO_PATTERN', 'gradient'), - videoInput: envVideoInput('LIVEKIT_VIDEO_INPUT', 'bgra'), - subscriberVideoQuality: envOptionalVideoQuality('LIVEKIT_SUBSCRIBER_VIDEO_QUALITY'), - screenCodecs, - expectedScreenCodecs, - codec: screenCodecs[0], - expectedScreenCodec: expectedCodecMime(expectedScreenCodecs[0] ?? screenCodecs[0]), - secondaryPublisher: secondaryPublisherRequested, - secondaryPublisherCodec, - secondaryPublisherCodecExplicit: configuredSecondaryPublisherCodec !== null, - expectedSecondaryPublisherCodec: expectedCodecMime(expectedSecondaryPublisherCodec), - expectedSecondaryPublisherCodecExplicit: configuredExpectedSecondaryPublisherCodec !== null, - screenSimulcast, - secondaryPublisherScreenSimulcast: envOptionalFlag('LIVEKIT_SECOND_PUBLISHER_SCREEN_SIMULCAST', screenSimulcast), - adaptiveSend: envOptionalFlag('LIVEKIT_ADAPTIVE_SEND', true), - minVideoFps, - minResolutionScale: envUnitNumber('LIVEKIT_MIN_RESOLUTION_SCALE', 0.5), - maxAudioBufferMs: envPositiveInteger('LIVEKIT_MAX_AUDIO_BUFFER_MS', 750), - secondaryPublisherMicrophone: envOptionalFlag('LIVEKIT_SECOND_PUBLISHER_ENABLE_MICROPHONE', microphone), - secondaryPublisherScreenAudio: envOptionalFlag('LIVEKIT_SECOND_PUBLISHER_ENABLE_SCREEN_AUDIO', screenAudio), - maxBitrateBps: envNumber('LIVEKIT_SCREEN_MAX_BITRATE_BPS', 400_000), - sampleRate: envPositiveInteger('LIVEKIT_AUDIO_SAMPLE_RATE', DEFAULT_SAMPLE_RATE), - channels: envPositiveInteger('LIVEKIT_AUDIO_CHANNELS', DEFAULT_CHANNELS), - audioChunkMs: envPositiveInteger('LIVEKIT_AUDIO_CHUNK_MS', DEFAULT_AUDIO_CHUNK_MS), - microphone, - screenAudio, - dataPacket: envOptionalFlag('LIVEKIT_ENABLE_DATA_PACKET', strict), - subscriptionCycle: envOptionalFlag('LIVEKIT_ENABLE_SUBSCRIPTION_CYCLE', strict), - validateServerPublishing: envOptionalFlag('LIVEKIT_VALIDATE_SERVER_PUBLISHING', strict), - camera: envOptionalFlag('LIVEKIT_ENABLE_CAMERA', false), - cameraDeviceId: envOptionalString(['LIVEKIT_CAMERA_DEVICE_ID']), - expectedCameraCodec: expectedCodecMime(envString(['LIVEKIT_EXPECT_CAMERA_CODEC'], '')), - e2eeKey: envOptionalString(['LIVEKIT_E2EE_KEY']), - expectedHardwareEncoder: envOptionalString(['LIVEKIT_EXPECT_HARDWARE_ENCODER']), - reportPath: envOptionalString(['LIVEKIT_HARNESS_REPORT_PATH', 'FLUXER_NATIVE_MEDIA_REPORT']), - verbose: envFlag('FLUXER_WEBRTC_SENDER_LIVEKIT_VERBOSE', 'LIVEKIT_VERBOSE'), - }; -} - -function expectedCodecMime(codec) { - switch (codec.trim().toLowerCase()) { - case '': - return null; - case 'vp8': - return 'video/VP8'; - case 'vp9': - return 'video/VP9'; - case 'h264': - return 'video/H264'; - case 'av1': - return 'video/AV1'; - case 'h265': - case 'hevc': - return 'video/H265'; - default: - throw new Error(`unsupported LIVEKIT_EXPECT_SCREEN_CODEC: ${codec}`); - } -} - -function liveKitTcpTarget(rawUrl) { - let url; - try { - url = new URL(rawUrl); - } catch (error) { - throw new Error(`LIVEKIT_URL is not a valid URL: ${error.message}`); - } - if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { - throw new Error('LIVEKIT_URL must use ws:// or wss://'); - } - return { - host: url.hostname.replace(/^\[(.*)\]$/, '$1'), - port: Number(url.port || (url.protocol === 'wss:' ? 443 : 80)), - }; -} - -function liveKitApiUrl(rawUrl) { - let url; - try { - url = new URL(rawUrl); - } catch (error) { - throw new Error(`LIVEKIT_URL is not a valid URL: ${error.message}`); - } - if (url.protocol === 'ws:') return `http://${url.host}`; - if (url.protocol === 'wss:') return `https://${url.host}`; - throw new Error('LIVEKIT_URL must use ws:// or wss://'); -} - -function probeTcp({host, port}, timeoutMs) { - return new Promise((resolve) => { - const socket = net.createConnection({host, port}); - let done = false; - const finish = (result) => { - if (done) return; - done = true; - socket.destroy(); - resolve(result); - }; - socket.setTimeout(timeoutMs); - socket.once('connect', () => finish({ok: true})); - socket.once('timeout', () => finish({ok: false, reason: `timed out after ${timeoutMs}ms`})); - socket.once('error', (error) => finish({ok: false, reason: error.message})); - }); -} - -function base64urlJson(value) { - return Buffer.from(JSON.stringify(value)).toString('base64url'); -} - -function createLiveKitToken({apiKey, apiSecret, room, identity, name}) { - const now = Math.floor(Date.now() / 1000); - const header = {alg: 'HS256', typ: 'JWT'}; - const payload = { - iss: apiKey, - sub: identity, - name, - nbf: now - 5, - exp: now + 600, - jti: randomUUID(), - video: { - roomJoin: true, - room, - canPublish: true, - canSubscribe: true, - canPublishData: true, - canPublishSources: ['camera', 'microphone', 'screen_share', 'screen_share_audio'], - }, - }; - const body = `${base64urlJson(header)}.${base64urlJson(payload)}`; - const signature = createHmac('sha256', apiSecret).update(body).digest('base64url'); - return `${body}.${signature}`; -} - -function createLiveKitAdminToken({apiKey, apiSecret, room}) { - const now = Math.floor(Date.now() / 1000); - const header = {alg: 'HS256', typ: 'JWT'}; - const payload = { - iss: apiKey, - sub: `fluxer-native-harness-admin-${process.pid}`, - nbf: now - 5, - exp: now + 600, - jti: randomUUID(), - video: { - room, - roomAdmin: true, - }, - }; - const body = `${base64urlJson(header)}.${base64urlJson(payload)}`; - const signature = createHmac('sha256', apiSecret).update(body).digest('base64url'); - return `${body}.${signature}`; -} - -function parsePayload(jsonPayload) { - try { - return JSON.parse(jsonPayload); - } catch { - return {}; - } -} - -function unpackNapiPair(args) { - if (args.length === 1 && Array.isArray(args[0])) { - return [args[0][0], args[0][1]]; - } - return [args[0], args[1]]; -} - -function createEngineState(name, verbose) { - return { - name, - events: [], - stats: null, - statsSamples: [], - trackIndex: new Map(), - videoCallbacks: 0, - videoBytes: 0, - videoFrameTimes: [], - videoFrameRecords: [], - maxVideoFrameGapMs: 0, - lastVideoMeta: null, - videoResolutionCounts: new Map(), - lastVideoFrameAtMs: null, - audioFrameCounts: [], - audioFrameTimes: [], - lastAudioFrameAtMs: null, - maxAudioFrameGapMs: 0, - recordEvent(eventType, jsonPayload) { - const payload = parsePayload(jsonPayload); - this.events.push({eventType, payload, atMs: Date.now()}); - if (typeof payload.trackSid === 'string' && payload.trackSid.length > 0) { - if (eventType === 'trackSubscribed') { - this.trackIndex.set(payload.trackSid, { - identity: payload.identity, - kind: normalizeTrackKind(payload.kind), - source: normalizeTrackSource(payload.source), - trackName: payload.trackName, - }); - } - if (eventType === 'trackUnsubscribed') { - this.trackIndex.delete(payload.trackSid); - } - } - if (eventType === 'stats') { - this.stats = payload; - this.statsSamples.push({payload, atMs: Date.now()}); - } - if (verbose) { - const suffix = eventType === 'stats' ? '' : ` ${JSON.stringify(payload)}`; - console.log(`[livekit-harness] ${name} event: ${eventType}${suffix}`); - } - }, - recordVideoFrame(metaJson, data) { - const now = Date.now(); - if (this.lastVideoFrameAtMs !== null) { - this.maxVideoFrameGapMs = Math.max(this.maxVideoFrameGapMs, now - this.lastVideoFrameAtMs); - } - this.lastVideoFrameAtMs = now; - this.videoFrameTimes.push(now); - this.videoCallbacks += 1; - this.videoBytes += data?.byteLength ?? data?.length ?? 0; - const parsedMeta = parsePayload(metaJson); - const indexedTrack = - typeof parsedMeta.trackSid === 'string' ? this.trackIndex.get(parsedMeta.trackSid) : undefined; - this.lastVideoMeta = { - ...parsedMeta, - identity: parsedMeta.identity ?? indexedTrack?.identity, - kind: parsedMeta.kind !== undefined ? normalizeTrackKind(parsedMeta.kind) : indexedTrack?.kind, - source: parsedMeta.source !== undefined ? normalizeTrackSource(parsedMeta.source) : indexedTrack?.source, - trackName: parsedMeta.trackName ?? indexedTrack?.trackName, - }; - if (Number.isFinite(this.lastVideoMeta.width) && Number.isFinite(this.lastVideoMeta.height)) { - const key = `${this.lastVideoMeta.width}x${this.lastVideoMeta.height}`; - this.videoResolutionCounts.set(key, (this.videoResolutionCounts.get(key) ?? 0) + 1); - this.videoFrameRecords.push({ - atMs: now, - identity: this.lastVideoMeta.identity, - kind: this.lastVideoMeta.kind, - source: this.lastVideoMeta.source, - trackName: this.lastVideoMeta.trackName, - trackSid: this.lastVideoMeta.trackSid, - width: this.lastVideoMeta.width, - height: this.lastVideoMeta.height, - }); - } - if (verbose && this.videoCallbacks === 1) { - console.log(`[livekit-harness] ${name} first video frame: ${metaJson}`); - } - }, - recordAudioFrameCount(count) { - const now = Date.now(); - const last = this.audioFrameCounts[this.audioFrameCounts.length - 1]; - if (last && count > last.count) { - if (this.lastAudioFrameAtMs !== null) { - this.maxAudioFrameGapMs = Math.max(this.maxAudioFrameGapMs, now - this.lastAudioFrameAtMs); - } - this.lastAudioFrameAtMs = now; - this.audioFrameTimes.push(now); - } - this.audioFrameCounts.push({count, atMs: now}); - }, - }; -} - -function hasEvent(state, eventType, predicate = () => true) { - return state.events.some((event) => event.eventType === eventType && predicate(event.payload)); -} - -function statsHasOutbound(stats, kind, source) { - return ( - Array.isArray(stats?.outbound) && - stats.outbound.some((entry) => entry.kind === kind && normalizeTrackSource(entry.source) === source) - ); -} - -function statsHasInbound(stats, kind) { - return Array.isArray(stats?.inbound) && stats.inbound.some((entry) => entry.kind === kind); -} - -function statsInboundCount(stats, kind) { - return Array.isArray(stats?.inbound) ? stats.inbound.filter((entry) => entry.kind === kind).length : 0; -} - -function totalPacketsLost(stats) { - const outboundLoss = Array.isArray(stats?.outbound) - ? stats.outbound.reduce((sum, entry) => sum + Math.max(0, Number(entry.packetsLost) || 0), 0) - : 0; - const inboundLoss = Array.isArray(stats?.inbound) - ? stats.inbound.reduce((sum, entry) => sum + Math.max(0, Number(entry.packetsLost) || 0), 0) - : 0; - return outboundLoss + inboundLoss; -} - -function maxObservedPacketsLost(...states) { - let max = 0; - for (const state of states) { - for (const sample of state?.statsSamples ?? []) { - max = Math.max(max, totalPacketsLost(sample.payload)); - } - max = Math.max(max, totalPacketsLost(state?.stats)); - } - return max; -} - -function maxObservedPacketsLostDelta(startedAtMs, endedAtMs, ...states) { - let maxDelta = 0; - for (const state of states) { - const samples = [...(state?.statsSamples ?? [])]; - if (state?.stats) samples.push({payload: state.stats, atMs: endedAtMs}); - samples.sort((a, b) => a.atMs - b.atMs); - let baseline = 0; - let maxDuring = 0; - let hasWindowSample = false; - for (const sample of samples) { - const total = totalPacketsLost(sample.payload); - if (sample.atMs <= startedAtMs) { - baseline = total; - continue; - } - if (sample.atMs <= endedAtMs) { - hasWindowSample = true; - maxDuring = Math.max(maxDuring, total); - } - } - if (hasWindowSample) maxDelta = Math.max(maxDelta, Math.max(0, maxDuring - baseline)); - } - return maxDelta; -} - -function statsHasOutboundCodec(stats, kind, source, expectedCodec) { - return ( - Array.isArray(stats?.outbound) && - stats.outbound.some( - (entry) => entry.kind === kind && normalizeTrackSource(entry.source) === source && entry.codec === expectedCodec, - ) - ); -} - -function hasTrackEvent(state, eventType, {identity, kind, source}) { - if (!state) return false; - return hasEvent( - state, - eventType, - (payload) => - (identity === undefined || payload.identity === identity) && - (kind === undefined || payload.kind === kind) && - (source === undefined || normalizeTrackSource(payload.source) === source), - ); -} - -function statsHasInboundCodec(stats, kind, expectedCodec) { - return ( - Array.isArray(stats?.inbound) && stats.inbound.some((entry) => entry.kind === kind && entry.codec === expectedCodec) - ); -} - -function startVideoPump(_engine, config, _pumpErrors) { - const stats = { - input: config.videoInput, - pattern: config.videoPattern, - targetFps: config.fps, - startedAtMs: Date.now(), - stoppedAtMs: null, - framesPushed: 0, - framesRejected: 0, - errors: 0, - maxPushGapMs: 0, - lastPushAtMs: null, - }; - let stopped = false; - return { - stats() { - const endedAtMs = stats.stoppedAtMs ?? Date.now(); - const elapsedMs = Math.max(1, endedAtMs - stats.startedAtMs); - return { - ...stats, - elapsedMs, - producedFps: Math.round((stats.framesPushed / (elapsedMs / 1000)) * 100) / 100, - }; - }, - stop() { - if (stopped) return; - stopped = true; - stats.stoppedAtMs = Date.now(); - }, - }; -} - -function syntheticPcmFrame({sampleRate, channels, chunkMs, cursor}) { - const samplesPerChannel = Math.round((sampleRate * chunkMs) / 1000); - const buffer = Buffer.alloc(samplesPerChannel * channels * 2); - const frequencyHz = 440; - for (let i = 0; i < samplesPerChannel; i += 1) { - const sample = Math.round(Math.sin(((cursor + i) / sampleRate) * frequencyHz * Math.PI * 2) * 10_000); - for (let channel = 0; channel < channels; channel += 1) { - buffer.writeInt16LE(sample, (i * channels + channel) * 2); - } - } - return {buffer, nextCursor: cursor + samplesPerChannel}; -} - -function startAudioPump(label, pushFrame, config, pumpErrors) { - let stopped = false; - let cursor = 0; - const promise = (async () => { - while (!stopped) { - const frame = syntheticPcmFrame({ - sampleRate: config.sampleRate, - channels: config.channels, - chunkMs: config.audioChunkMs, - cursor, - }); - cursor = frame.nextCursor; - try { - const ok = await pushFrame(frame.buffer, config.sampleRate, config.channels); - if (!ok) pumpErrors.push(new Error(`${label} returned false`)); - } catch (error) { - pumpErrors.push(error); - } - await delay(config.audioChunkMs); - } - })(); - return { - stop() { - stopped = true; - }, - async done() { - await promise; - }, - }; -} - -function startAudioSampler(engine, state) { - const timer = setInterval(() => { - try { - state.recordAudioFrameCount(engine.inboundAudioFrames()); - } catch {} - }, AUDIO_POLL_INTERVAL_MS); - timer.unref?.(); - return { - stop() { - clearInterval(timer); - }, - }; -} - -function receivedFpsFromTimes(frames) { - if (frames.length < 2) return 0; - const elapsedSec = Math.max(0.001, (frames[frames.length - 1] - frames[0]) / 1000); - return (frames.length - 1) / elapsedSec; -} - -function maxGapMs(times, startedAtMs, endedAtMs) { - const windowTimes = times.filter((time) => time >= startedAtMs && time <= endedAtMs); - let maxGap = 0; - for (let index = 1; index < windowTimes.length; index += 1) { - maxGap = Math.max(maxGap, windowTimes[index] - windowTimes[index - 1]); - } - return maxGap; -} - -function countWindowSamples(times, startedAtMs, endedAtMs) { - return times.filter((time) => time >= startedAtMs && time <= endedAtMs).length; -} - -function primaryScreenVideoSelector(config) { - return { - identity: config.publisherIdentity, - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - }; -} - -function frameRecordMatches(record, selector) { - if (!record) return false; - if (selector.kind !== undefined && record.kind !== undefined && record.kind !== selector.kind) return false; - if (selector.source !== undefined && record.source !== undefined && record.source !== selector.source) return false; - if (selector.identity !== undefined && record.identity !== undefined && record.identity !== selector.identity) { - return false; - } - return true; -} - -function selectedVideoFrameRecords(state, startedAtMs, endedAtMs, selector) { - const records = []; - for (const record of state?.videoFrameRecords ?? []) { - if (record.atMs < startedAtMs || record.atMs > endedAtMs) continue; - if (!frameRecordMatches(record, selector)) continue; - records.push(record); - } - return records; -} - -function selectedVideoFrameTimes(state, startedAtMs, endedAtMs, selector) { - return selectedVideoFrameRecords(state, startedAtMs, endedAtMs, selector).map((record) => record.atMs); -} - -function lastVideoFrameRecord(state, startedAtMs, endedAtMs, selector) { - return selectedVideoFrameRecords(state, startedAtMs, endedAtMs, selector).at(-1) ?? null; -} - -function videoResolutionCounts(state, startedAtMs, endedAtMs, selector) { - const counts = new Map(); - for (const record of selectedVideoFrameRecords(state, startedAtMs, endedAtMs, selector)) { - const key = `${record.width}x${record.height}`; - counts.set(key, (counts.get(key) ?? 0) + 1); - } - return Object.fromEntries(counts); -} - -function videoResolutionMismatchCount(state, expectedWidth, expectedHeight, startedAtMs, endedAtMs, selector) { - let mismatches = 0; - for (const record of selectedVideoFrameRecords(state, startedAtMs, endedAtMs, selector)) { - if (record.width !== expectedWidth || record.height !== expectedHeight) { - mismatches += 1; - } - } - return mismatches; -} - -function currentAvDriftMs(state, startedAtMs, endedAtMs, selector) { - const videoTimes = selectedVideoFrameTimes(state, startedAtMs, endedAtMs, selector); - const audioTimes = (state?.audioFrameTimes ?? []).filter((time) => time >= startedAtMs && time <= endedAtMs); - const lastVideoFrameAtMs = videoTimes.at(-1) ?? null; - const lastAudioFrameAtMs = audioTimes.at(-1) ?? null; - if (lastVideoFrameAtMs === null || lastAudioFrameAtMs === null) return null; - return Math.abs(lastVideoFrameAtMs - lastAudioFrameAtMs); -} - -function expectedInboundVideoTrackCount(config) { - let count = 1; - if (config.camera) count += 1; - if (config.secondaryPublisher) count += 1; - return count; -} - -function expectedInboundAudioTrackCount(config) { - let count = 0; - if (config.microphone) count += 1; - if (config.screenAudio) count += 1; - if (config.secondaryPublisher && config.secondaryPublisherMicrophone) count += 1; - if (config.secondaryPublisher && config.secondaryPublisherScreenAudio) count += 1; - return count; -} - -function normalizeTrackKind(kind) { - switch (String(kind ?? '').toLowerCase()) { - case '0': - case 'audio': - return 'audio'; - case '1': - case 'video': - return 'video'; - case '2': - case 'data': - return 'data'; - default: - return String(kind ?? '').toLowerCase(); - } -} - -function normalizeTrackSource(source) { - switch (String(source ?? '').toLowerCase()) { - case '1': - case 'camera': - case 'camera_source': - return 'camera'; - case '2': - case 'microphone': - case 'mic': - return 'microphone'; - case '3': - case 'screen_share': - case 'screenshare': - case 'screen-share': - return LIVEKIT_SCREEN_SHARE_SOURCE; - case '4': - case 'screen_share_audio': - case 'screenshareaudio': - case 'screen-share-audio': - return LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE; - default: - return String(source ?? ''); - } -} - -function findServerParticipant(serverState, identity) { - return serverState?.participants?.find((participant) => participant.identity === identity) ?? null; -} - -function normalizeMimeType(mimeType) { - if (typeof mimeType !== 'string' || mimeType.trim() === '') return null; - return mimeType.trim().toLowerCase(); -} - -function serverTrackMatches(track, {kind, source, mimeType}) { - if (normalizeTrackKind(track.type ?? track.kind) !== kind) return false; - if (normalizeTrackSource(track.source) !== source) return false; - if (track.muted !== false) return false; - const expectedMimeType = normalizeMimeType(mimeType); - if (expectedMimeType !== null && normalizeMimeType(track.mimeType ?? track.mime_type) !== expectedMimeType) { - return false; - } - return true; -} - -function serverParticipantHasTrack(serverState, identity, expectedTrack) { - const participant = findServerParticipant(serverState, identity); - if (!participant) return false; - const tracks = participant.tracks ?? []; - return tracks.some((track) => serverTrackMatches(track, expectedTrack)); -} - -function expectedServerTracks(config, publisherIdentity, secondaryPublisherIdentity) { - const tracks = [ - { - identity: publisherIdentity, - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - mimeType: config.expectedScreenCodec, - label: 'publisher screenshare', - }, - ]; - if (config.microphone) { - tracks.push({identity: publisherIdentity, kind: 'audio', source: 'microphone', label: 'publisher microphone'}); - } - if (config.screenAudio) { - tracks.push({ - identity: publisherIdentity, - kind: 'audio', - source: LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE, - label: 'publisher screen-share audio', - }); - } - if (config.camera) { - tracks.push({ - identity: publisherIdentity, - kind: 'video', - source: 'camera', - mimeType: config.expectedCameraCodec, - label: 'publisher camera', - }); - } - if (config.secondaryPublisher) { - tracks.push({ - identity: secondaryPublisherIdentity, - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - mimeType: config.expectedSecondaryPublisherCodec, - label: 'secondary publisher screenshare', - }); - if (config.secondaryPublisherMicrophone) { - tracks.push({ - identity: secondaryPublisherIdentity, - kind: 'audio', - source: 'microphone', - label: 'secondary publisher microphone', - }); - } - if (config.secondaryPublisherScreenAudio) { - tracks.push({ - identity: secondaryPublisherIdentity, - kind: 'audio', - source: LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE, - label: 'secondary publisher screen-share audio', - }); - } - } - return tracks; -} - -function serverPublishedTrackChecks({config, serverState, publisherIdentity, secondaryPublisherIdentity}) { - if (!config.validateServerPublishing) return []; - const checks = [ - { - name: 'server publishing API participants listed', - pass: Array.isArray(serverState?.participants) && !serverState?.error, - }, - ]; - for (const track of expectedServerTracks(config, publisherIdentity, secondaryPublisherIdentity)) { - checks.push({ - name: `server sees ${track.label} publication`, - pass: serverParticipantHasTrack(serverState, track.identity, { - kind: track.kind, - source: track.source, - mimeType: track.mimeType, - }), - }); - } - return checks; -} - -function currentChecks({ - publisher, - subscriber, - publisherState, - subscriberState, - config, - publisherIdentity, - secondaryPublisher, - secondaryPublisherState, - secondaryPublisherIdentity, - serverState, -}) { - const expectedVideoTracks = expectedInboundVideoTrackCount(config); - const expectedAudioTracks = expectedInboundAudioTrackCount(config); - const screenVideoMeta = lastVideoFrameRecord( - subscriberState, - Number.NEGATIVE_INFINITY, - Number.POSITIVE_INFINITY, - primaryScreenVideoSelector(config), - ); - const checks = [ - {name: 'publisher connected', pass: publisher.isConnected()}, - {name: 'subscriber connected', pass: subscriber.isConnected()}, - { - name: 'publisher local screenshare publication', - pass: hasTrackEvent(publisherState, 'localTrackPublished', { - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - }), - }, - { - name: 'subscriber remote screenshare subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: publisherIdentity, - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - }), - }, - { - name: `subscriber inbound video frames >= ${MIN_INBOUND_VIDEO_FRAMES * expectedVideoTracks}`, - pass: subscriber.inboundVideoFrames() >= MIN_INBOUND_VIDEO_FRAMES * expectedVideoTracks, - }, - { - name: 'subscriber video frame callback', - pass: subscriberState.videoCallbacks > 0 && subscriberState.videoBytes > 0, - }, - { - name: 'publisher outbound screenshare stats', - pass: statsHasOutbound(publisherState.stats, 'video', LIVEKIT_SCREEN_SHARE_SOURCE), - }, - { - name: 'subscriber inbound video stats', - pass: statsHasInbound(subscriberState.stats, 'video'), - }, - { - name: `subscriber inbound video stats count >= ${expectedVideoTracks}`, - pass: statsInboundCount(subscriberState.stats, 'video') >= expectedVideoTracks, - }, - { - name: `subscriber video resolution ${config.expectedWidth}x${config.expectedHeight}`, - pass: screenVideoMeta?.width === config.expectedWidth && screenVideoMeta?.height === config.expectedHeight, - }, - ]; - if (expectedAudioTracks > 0) { - checks.push( - { - name: `subscriber aggregate inbound audio frames >= ${MIN_INBOUND_AUDIO_FRAMES * expectedAudioTracks}`, - pass: subscriber.inboundAudioFrames() >= MIN_INBOUND_AUDIO_FRAMES * expectedAudioTracks, - }, - { - name: `subscriber inbound audio stats count >= ${expectedAudioTracks}`, - pass: statsInboundCount(subscriberState.stats, 'audio') >= expectedAudioTracks, - }, - ); - } - if (config.microphone) { - checks.push( - { - name: 'publisher local microphone publication', - pass: hasTrackEvent(publisherState, 'localTrackPublished', {kind: 'audio', source: 'microphone'}), - }, - { - name: 'subscriber remote microphone subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: publisherIdentity, - kind: 'audio', - source: 'microphone', - }), - }, - { - name: `subscriber inbound audio frames >= ${MIN_INBOUND_AUDIO_FRAMES * Math.max(1, expectedAudioTracks)}`, - pass: subscriber.inboundAudioFrames() >= MIN_INBOUND_AUDIO_FRAMES * Math.max(1, expectedAudioTracks), - }, - { - name: 'publisher outbound microphone stats', - pass: statsHasOutbound(publisherState.stats, 'audio', 'microphone'), - }, - { - name: 'subscriber inbound audio stats', - pass: statsHasInbound(subscriberState.stats, 'audio'), - }, - ); - } - if (config.screenAudio) { - checks.push( - { - name: 'publisher local screen-share audio publication', - pass: hasTrackEvent(publisherState, 'localTrackPublished', { - kind: 'audio', - source: LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE, - }), - }, - { - name: 'subscriber remote screen-share audio subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: publisherIdentity, - kind: 'audio', - source: LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE, - }), - }, - { - name: 'publisher outbound screen-share audio stats', - pass: statsHasOutbound(publisherState.stats, 'audio', LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE), - }, - ); - } - if (config.camera) { - checks.push( - { - name: 'publisher local camera publication', - pass: hasTrackEvent(publisherState, 'localTrackPublished', {kind: 'video', source: 'camera'}), - }, - { - name: 'subscriber remote camera subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: publisherIdentity, - kind: 'video', - source: 'camera', - }), - }, - { - name: 'publisher outbound camera stats', - pass: statsHasOutbound(publisherState.stats, 'video', 'camera'), - }, - ); - if (config.expectedCameraCodec) { - checks.push({ - name: `publisher outbound camera codec ${config.expectedCameraCodec}`, - pass: statsHasOutboundCodec(publisherState.stats, 'video', 'camera', config.expectedCameraCodec), - }); - } - } - if (config.secondaryPublisher) { - checks.push( - {name: 'secondary publisher connected', pass: secondaryPublisher?.isConnected() === true}, - { - name: 'secondary publisher local screenshare publication', - pass: hasTrackEvent(secondaryPublisherState, 'localTrackPublished', { - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - }), - }, - { - name: 'subscriber remote secondary screenshare subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: secondaryPublisherIdentity, - kind: 'video', - source: LIVEKIT_SCREEN_SHARE_SOURCE, - }), - }, - { - name: 'secondary publisher outbound screenshare stats', - pass: statsHasOutbound(secondaryPublisherState?.stats, 'video', LIVEKIT_SCREEN_SHARE_SOURCE), - }, - ); - if (config.expectedSecondaryPublisherCodec) { - checks.push( - { - name: `secondary publisher outbound screenshare codec ${config.expectedSecondaryPublisherCodec}`, - pass: statsHasOutboundCodec( - secondaryPublisherState?.stats, - 'video', - LIVEKIT_SCREEN_SHARE_SOURCE, - config.expectedSecondaryPublisherCodec, - ), - }, - { - name: `subscriber inbound secondary codec ${config.expectedSecondaryPublisherCodec}`, - pass: statsHasInboundCodec(subscriberState.stats, 'video', config.expectedSecondaryPublisherCodec), - }, - ); - } - if (config.secondaryPublisherMicrophone) { - checks.push( - { - name: 'secondary publisher local microphone publication', - pass: hasTrackEvent(secondaryPublisherState, 'localTrackPublished', { - kind: 'audio', - source: 'microphone', - }), - }, - { - name: 'subscriber remote secondary microphone subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: secondaryPublisherIdentity, - kind: 'audio', - source: 'microphone', - }), - }, - { - name: 'secondary publisher outbound microphone stats', - pass: statsHasOutbound(secondaryPublisherState?.stats, 'audio', 'microphone'), - }, - ); - } - if (config.secondaryPublisherScreenAudio) { - checks.push( - { - name: 'secondary publisher local screen-share audio publication', - pass: hasTrackEvent(secondaryPublisherState, 'localTrackPublished', { - kind: 'audio', - source: LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE, - }), - }, - { - name: 'subscriber remote secondary screen-share audio subscription', - pass: hasTrackEvent(subscriberState, 'trackSubscribed', { - identity: secondaryPublisherIdentity, - kind: 'audio', - source: LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE, - }), - }, - { - name: 'secondary publisher outbound screen-share audio stats', - pass: statsHasOutbound(secondaryPublisherState?.stats, 'audio', LIVEKIT_SCREEN_SHARE_AUDIO_SOURCE), - }, - ); - } - } - if (config.dataPacket) { - checks.push({ - name: 'subscriber data packet received', - pass: hasEvent(subscriberState, 'dataReceived', (payload) => payload.topic === 'native-media-harness'), - }); - if (config.secondaryPublisher) { - checks.push({ - name: 'subscriber secondary data packet received', - pass: hasEvent( - subscriberState, - 'dataReceived', - (payload) => payload.topic === 'native-media-harness-secondary', - ), - }); - } - } - if (config.expectedScreenCodec) { - checks.push( - { - name: `publisher outbound screenshare codec ${config.expectedScreenCodec}`, - pass: statsHasOutboundCodec( - publisherState.stats, - 'video', - LIVEKIT_SCREEN_SHARE_SOURCE, - config.expectedScreenCodec, - ), - }, - { - name: `subscriber inbound video codec ${config.expectedScreenCodec}`, - pass: statsHasInboundCodec(subscriberState.stats, 'video', config.expectedScreenCodec), - }, - ); - } - if (config.expectedHardwareEncoder) { - const capabilities = getHardwareEncoderCapabilities(); - checks.push({ - name: `hardware encoder backend ${config.expectedHardwareEncoder}`, - pass: - capabilities.available === true && - String(capabilities.backend).toLowerCase() === config.expectedHardwareEncoder.toLowerCase(), - }); - } - checks.push(...serverPublishedTrackChecks({config, serverState, publisherIdentity, secondaryPublisherIdentity})); - return checks; -} - -async function waitForHarnessSuccess(context, pumpErrors, timeoutMs) { - const start = Date.now(); - let checks = currentChecks(context); - while (Date.now() - start < timeoutMs) { - if (pumpErrors.length > 0) { - throw pumpErrors[0]; - } - await enforceSubscriberVideoQuality(context); - await refreshServerPublishingState(context); - checks = currentChecks(context); - if (checks.every((check) => check.pass)) return checks; - await delay(250); - } - const missing = checks.filter((check) => !check.pass).map((check) => check.name); - throw new Error(`timed out after ${timeoutMs}ms waiting for: ${missing.join(', ')}`); -} - -async function holdStrictDuration(context, pumpErrors, durationMs) { - const startedAtMs = Date.now(); - context.strictStartedAtMs = startedAtMs; - if (durationMs <= 0) { - context.strictEndedAtMs = startedAtMs; - return; - } - const endAtMs = startedAtMs + durationMs; - while (Date.now() < endAtMs) { - if (pumpErrors.length > 0) throw pumpErrors[0]; - await delay(1000); - } - context.strictEndedAtMs = Date.now(); -} - -async function withTimeout(promise, timeoutMs, label) { - const timeout = delay(timeoutMs).then(() => { - throw new Error(`${label} timed out after ${timeoutMs}ms`); - }); - return Promise.race([promise, timeout]); -} - -async function disconnectEngine(engine, label, timeoutMs) { - if (!engine) return; - try { - await withTimeout(engine.disconnect(), timeoutMs, `${label} disconnect`); - } catch (error) { - console.warn(`[livekit-harness] ${label} disconnect failed: ${error.message}`); - } -} - -async function refreshServerPublishingState(context) { - const {config, serverState} = context; - if (!config.validateServerPublishing) return; - const now = Date.now(); - if (serverState.inFlight || now - serverState.lastAttemptAtMs < 500) return; - serverState.inFlight = true; - serverState.lastAttemptAtMs = now; - try { - serverState.participants = await listServerParticipants(config); - serverState.error = null; - serverState.fetchedAtMs = Date.now(); - } catch (error) { - serverState.error = String(error.message ?? error); - } finally { - serverState.inFlight = false; - } -} - -async function listServerParticipants(config) { - const token = createLiveKitAdminToken({ - apiKey: config.apiKey, - apiSecret: config.apiSecret, - room: config.room, - }); - const response = await fetch(`${config.serverApiUrl}/twirp/livekit.RoomService/ListParticipants`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({room: config.room}), - }); - const bodyText = await response.text(); - if (!response.ok) { - throw new Error(`RoomService.ListParticipants failed with HTTP ${response.status}: ${bodyText.slice(0, 240)}`); - } - let body; - try { - body = bodyText ? JSON.parse(bodyText) : {}; - } catch (error) { - throw new Error(`RoomService.ListParticipants returned invalid JSON: ${error.message}`); - } - if (!Array.isArray(body.participants)) { - throw new Error('RoomService.ListParticipants response did not contain participants'); - } - return body.participants; -} - -async function runSubscriptionCycle(subscriber, publisherIdentity) { - await subscriber.setRemoteTrackSubscription({ - participantIdentity: publisherIdentity, - source: LIVEKIT_SCREEN_SHARE_SOURCE, - subscribed: true, - enabled: false, - quality: 'low', - }); - await delay(500); - await subscriber.setRemoteTrackSubscription({ - participantIdentity: publisherIdentity, - source: LIVEKIT_SCREEN_SHARE_SOURCE, - subscribed: true, - enabled: true, - quality: 'high', - }); -} - -async function setSubscriberVideoQuality(subscriber, publisherIdentity, quality) { - if (!quality) return; - await subscriber.setRemoteTrackSubscription({ - participantIdentity: publisherIdentity, - source: LIVEKIT_SCREEN_SHARE_SOURCE, - subscribed: true, - enabled: true, - quality, - }); -} - -async function enforceSubscriberVideoQuality(context) { - await setSubscriberVideoQuality(context.subscriber, context.publisherIdentity, context.config.subscriberVideoQuality); - if (context.secondaryPublisher) { - await setSubscriberVideoQuality( - context.subscriber, - context.secondaryPublisherIdentity, - context.config.subscriberVideoQuality, - ); - } -} - -function sanitizeConfig(config) { - const { - apiSecret: _apiSecret, - e2eeKey: _e2eeKey, - publisherToken, - secondaryPublisherToken, - subscriberToken, - ...safe - } = config; - return { - ...safe, - url: sanitizeReportUrl(config.url), - serverApiUrl: sanitizeReportUrl(config.serverApiUrl), - apiSecret: '', - e2eeKey: config.e2eeKey ? '' : null, - publisherToken: publisherToken ? '' : null, - secondaryPublisherToken: secondaryPublisherToken ? '' : null, - subscriberToken: subscriberToken ? '' : null, - }; -} - -function summarizeServerPublishing(serverState) { - if (!serverState) return null; - return { - fetchedAtMs: serverState.fetchedAtMs, - error: serverState.error, - participants: (serverState.participants ?? []).map((participant) => ({ - identity: participant.identity, - trackCount: participant.tracks?.length ?? 0, - tracks: (participant.tracks ?? []).map((track) => ({ - sid: track.sid ?? null, - name: track.name ?? null, - kind: normalizeTrackKind(track.type ?? track.kind), - source: normalizeTrackSource(track.source), - mimeType: track.mimeType ?? track.mime_type ?? null, - muted: track.muted ?? null, - })), - })), - }; -} - -function sanitizeReportUrl(rawUrl) { - if (typeof rawUrl !== 'string') return rawUrl; - try { - const url = new URL(rawUrl); - if (!url.username && !url.password && !url.search && !url.hash) return rawUrl; - url.username = ''; - url.password = ''; - url.search = ''; - url.hash = ''; - return url.toString(); - } catch { - return rawUrl.replace(/\/\/[^/@\s]+@/g, '//@').replace(/[?][^\s#]*/g, '?'); - } -} - -function scenarioRoomName(room, scenarioName, scenarioCount, externalTokens) { - if (scenarioCount <= 1 || externalTokens) return room; - const suffix = scenarioName.replace(/[^a-z0-9_.-]/gi, '-').replace(/-+/g, '-'); - return `${room}-${suffix}`.slice(0, 128); -} - -function buildScenarioConfigs(baseConfig) { - const scenarioCount = baseConfig.screenCodecs.length; - return baseConfig.screenCodecs.map((codec, index) => { - const expectedCodec = baseConfig.expectedScreenCodecs[index] ?? codec; - const secondaryCodec = baseConfig.secondaryPublisherCodecExplicit - ? baseConfig.secondaryPublisherCodec - : (baseConfig.screenCodecs[(index + 1) % baseConfig.screenCodecs.length] ?? codec); - const expectedSecondaryCodec = baseConfig.expectedSecondaryPublisherCodecExplicit - ? baseConfig.expectedSecondaryPublisherCodec - : expectedCodecMime(secondaryCodec); - const scenarioName = baseConfig.secondaryPublisher ? `codec-${codec}-with-${secondaryCodec}` : `codec-${codec}`; - return { - ...baseConfig, - scenarioName, - room: scenarioRoomName(baseConfig.room, scenarioName, scenarioCount, baseConfig.externalTokens), - codec, - expectedScreenCodec: expectedCodecMime(expectedCodec), - secondaryPublisherCodec: secondaryCodec, - expectedSecondaryPublisherCodec: expectedSecondaryCodec, - reportPath: scenarioCount > 1 ? null : baseConfig.reportPath, - }; - }); -} - -function buildReport({status, config, checks = [], error = null, skippedReason = null, context = null}) { - const now = Date.now(); - const strictStartedAtMs = context?.strictStartedAtMs ?? context?.startedAtMs ?? now; - const strictEndedAtMs = context?.strictEndedAtMs ?? now; - const subscriberState = context?.subscriberState; - const subscriber = context?.subscriber; - const publisherState = context?.publisherState; - const secondaryPublisherState = context?.secondaryPublisherState; - const capabilities = typeof getHardwareEncoderCapabilities === 'function' ? getHardwareEncoderCapabilities() : null; - const strictVideoSelector = primaryScreenVideoSelector(config); - const inboundAudioFrames = subscriber ? subscriber.inboundAudioFrames() : 0; - const inboundVideoFrames = subscriber ? subscriber.inboundVideoFrames() : 0; - const strictVideoFrameTimes = subscriberState - ? selectedVideoFrameTimes(subscriberState, strictStartedAtMs, strictEndedAtMs, strictVideoSelector) - : []; - const strictVideoFrames = strictVideoFrameTimes.length; - const strictAudioFrames = subscriberState - ? countWindowSamples(subscriberState.audioFrameTimes, strictStartedAtMs, strictEndedAtMs) - : 0; - const measuredFps = receivedFpsFromTimes(strictVideoFrameTimes); - const avDriftMs = subscriberState - ? currentAvDriftMs(subscriberState, strictStartedAtMs, strictEndedAtMs, strictVideoSelector) - : null; - const observedPacketLoss = maxObservedPacketsLost(publisherState, secondaryPublisherState, subscriberState); - const observedPacketLossDelta = maxObservedPacketsLostDelta( - strictStartedAtMs, - strictEndedAtMs, - publisherState, - secondaryPublisherState, - subscriberState, - ); - return { - status, - startedAt: context?.startedAtIso ?? new Date(now).toISOString(), - endedAt: new Date(now).toISOString(), - platform: process.platform, - arch: process.arch, - config: sanitizeConfig(config), - checks, - skippedReason, - error: error ? String(error.stack ?? error.message ?? error) : null, - hardwareEncoder: capabilities, - metrics: { - inboundAudioFrames, - inboundVideoFrames, - strictAudioFrames, - strictVideoFrames, - videoCallbacks: subscriberState?.videoCallbacks ?? 0, - videoBytes: subscriberState?.videoBytes ?? 0, - receivedFps: Math.round(measuredFps * 100) / 100, - requiredFps: config.minFpsRatio > 0 ? Math.round(config.fps * config.minFpsRatio * 100) / 100 : null, - maxVideoFrameGapMs: maxGapMs(strictVideoFrameTimes, strictStartedAtMs, strictEndedAtMs), - maxAudioFrameGapMs: subscriberState - ? maxGapMs(subscriberState.audioFrameTimes, strictStartedAtMs, strictEndedAtMs) - : 0, - maxVideoFrameGapOverallMs: subscriberState?.maxVideoFrameGapMs ?? 0, - maxAudioFrameGapOverallMs: subscriberState?.maxAudioFrameGapMs ?? 0, - avDriftMs, - maxObservedPacketLoss: observedPacketLoss, - maxObservedPacketLossDelta: observedPacketLossDelta, - videoResolutionCounts: videoResolutionCounts( - subscriberState, - strictStartedAtMs, - strictEndedAtMs, - strictVideoSelector, - ), - videoResolutionMismatchCount: videoResolutionMismatchCount( - subscriberState, - config.expectedWidth, - config.expectedHeight, - strictStartedAtMs, - strictEndedAtMs, - strictVideoSelector, - ), - droppedVideoFrameCallbacks: subscriber ? subscriber.droppedVideoFrameCallbacks() : 0, - publisherVideoPump: context?.videoPump?.stats?.() ?? null, - secondaryPublisherVideoPump: context?.secondaryVideoPump?.stats?.() ?? null, - serverPublishing: summarizeServerPublishing(context?.serverState), - lastVideoMeta: subscriberState?.lastVideoMeta ?? null, - lastStrictVideoMeta: lastVideoFrameRecord( - subscriberState, - strictStartedAtMs, - strictEndedAtMs, - strictVideoSelector, - ), - publisherStats: publisherState?.stats ?? null, - secondaryPublisherStats: secondaryPublisherState?.stats ?? null, - subscriberStats: subscriberState?.stats ?? null, - }, - }; -} - -function strictFailures(report, config) { - const failures = []; - if (config.minFpsRatio > 0 && report.metrics.receivedFps < config.fps * config.minFpsRatio) { - failures.push(`received FPS ${report.metrics.receivedFps} below ${config.fps * config.minFpsRatio}`); - } - if (config.maxFrameGapMs > 0) { - if (report.metrics.strictVideoFrames < 2) { - failures.push( - 'video frame gap unavailable because fewer than two video frames were observed during strict window', - ); - } else if (report.metrics.maxVideoFrameGapMs > config.maxFrameGapMs) { - failures.push(`max video frame gap ${report.metrics.maxVideoFrameGapMs}ms exceeds ${config.maxFrameGapMs}ms`); - } - } - if (config.maxAudioFrameGapMs > 0 && expectedInboundAudioTrackCount(config) > 0) { - if (report.metrics.strictAudioFrames < 2) { - failures.push( - 'audio frame gap unavailable because fewer than two audio frames were observed during strict window', - ); - } else if (report.metrics.maxAudioFrameGapMs > config.maxAudioFrameGapMs) { - failures.push( - `max audio frame gap ${report.metrics.maxAudioFrameGapMs}ms exceeds ${config.maxAudioFrameGapMs}ms`, - ); - } - } - if (config.maxAvDriftMs > 0) { - if (report.metrics.avDriftMs === null) { - failures.push('A/V drift unavailable because video or audio timing was not observed'); - } else if (report.metrics.avDriftMs > config.maxAvDriftMs) { - failures.push(`A/V drift ${report.metrics.avDriftMs}ms exceeds ${config.maxAvDriftMs}ms`); - } - } - if (config.maxPacketLoss !== null && report.metrics.maxObservedPacketLossDelta > config.maxPacketLoss) { - failures.push( - `observed packet loss delta ${report.metrics.maxObservedPacketLossDelta} exceeds ${config.maxPacketLoss}`, - ); - } - if (config.requireStableResolution && report.metrics.videoResolutionMismatchCount > 0) { - failures.push( - `observed ${report.metrics.videoResolutionMismatchCount} video frames outside ${config.expectedWidth}x${config.expectedHeight}`, - ); - } - if (report.metrics.droppedVideoFrameCallbacks > 0) { - failures.push(`dropped video frame callbacks ${report.metrics.droppedVideoFrameCallbacks}`); - } - return failures; -} - -async function writeReportIfRequested(report, reportPath) { - if (!reportPath) return; - await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); -} - -async function runHarness(config) { - if (!isSupported()) { - throw new Error(`native binding unavailable: ${loadError?.message ?? 'unknown error'}`); - } - - const target = liveKitTcpTarget(config.url); - const probe = await probeTcp(target, config.probeTimeoutMs); - if (!probe.ok) { - const message = `LiveKit is unreachable at ${config.url} (${probe.reason})`; - if (!config.required) { - console.log(`[livekit-harness] SKIP: ${message}. Set FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED=1 to fail instead.`); - return {skipped: true, skippedReason: message}; - } - throw new Error(message); - } - - const publisherIdentity = config.publisherIdentity; - const secondaryPublisherIdentity = config.secondaryPublisherIdentity; - const subscriberIdentity = config.subscriberIdentity; - const publisherToken = - config.publisherToken ?? - createLiveKitToken({ - apiKey: config.apiKey, - apiSecret: config.apiSecret, - room: config.room, - identity: publisherIdentity, - name: 'Fluxer Native Publisher', - }); - const secondaryPublisherToken = config.secondaryPublisher - ? (config.secondaryPublisherToken ?? - createLiveKitToken({ - apiKey: config.apiKey, - apiSecret: config.apiSecret, - room: config.room, - identity: secondaryPublisherIdentity, - name: 'Fluxer Native Publisher 2', - })) - : null; - const subscriberToken = - config.subscriberToken ?? - createLiveKitToken({ - apiKey: config.apiKey, - apiSecret: config.apiSecret, - room: config.room, - identity: subscriberIdentity, - name: 'Fluxer Native Subscriber', - }); - - const publisher = new VoiceEngine(); - const secondaryPublisher = config.secondaryPublisher ? new VoiceEngine() : null; - const subscriber = new VoiceEngine(); - const publisherState = createEngineState('publisher', config.verbose); - const secondaryPublisherState = config.secondaryPublisher ? createEngineState('publisher-2', config.verbose) : null; - const subscriberState = createEngineState('subscriber', config.verbose); - const pumpErrors = []; - const context = { - publisher, - secondaryPublisher, - subscriber, - publisherState, - secondaryPublisherState, - subscriberState, - serverState: { - participants: null, - error: null, - fetchedAtMs: null, - lastAttemptAtMs: 0, - inFlight: false, - }, - config, - publisherIdentity, - secondaryPublisherIdentity, - subscriberIdentity, - startedAtMs: Date.now(), - startedAtIso: new Date().toISOString(), - strictStartedAtMs: null, - strictEndedAtMs: null, - }; - let videoPump = null; - let secondaryVideoPump = null; - let micPump = null; - let secondaryMicPump = null; - let screenAudioPump = null; - let secondaryScreenAudioPump = null; - let audioSampler = null; - - publisher.setEventCallback((...args) => { - const [eventType, payload] = unpackNapiPair(args); - publisherState.recordEvent(eventType, payload); - }); - secondaryPublisher?.setEventCallback((...args) => { - const [eventType, payload] = unpackNapiPair(args); - secondaryPublisherState.recordEvent(eventType, payload); - }); - subscriber.setEventCallback((...args) => { - const [eventType, payload] = unpackNapiPair(args); - subscriberState.recordEvent(eventType, payload); - }); - subscriber.setVideoFrameCallback((...args) => { - const [metaJson, data] = unpackNapiPair(args); - subscriberState.recordVideoFrame(metaJson, data); - }); - subscriber.setCountInboundAudio(true); - audioSampler = startAudioSampler(subscriber, subscriberState); - - try { - const e2eeKey = config.e2eeKey ? Buffer.from(config.e2eeKey, 'utf8') : undefined; - console.log(`[livekit-harness] connecting to ${config.url} room=${config.room}`); - await withTimeout( - Promise.all( - [ - publisher.connect(config.url, publisherToken, e2eeKey), - secondaryPublisher && secondaryPublisherToken - ? secondaryPublisher.connect(config.url, secondaryPublisherToken, e2eeKey) - : null, - subscriber.connect(config.url, subscriberToken, e2eeKey), - ].filter(Boolean), - ), - config.connectTimeoutMs, - 'connect', - ); - - await withTimeout( - publisher.publishScreenShare( - config.width, - config.height, - config.codec, - config.maxBitrateBps, - config.fps, - config.screenSimulcast, - { - adaptiveSend: config.adaptiveSend, - minVideoFps: config.minVideoFps, - minResolutionScale: config.minResolutionScale, - maxAudioBufferMs: config.maxAudioBufferMs, - captureId: 'livekit-harness-primary-screen', - trackName: 'livekit-harness-primary-screen', - }, - ), - 10_000, - 'publish screenshare', - ); - if (secondaryPublisher) { - await withTimeout( - secondaryPublisher.publishScreenShare( - config.width, - config.height, - config.secondaryPublisherCodec, - config.maxBitrateBps, - config.fps, - config.secondaryPublisherScreenSimulcast, - { - adaptiveSend: config.adaptiveSend, - minVideoFps: config.minVideoFps, - minResolutionScale: config.minResolutionScale, - maxAudioBufferMs: config.maxAudioBufferMs, - captureId: 'livekit-harness-secondary-screen', - trackName: 'livekit-harness-secondary-screen', - }, - ), - 10_000, - 'publish secondary screenshare', - ); - } - if (config.microphone) { - await withTimeout(publisher.publishMicrophone(config.sampleRate, config.channels), 10_000, 'publish microphone'); - } - if (secondaryPublisher && config.secondaryPublisherMicrophone) { - await withTimeout( - secondaryPublisher.publishMicrophone(config.sampleRate, config.channels), - 10_000, - 'publish secondary microphone', - ); - } - if (config.screenAudio) { - await withTimeout( - publisher.publishScreenShareAudio(config.sampleRate, config.channels), - 10_000, - 'publish screen-share audio', - ); - } - if (secondaryPublisher && config.secondaryPublisherScreenAudio) { - await withTimeout( - secondaryPublisher.publishScreenShareAudio(config.sampleRate, config.channels), - 10_000, - 'publish secondary screen-share audio', - ); - } - if (config.camera) { - await withTimeout( - publisher.publishCamera({ - deviceId: config.cameraDeviceId ?? undefined, - width: config.width, - height: config.height, - frameRate: config.fps, - }), - 10_000, - 'publish camera', - ); - } - - videoPump = startVideoPump(publisher, config, pumpErrors); - context.videoPump = videoPump; - if (secondaryPublisher) { - secondaryVideoPump = startVideoPump(secondaryPublisher, config, pumpErrors); - context.secondaryVideoPump = secondaryVideoPump; - } - if (config.microphone) { - micPump = startAudioPump( - 'pushPcm', - (buffer, sampleRate, channels) => publisher.pushPcm(buffer, sampleRate, channels), - config, - pumpErrors, - ); - } - if (secondaryPublisher && config.secondaryPublisherMicrophone) { - secondaryMicPump = startAudioPump( - 'secondary pushPcm', - (buffer, sampleRate, channels) => secondaryPublisher.pushPcm(buffer, sampleRate, channels), - config, - pumpErrors, - ); - } - if (config.screenAudio) { - screenAudioPump = startAudioPump( - 'pushScreenSharePcm', - (buffer, sampleRate, channels) => publisher.pushScreenSharePcm(buffer, sampleRate, channels), - config, - pumpErrors, - ); - } - if (secondaryPublisher && config.secondaryPublisherScreenAudio) { - secondaryScreenAudioPump = startAudioPump( - 'secondary pushScreenSharePcm', - (buffer, sampleRate, channels) => secondaryPublisher.pushScreenSharePcm(buffer, sampleRate, channels), - config, - pumpErrors, - ); - } - if (config.dataPacket) { - await publisher.publishData(Buffer.from('fluxer-native-media-harness'), { - reliable: true, - topic: 'native-media-harness', - destinationIdentities: [subscriberIdentity], - }); - if (secondaryPublisher) { - await secondaryPublisher.publishData(Buffer.from('fluxer-native-media-harness-secondary'), { - reliable: true, - topic: 'native-media-harness-secondary', - destinationIdentities: [subscriberIdentity], - }); - } - } - await setSubscriberVideoQuality(subscriber, publisherIdentity, config.subscriberVideoQuality); - if (secondaryPublisher) { - await setSubscriberVideoQuality(subscriber, secondaryPublisherIdentity, config.subscriberVideoQuality); - } - if (config.subscriptionCycle) { - await runSubscriptionCycle(subscriber, publisherIdentity); - if (secondaryPublisher) { - await runSubscriptionCycle(subscriber, secondaryPublisherIdentity); - } - } - - const checks = await waitForHarnessSuccess(context, pumpErrors, config.timeoutMs); - await holdStrictDuration(context, pumpErrors, config.durationMs); - const report = buildReport({status: 'pass', config, checks, context}); - const failures = strictFailures(report, config); - if (failures.length > 0) { - const failedReport = {...report, status: 'fail', error: failures.join('; ')}; - await writeReportIfRequested(failedReport, config.reportPath); - const error = new Error(failures.join('; ')); - error.report = failedReport; - throw error; - } - await writeReportIfRequested(report, config.reportPath); - const producedFps = report.metrics.publisherVideoPump?.producedFps ?? null; - console.log( - `[livekit-harness] PASS: ${checks.length} checks, inboundAudioFrames=${report.metrics.inboundAudioFrames}, inboundVideoFrames=${report.metrics.inboundVideoFrames}, videoCallbacks=${report.metrics.videoCallbacks}, receivedFps=${report.metrics.receivedFps}, producedFps=${producedFps}, maxVideoFrameGapMs=${report.metrics.maxVideoFrameGapMs}, maxAudioFrameGapMs=${report.metrics.maxAudioFrameGapMs}, maxObservedPacketLoss=${report.metrics.maxObservedPacketLoss}, maxObservedPacketLossDelta=${report.metrics.maxObservedPacketLossDelta}, resolutionMismatches=${report.metrics.videoResolutionMismatchCount}, droppedVideoFrameCallbacks=${report.metrics.droppedVideoFrameCallbacks}`, - ); - return {skipped: false, report}; - } catch (error) { - if (!error.report) { - let checks = []; - try { - checks = currentChecks(context); - } catch {} - error.report = buildReport({status: 'fail', config, checks, error, context}); - } - throw error; - } finally { - videoPump?.stop(); - secondaryVideoPump?.stop(); - micPump?.stop(); - secondaryMicPump?.stop(); - screenAudioPump?.stop(); - secondaryScreenAudioPump?.stop(); - audioSampler?.stop(); - await micPump?.done(); - await secondaryMicPump?.done(); - await screenAudioPump?.done(); - await secondaryScreenAudioPump?.done(); - await disconnectEngine(subscriber, 'subscriber', config.disconnectTimeoutMs); - await disconnectEngine(secondaryPublisher, 'publisher-2', config.disconnectTimeoutMs); - await disconnectEngine(publisher, 'publisher', config.disconnectTimeoutMs); - } -} - -function buildAggregateReport({status, config, scenarioReports, startedAtMs}) { - return { - status, - startedAt: new Date(startedAtMs).toISOString(), - endedAt: new Date().toISOString(), - platform: process.platform, - arch: process.arch, - config: sanitizeConfig(config), - scenarioCount: scenarioReports.length, - scenarios: scenarioReports, - }; -} - -async function runScenarioSuite(baseConfig, scenarios) { - const startedAtMs = Date.now(); - const scenarioReports = []; - for (let index = 0; index < scenarios.length; index += 1) { - const scenario = scenarios[index]; - console.log( - `[livekit-harness] scenario ${index + 1}/${scenarios.length}: ${scenario.scenarioName} room=${scenario.room}`, - ); - try { - const result = await runHarness(scenario); - if (result.skipped) { - scenarioReports.push(buildReport({status: 'skip', config: scenario, skippedReason: result.skippedReason})); - } else { - scenarioReports.push(result.report); - } - } catch (error) { - scenarioReports.push(error.report ?? buildReport({status: 'fail', config: scenario, error})); - } - } - const failed = scenarioReports.filter((report) => report.status === 'fail'); - const skipped = scenarioReports.filter((report) => report.status === 'skip'); - const status = failed.length > 0 ? 'fail' : skipped.length === scenarioReports.length ? 'skip' : 'pass'; - const aggregate = buildAggregateReport({status, config: baseConfig, scenarioReports, startedAtMs}); - await writeReportIfRequested(aggregate, baseConfig.reportPath); - if (status === 'fail') { - const failedNames = failed.map((report) => report.config.scenarioName ?? report.config.codec).join(', '); - const error = new Error(`LiveKit scenario suite failed: ${failedNames}`); - error.report = aggregate; - throw error; - } - return aggregate; -} - -async function main() { - const baseConfig = parseConfig(); - const scenarios = buildScenarioConfigs(baseConfig); - try { - if (scenarios.length > 1) { - const aggregate = await runScenarioSuite(baseConfig, scenarios); - console.log(`[livekit-harness] ${aggregate.status.toUpperCase()}: ${aggregate.scenarioCount} codec scenarios`); - return 0; - } - const config = scenarios[0]; - const result = await runHarness(config); - if (result.skipped) { - const report = buildReport({status: 'skip', config, skippedReason: result.skippedReason}); - await writeReportIfRequested(report, baseConfig.reportPath); - } - return 0; - } catch (error) { - const report = error.report ?? buildReport({status: 'fail', config: baseConfig, error}); - await writeReportIfRequested(report, baseConfig.reportPath); - console.error(`[livekit-harness] FAIL: ${error.stack ?? error.message}`); - return 1; - } -} - -export { - buildAggregateReport, - buildReport, - buildScenarioConfigs, - createLiveKitAdminToken, - createLiveKitToken, - expectedCodecMime, - jwtSubject, - liveKitApiUrl, - liveKitTcpTarget, - parseCodecList, - parseConfig, - sanitizeConfig, - scenarioRoomName, - serverPublishedTrackChecks, - statsHasOutbound, - statsHasOutboundCodec, - strictFailures, -}; - -if (process.argv[1] === fileURLToPath(import.meta.url)) { - const exitCode = await main(); - setImmediate(() => process.exit(exitCode)); -} diff --git a/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.test.mjs b/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.test.mjs deleted file mode 100644 index ed6547f08..000000000 --- a/fluxer_desktop/native/webrtc-sender/scripts/livekit-harness.test.mjs +++ /dev/null @@ -1,842 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import assert from 'node:assert/strict'; -import {describe, test} from 'node:test'; -import { - buildReport, - buildScenarioConfigs, - createLiveKitAdminToken, - expectedCodecMime, - jwtSubject, - liveKitApiUrl, - liveKitTcpTarget, - parseCodecList, - parseConfig, - sanitizeConfig, - scenarioRoomName, - serverPublishedTrackChecks, - statsHasOutbound, - statsHasOutboundCodec, - strictFailures, -} from './livekit-harness.mjs'; - -function tokenForSubject(subject) { - const payload = Buffer.from(JSON.stringify({sub: subject}), 'utf8').toString('base64url'); - return `header.${payload}.signature`; -} - -function jwtPayload(token) { - const parts = token.split('.'); - return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')); -} - -async function withHarnessEnv(env, callback) { - const previous = {...process.env}; - for (const key of Object.keys(process.env)) { - if ( - key.startsWith('LIVEKIT_') || - key.startsWith('FLUXER_WEBRTC_SENDER_LIVEKIT') || - key.startsWith('FLUXER_NATIVE_MEDIA') - ) { - delete process.env[key]; - } - } - Object.assign(process.env, env); - try { - return await callback(); - } finally { - for (const key of Object.keys(process.env)) { - delete process.env[key]; - } - Object.assign(process.env, previous); - } -} - -function strictReportFor( - config, - { - videoFrameTimes = null, - videoFrameRecords = null, - audioFrameTimes = [], - droppedVideoFrameCallbacks = 0, - subscriberStatsSamples = [], - } = {}, -) { - const strictStartedAtMs = 1_000; - const strictEndedAtMs = 2_000; - const effectiveVideoFrameRecords = - videoFrameRecords ?? - (videoFrameTimes ?? []).map((atMs) => ({ - atMs, - identity: config.publisherIdentity, - kind: 'video', - source: 'screen_share', - trackName: 'screen', - trackSid: 'TR_screen', - width: config.expectedWidth, - height: config.expectedHeight, - })); - const effectiveVideoFrameTimes = videoFrameTimes ?? effectiveVideoFrameRecords.map((record) => record.atMs); - return buildReport({ - status: 'pass', - config, - context: { - startedAtMs: strictStartedAtMs - 500, - startedAtIso: new Date(strictStartedAtMs - 500).toISOString(), - strictStartedAtMs, - strictEndedAtMs, - publisherState: {statsSamples: [], stats: null}, - subscriberState: { - statsSamples: subscriberStatsSamples, - stats: subscriberStatsSamples.at(-1)?.payload ?? null, - videoCallbacks: effectiveVideoFrameTimes.length, - videoBytes: effectiveVideoFrameTimes.length * 4, - videoFrameTimes: effectiveVideoFrameTimes, - videoFrameRecords: effectiveVideoFrameRecords, - audioFrameTimes, - lastVideoMeta: effectiveVideoFrameRecords.at(-1) ?? null, - lastVideoFrameAtMs: effectiveVideoFrameTimes.at(-1) ?? null, - lastAudioFrameAtMs: audioFrameTimes.at(-1) ?? null, - }, - subscriber: { - inboundAudioFrames: () => audioFrameTimes.length, - inboundVideoFrames: () => effectiveVideoFrameTimes.length, - droppedVideoFrameCallbacks: () => droppedVideoFrameCallbacks, - }, - }, - }); -} - -function findCheck(checks, name) { - const check = checks.find((entry) => entry.name === name); - assert.ok(check, `missing check: ${name}`); - return check; -} - -function serverChecksFor(config, serverState) { - return serverPublishedTrackChecks({ - config, - serverState, - publisherIdentity: config.publisherIdentity, - secondaryPublisherIdentity: config.secondaryPublisherIdentity, - }); -} - -describe('livekit harness setup matrix', () => { - test('normalizes LiveKit screenshare source spelling in outbound stats', () => { - const stats = { - outbound: [ - { - trackSid: 'TR_screen', - source: 'screenshare', - kind: 'video', - codec: 'video/VP8', - bitrateKbps: 400, - packetsLost: 0, - }, - ], - }; - - assert.equal(statsHasOutbound(stats, 'video', 'screen_share'), true); - assert.equal(statsHasOutboundCodec(stats, 'video', 'screen_share', 'video/VP8'), true); - assert.equal(statsHasOutboundCodec(stats, 'video', 'screen_share', 'video/H264'), false); - }); - - test('maps all supported codec spellings to expected mime types', () => { - assert.equal(expectedCodecMime('vp8'), 'video/VP8'); - assert.equal(expectedCodecMime('VP9'), 'video/VP9'); - assert.equal(expectedCodecMime('h264'), 'video/H264'); - assert.equal(expectedCodecMime('av1'), 'video/AV1'); - assert.equal(expectedCodecMime('h265'), 'video/H265'); - assert.equal(expectedCodecMime('hevc'), 'video/H265'); - assert.equal(expectedCodecMime(''), null); - assert.throws(() => expectedCodecMime('h266'), /unsupported/); - }); - - test('parses codec lists with fallback and rejects empty effective lists', () => { - assert.deepEqual(parseCodecList(' vp8, h264,, hevc ', 'av1'), ['vp8', 'h264', 'hevc']); - assert.deepEqual(parseCodecList('', 'vp9'), ['vp9']); - assert.throws(() => parseCodecList('', ''), /at least one/); - assert.throws(() => parseCodecList('vp8,h266', 'vp8'), /unsupported/); - }); - - test('default non-strict config keeps the fast local live setup small', async () => { - await withHarnessEnv({}, () => { - const config = parseConfig(); - assert.equal(config.url, 'ws://localhost:7880'); - assert.equal(config.serverApiUrl, 'http://localhost:7880'); - assert.deepEqual(config.screenCodecs, ['vp8']); - assert.equal(config.expectedScreenCodec, 'video/VP8'); - assert.equal(config.secondaryPublisher, false); - assert.equal(config.microphone, true); - assert.equal(config.screenAudio, false); - assert.equal(config.dataPacket, false); - assert.equal(config.subscriptionCycle, false); - assert.equal(config.validateServerPublishing, false); - assert.equal(config.durationMs, 0); - assert.equal(config.externalTokens, false); - assert.equal(config.screenSimulcast, true); - assert.equal(config.secondaryPublisherScreenSimulcast, true); - assert.equal(config.adaptiveSend, true); - assert.equal(config.minVideoFps, 15); - assert.equal(config.minResolutionScale, 0.5); - assert.equal(config.maxAudioBufferMs, 750); - }); - }); - - test('strict config expands to multi-codec secondary-publisher scenarios', async () => { - await withHarnessEnv( - { - LIVEKIT_HARNESS_STRICT: '1', - LIVEKIT_ROOM: 'matrix-room', - LIVEKIT_SCREEN_CODECS: 'vp8,h264,hevc', - LIVEKIT_EXPECT_SCREEN_CODECS: 'vp8,h264,h265', - }, - () => { - const config = parseConfig(); - assert.equal(config.secondaryPublisher, true); - assert.equal(config.screenAudio, true); - assert.equal(config.dataPacket, true); - assert.equal(config.subscriptionCycle, true); - assert.equal(config.validateServerPublishing, true); - assert.equal(config.durationMs, 10 * 60 * 1000); - assert.equal(config.maxAudioFrameGapMs, 250); - assert.equal(config.maxPacketLoss, null); - assert.equal(config.requireStableResolution, true); - assert.equal(config.videoPattern, 'gradient'); - - const scenarios = buildScenarioConfigs(config); - assert.deepEqual( - scenarios.map((scenario) => scenario.scenarioName), - ['codec-vp8-with-h264', 'codec-h264-with-hevc', 'codec-hevc-with-vp8'], - ); - assert.deepEqual( - scenarios.map((scenario) => scenario.expectedScreenCodec), - ['video/VP8', 'video/H264', 'video/H265'], - ); - assert.deepEqual( - scenarios.map((scenario) => scenario.expectedSecondaryPublisherCodec), - ['video/H264', 'video/H265', 'video/VP8'], - ); - assert.equal(scenarios[0].room, 'matrix-room-codec-vp8-with-h264'); - assert.equal(scenarios[1].reportPath, null); - }, - ); - }); - - test('stress knobs parse packet loss, resolution stability, audio gap, and video pattern', async () => { - await withHarnessEnv( - { - LIVEKIT_MAX_PACKET_LOSS: '0', - LIVEKIT_REQUIRE_STABLE_RESOLUTION: '0', - LIVEKIT_MAX_AUDIO_FRAME_GAP_MS: '180', - LIVEKIT_VIDEO_PATTERN: 'fast', - LIVEKIT_VIDEO_INPUT: 'nv12', - LIVEKIT_SUBSCRIBER_VIDEO_QUALITY: 'high', - LIVEKIT_SCREEN_SIMULCAST: '0', - LIVEKIT_SECOND_PUBLISHER_SCREEN_SIMULCAST: '1', - LIVEKIT_SCREEN_FPS: '60', - LIVEKIT_ADAPTIVE_SEND: '0', - LIVEKIT_MIN_VIDEO_FPS: '24', - LIVEKIT_MIN_RESOLUTION_SCALE: '0.75', - LIVEKIT_MAX_AUDIO_BUFFER_MS: '640', - }, - () => { - const config = parseConfig(); - assert.equal(config.maxPacketLoss, 0); - assert.equal(config.requireStableResolution, false); - assert.equal(config.maxAudioFrameGapMs, 180); - assert.equal(config.videoPattern, 'fast'); - assert.equal(config.videoInput, 'nv12'); - assert.equal(config.subscriberVideoQuality, 'high'); - assert.equal(config.screenSimulcast, false); - assert.equal(config.secondaryPublisherScreenSimulcast, true); - assert.equal(config.adaptiveSend, false); - assert.equal(config.minVideoFps, 24); - assert.equal(config.minResolutionScale, 0.75); - assert.equal(config.maxAudioBufferMs, 640); - }, - ); - }); - - test('stress knobs reject impossible send pacing configurations', async () => { - await withHarnessEnv({LIVEKIT_SCREEN_FPS: '0'}, () => - assert.throws(() => parseConfig(), /LIVEKIT_SCREEN_FPS must be a positive number/), - ); - await withHarnessEnv({LIVEKIT_MIN_VIDEO_FPS: '0'}, () => - assert.throws(() => parseConfig(), /LIVEKIT_MIN_VIDEO_FPS must be a positive number/), - ); - await withHarnessEnv({LIVEKIT_SCREEN_FPS: '30', LIVEKIT_MIN_VIDEO_FPS: '60'}, () => - assert.throws(() => parseConfig(), /MIN_VIDEO_FPS must be less than or equal/), - ); - await withHarnessEnv({LIVEKIT_MAX_AUDIO_BUFFER_MS: '0'}, () => - assert.throws(() => parseConfig(), /LIVEKIT_MAX_AUDIO_BUFFER_MS must be a positive integer/), - ); - await withHarnessEnv({LIVEKIT_MIN_RESOLUTION_SCALE: '0'}, () => - assert.throws(() => parseConfig(), /LIVEKIT_MIN_RESOLUTION_SCALE must be a positive number/), - ); - await withHarnessEnv({LIVEKIT_MIN_RESOLUTION_SCALE: '1.5'}, () => - assert.throws(() => parseConfig(), /LIVEKIT_MIN_RESOLUTION_SCALE must be greater than 0/), - ); - await withHarnessEnv({LIVEKIT_ENABLE_SCREEN_AUDIO: 'maybe'}, () => - assert.throws(() => parseConfig(), /LIVEKIT_ENABLE_SCREEN_AUDIO must be a boolean flag/), - ); - await withHarnessEnv({LIVEKIT_SCREEN_CODECS: 'vp8,h264', LIVEKIT_EXPECT_SCREEN_CODECS: 'vp8'}, () => - assert.throws(() => parseConfig(), /EXPECT_SCREEN_CODECS length must match/), - ); - }); - - test('explicit secondary codec expectations override rotating defaults', async () => { - await withHarnessEnv( - { - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_SCREEN_CODECS: 'vp8,h264', - LIVEKIT_SECOND_PUBLISHER_CODEC: 'av1', - LIVEKIT_EXPECT_SECOND_PUBLISHER_SCREEN_CODEC: 'hevc', - }, - () => { - const scenarios = buildScenarioConfigs(parseConfig()); - assert.deepEqual( - scenarios.map((scenario) => scenario.secondaryPublisherCodec), - ['av1', 'av1'], - ); - assert.deepEqual( - scenarios.map((scenario) => scenario.expectedSecondaryPublisherCodec), - ['video/H265', 'video/H265'], - ); - }, - ); - }); - - test('secondary screen codec aliases are accepted and conflicting aliases are rejected', async () => { - await withHarnessEnv( - { - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_SCREEN_CODECS: 'h264', - LIVEKIT_SECOND_PUBLISHER_SCREEN_CODECS: 'vp8', - LIVEKIT_EXPECT_SECONDARY_PUBLISHER_SCREEN_CODEC: 'vp8', - }, - () => { - const config = parseConfig(); - assert.equal(config.secondaryPublisherCodec, 'vp8'); - assert.equal(config.expectedSecondaryPublisherCodec, 'video/VP8'); - }, - ); - - await withHarnessEnv( - { - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_SECOND_PUBLISHER_CODEC: 'h264', - LIVEKIT_SECOND_PUBLISHER_SCREEN_CODEC: 'vp8', - }, - () => assert.throws(() => parseConfig(), /conflicts with LIVEKIT_SECOND_PUBLISHER_CODEC/), - ); - - await withHarnessEnv( - { - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_EXPECT_SECOND_PUBLISHER_SCREEN_CODEC: 'h264', - LIVEKIT_EXPECT_SECONDARY_PUBLISHER_SCREEN_CODEC: 'vp8', - }, - () => assert.throws(() => parseConfig(), /conflicts with LIVEKIT_EXPECT_SECOND_PUBLISHER_SCREEN_CODEC/), - ); - }); - - test('external token setup derives identities, requires matching tokens, and redacts reports', async () => { - await withHarnessEnv( - { - LIVEKIT_PUBLISHER_TOKEN: tokenForSubject('publisher-subject'), - }, - () => assert.throws(() => parseConfig(), /both required/), - ); - - await withHarnessEnv( - { - LIVEKIT_ROOM: 'shared-token-room', - LIVEKIT_SCREEN_CODECS: 'vp8,h264', - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_PUBLISHER_TOKEN: tokenForSubject('publisher-subject'), - LIVEKIT_SUBSCRIBER_TOKEN: tokenForSubject('subscriber-subject'), - }, - () => assert.throws(() => parseConfig(), /SECONDARY_PUBLISHER_TOKEN/), - ); - - await withHarnessEnv( - { - LIVEKIT_ROOM: 'shared-token-room', - LIVEKIT_SCREEN_CODECS: 'vp8,h264', - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_PUBLISHER_TOKEN: tokenForSubject('publisher-subject'), - LIVEKIT_SUBSCRIBER_TOKEN: tokenForSubject('subscriber-subject'), - LIVEKIT_SECONDARY_PUBLISHER_TOKEN: tokenForSubject('secondary-subject'), - LIVEKIT_E2EE_KEY: 'secret-key', - }, - () => { - const config = parseConfig(); - assert.equal(config.externalTokens, true); - assert.equal(config.publisherIdentity, 'publisher-subject'); - assert.equal(config.subscriberIdentity, 'subscriber-subject'); - assert.equal(config.secondaryPublisherIdentity, 'secondary-subject'); - assert.deepEqual( - buildScenarioConfigs(config).map((scenario) => scenario.room), - ['shared-token-room', 'shared-token-room'], - ); - - const sanitized = sanitizeConfig(config); - assert.equal(sanitized.url, 'ws://localhost:7880'); - assert.equal(sanitized.apiSecret, ''); - assert.equal(sanitized.e2eeKey, ''); - assert.equal(sanitized.publisherToken, ''); - assert.equal(sanitized.subscriberToken, ''); - assert.equal(sanitized.secondaryPublisherToken, ''); - }, - ); - }); - - test('report sanitization strips URL credentials, query strings, and fragments', async () => { - await withHarnessEnv( - { - LIVEKIT_URL: 'wss://user:password@example.test:443/rtc?access_token=secret#fragment', - }, - () => { - const sanitized = sanitizeConfig(parseConfig()); - assert.equal(sanitized.url, 'wss://example.test/rtc'); - assert.doesNotMatch(JSON.stringify(sanitized), /user|password|access_token|secret|fragment/); - }, - ); - }); - - test('jwt subject and TCP target parsing catch malformed live setups early', () => { - assert.equal(jwtSubject(tokenForSubject('user_1_connection')), 'user_1_connection'); - assert.throws(() => jwtSubject('not-a-jwt'), /must be a JWT/); - assert.throws(() => jwtSubject('header.bad-json.signature'), /failed to decode/); - assert.throws(() => jwtSubject(tokenForSubject('')), /does not contain/); - - assert.deepEqual(liveKitTcpTarget('ws://localhost:7880'), {host: 'localhost', port: 7880}); - assert.deepEqual(liveKitTcpTarget('wss://[::1]/rtc'), {host: '::1', port: 443}); - assert.equal(liveKitApiUrl('ws://localhost:7880/rtc?token=secret'), 'http://localhost:7880'); - assert.equal(liveKitApiUrl('wss://livekit.example.test/rtc'), 'https://livekit.example.test'); - assert.throws(() => liveKitTcpTarget('https://localhost'), /must use ws/); - }); - - test('admin token has the room-scoped grant needed for server publishing validation', () => { - const token = createLiveKitAdminToken({apiKey: 'devkey', apiSecret: 'secret', room: 'room-a'}); - const payload = jwtPayload(token); - assert.equal(payload.iss, 'devkey'); - assert.equal(payload.video.room, 'room-a'); - assert.equal(payload.video.roomAdmin, true); - assert.equal(payload.video.roomJoin, undefined); - }); - - test('scenario room names are stable and bounded for generated-token suites', () => { - assert.equal(scenarioRoomName('room', 'codec-vp8', 1, false), 'room'); - assert.equal(scenarioRoomName('room', 'codec:vP8 with H264', 2, false), 'room-codec-vP8-with-H264'); - assert.equal(scenarioRoomName('room', 'codec-vp8', 2, true), 'room'); - - const longRoom = scenarioRoomName('r'.repeat(120), 'codec-h264-with-hevc', 2, false); - assert.equal(longRoom.length, 128); - assert.ok(longRoom.startsWith('r'.repeat(120))); - }); - - test('strict report checks fail when the strict window has too few samples to measure gaps', async () => { - await withHarnessEnv( - { - LIVEKIT_MIN_RECEIVED_FPS_RATIO: '0', - LIVEKIT_MAX_FRAME_GAP_MS: '250', - LIVEKIT_MAX_AUDIO_FRAME_GAP_MS: '250', - LIVEKIT_MAX_AV_DRIFT_MS: '0', - }, - () => { - const config = parseConfig(); - const report = strictReportFor(config, { - videoFrameTimes: [1_250], - audioFrameTimes: [1_300], - }); - assert.equal(report.metrics.strictVideoFrames, 1); - assert.equal(report.metrics.strictAudioFrames, 1); - assert.match(strictFailures(report, config).join('\n'), /video frame gap unavailable/); - assert.match(strictFailures(report, config).join('\n'), /audio frame gap unavailable/); - }, - ); - }); - - test('strict report checks use only samples inside the measured window', async () => { - await withHarnessEnv( - { - LIVEKIT_MIN_RECEIVED_FPS_RATIO: '0', - LIVEKIT_MAX_FRAME_GAP_MS: '250', - LIVEKIT_MAX_AUDIO_FRAME_GAP_MS: '250', - LIVEKIT_MAX_AV_DRIFT_MS: '0', - }, - () => { - const config = parseConfig(); - const report = strictReportFor(config, { - videoFrameTimes: [500, 1_050, 1_200, 2_500], - audioFrameTimes: [600, 1_100, 1_240, 2_600], - }); - assert.equal(report.metrics.strictVideoFrames, 2); - assert.equal(report.metrics.strictAudioFrames, 2); - assert.equal(report.metrics.maxVideoFrameGapMs, 150); - assert.equal(report.metrics.maxAudioFrameGapMs, 140); - assert.deepEqual(strictFailures(report, config), []); - }, - ); - }); - - test('strict screen report ignores camera video callbacks for resolution and drift', async () => { - await withHarnessEnv( - { - LIVEKIT_ENABLE_CAMERA: '1', - LIVEKIT_MIN_RECEIVED_FPS_RATIO: '0', - LIVEKIT_MAX_FRAME_GAP_MS: '250', - LIVEKIT_MAX_AUDIO_FRAME_GAP_MS: '250', - LIVEKIT_MAX_AV_DRIFT_MS: '80', - }, - () => { - const config = parseConfig(); - const report = strictReportFor(config, { - videoFrameRecords: [ - { - atMs: 1_010, - identity: config.publisherIdentity, - kind: 'video', - source: 'screen_share', - trackName: 'screen', - trackSid: 'TR_screen', - width: 320, - height: 180, - }, - { - atMs: 1_025, - identity: config.publisherIdentity, - kind: 'video', - source: 'camera', - trackName: 'camera', - trackSid: 'TR_camera', - width: 480, - height: 360, - }, - { - atMs: 1_080, - identity: config.publisherIdentity, - kind: 'video', - source: 'screen_share', - trackName: 'screen', - trackSid: 'TR_screen', - width: 320, - height: 180, - }, - { - atMs: 1_220, - identity: config.publisherIdentity, - kind: 'video', - source: 'camera', - trackName: 'camera', - trackSid: 'TR_camera', - width: 480, - height: 360, - }, - ], - audioFrameTimes: [1_040, 1_100], - }); - - assert.equal(report.metrics.videoCallbacks, 4); - assert.equal(report.metrics.strictVideoFrames, 2); - assert.deepEqual(report.metrics.videoResolutionCounts, {'320x180': 2}); - assert.equal(report.metrics.videoResolutionMismatchCount, 0); - assert.equal(report.metrics.avDriftMs, 20); - assert.deepEqual(strictFailures(report, config), []); - }, - ); - }); - - test('strict packet loss gate uses loss deltas inside the strict window', async () => { - await withHarnessEnv( - { - LIVEKIT_MIN_RECEIVED_FPS_RATIO: '0', - LIVEKIT_MAX_FRAME_GAP_MS: '0', - LIVEKIT_MAX_AUDIO_FRAME_GAP_MS: '0', - LIVEKIT_MAX_AV_DRIFT_MS: '0', - LIVEKIT_MAX_PACKET_LOSS: '0', - }, - () => { - const config = parseConfig(); - const report = strictReportFor(config, { - videoFrameTimes: [1_100, 1_200], - subscriberStatsSamples: [ - {atMs: 900, payload: {outbound: [], inbound: [{kind: 'video', packetsLost: 4}]}}, - {atMs: 1_500, payload: {outbound: [], inbound: [{kind: 'video', packetsLost: 4}]}}, - ], - }); - assert.equal(report.metrics.maxObservedPacketLoss, 4); - assert.equal(report.metrics.maxObservedPacketLossDelta, 0); - assert.deepEqual(strictFailures(report, config), []); - - const failingReport = strictReportFor(config, { - videoFrameTimes: [1_100, 1_200], - subscriberStatsSamples: [ - {atMs: 900, payload: {outbound: [], inbound: [{kind: 'video', packetsLost: 4}]}}, - {atMs: 1_500, payload: {outbound: [], inbound: [{kind: 'video', packetsLost: 5}]}}, - ], - }); - assert.equal(failingReport.metrics.maxObservedPacketLossDelta, 1); - assert.match(strictFailures(failingReport, config).join('\n'), /packet loss delta 1/); - }, - ); - }); - - test('server publishing checks cover primary and secondary screen, audio, and camera tracks', async () => { - await withHarnessEnv( - { - LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1', - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_ENABLE_SCREEN_AUDIO: '1', - LIVEKIT_ENABLE_CAMERA: '1', - }, - () => { - const config = parseConfig(); - const serverState = { - participants: [ - { - identity: config.publisherIdentity, - tracks: [ - {type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false, sid: 'TR_V1'}, - {type: 'AUDIO', source: 'MICROPHONE', mimeType: 'audio/red', muted: false, sid: 'TR_A1'}, - {type: 'AUDIO', source: 'SCREEN_SHARE_AUDIO', mimeType: 'audio/red', muted: false, sid: 'TR_A2'}, - {type: 'VIDEO', source: 'CAMERA', muted: false, sid: 'TR_V2'}, - ], - }, - { - identity: config.secondaryPublisherIdentity, - tracks: [ - {type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false, sid: 'TR_V3'}, - {type: 'AUDIO', source: 'MICROPHONE', mimeType: 'audio/red', muted: false, sid: 'TR_A3'}, - {type: 'AUDIO', source: 'SCREEN_SHARE_AUDIO', mimeType: 'audio/red', muted: false, sid: 'TR_A4'}, - ], - }, - ], - error: null, - }; - const checks = serverChecksFor(config, serverState); - assert.equal(checks.length, 8); - assert.deepEqual( - checks.map((check) => [check.name, check.pass]), - [ - ['server publishing API participants listed', true], - ['server sees publisher screenshare publication', true], - ['server sees publisher microphone publication', true], - ['server sees publisher screen-share audio publication', true], - ['server sees publisher camera publication', true], - ['server sees secondary publisher screenshare publication', true], - ['server sees secondary publisher microphone publication', true], - ['server sees secondary publisher screen-share audio publication', true], - ], - ); - }, - ); - }); - - test('server publishing checks fail closed on missing server tracks', async () => { - await withHarnessEnv({LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1', LIVEKIT_ENABLE_SCREEN_AUDIO: '1'}, () => { - const config = parseConfig(); - const checks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(checks, 'server sees publisher screenshare publication').pass, true); - assert.equal(findCheck(checks, 'server sees publisher screen-share audio publication').pass, false); - }); - }); - - test('server publishing checks reject wrong participant identity', async () => { - await withHarnessEnv({LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1'}, () => { - const config = parseConfig(); - const checks = serverChecksFor(config, { - participants: [ - { - identity: config.subscriberIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(checks, 'server publishing API participants listed').pass, true); - assert.equal(findCheck(checks, 'server sees publisher screenshare publication').pass, false); - }); - }); - - test('server publishing checks reject wrong kind, source, and screen mime', async () => { - await withHarnessEnv({LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1', LIVEKIT_EXPECT_SCREEN_CODEC: 'h264'}, () => { - const config = parseConfig(); - const wrongKindChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'AUDIO', source: 'SCREEN_SHARE', mimeType: 'video/H264', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(wrongKindChecks, 'server sees publisher screenshare publication').pass, false); - - const wrongSourceChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'CAMERA', mimeType: 'video/H264', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(wrongSourceChecks, 'server sees publisher screenshare publication').pass, false); - - const wrongMimeChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(wrongMimeChecks, 'server sees publisher screenshare publication').pass, false); - - const matchingChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mime_type: 'video/h264', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(matchingChecks, 'server sees publisher screenshare publication').pass, true); - }); - }); - - test('server publishing checks reject muted or missing mute state on expected tracks', async () => { - await withHarnessEnv({LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1'}, () => { - const config = parseConfig(); - const mutedChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: true}], - }, - ], - error: null, - }); - assert.equal(findCheck(mutedChecks, 'server sees publisher screenshare publication').pass, false); - - const missingMutedChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8'}], - }, - ], - error: null, - }); - assert.equal(findCheck(missingMutedChecks, 'server sees publisher screenshare publication').pass, false); - }); - }); - - test('server publishing checks handle protobuf numeric enums without accepting data tracks as video', async () => { - await withHarnessEnv({LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1'}, () => { - const config = parseConfig(); - const dataTrackChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 2, source: 3, mimeType: 'video/VP8', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(dataTrackChecks, 'server sees publisher screenshare publication').pass, false); - - const videoTrackChecks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 1, source: 3, mimeType: 'video/VP8', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(videoTrackChecks, 'server sees publisher screenshare publication').pass, true); - }); - }); - - test('server publishing checks fail closed on missing secondary, audio, camera, and screen-audio tracks', async () => { - await withHarnessEnv( - { - LIVEKIT_VALIDATE_SERVER_PUBLISHING: '1', - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_ENABLE_SCREEN_AUDIO: '1', - LIVEKIT_ENABLE_CAMERA: '1', - }, - () => { - const config = parseConfig(); - const checks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}], - }, - { - identity: config.secondaryPublisherIdentity, - tracks: [{type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}], - }, - ], - error: null, - }); - assert.equal(findCheck(checks, 'server sees publisher screenshare publication').pass, true); - assert.equal(findCheck(checks, 'server sees publisher microphone publication').pass, false); - assert.equal(findCheck(checks, 'server sees publisher screen-share audio publication').pass, false); - assert.equal(findCheck(checks, 'server sees publisher camera publication').pass, false); - assert.equal(findCheck(checks, 'server sees secondary publisher screenshare publication').pass, true); - assert.equal(findCheck(checks, 'server sees secondary publisher microphone publication').pass, false); - assert.equal(findCheck(checks, 'server sees secondary publisher screen-share audio publication').pass, false); - }, - ); - }); - - test('strict server publishing validation does not pass open on Twirp errors', async () => { - await withHarnessEnv({LIVEKIT_HARNESS_STRICT: '1'}, () => { - const config = parseConfig(); - const checks = serverChecksFor(config, { - participants: [ - { - identity: config.publisherIdentity, - tracks: [ - {type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}, - {type: 'AUDIO', source: 'MICROPHONE', mimeType: 'audio/red', muted: false}, - {type: 'AUDIO', source: 'SCREEN_SHARE_AUDIO', mimeType: 'audio/red', muted: false}, - ], - }, - { - identity: config.secondaryPublisherIdentity, - tracks: [ - {type: 'VIDEO', source: 'SCREEN_SHARE', mimeType: 'video/VP8', muted: false}, - {type: 'AUDIO', source: 'MICROPHONE', mimeType: 'audio/red', muted: false}, - {type: 'AUDIO', source: 'SCREEN_SHARE_AUDIO', mimeType: 'audio/red', muted: false}, - ], - }, - ], - error: 'RoomService.ListParticipants failed with HTTP 401', - }); - assert.equal(config.validateServerPublishing, true); - assert.equal(findCheck(checks, 'server publishing API participants listed').pass, false); - assert.equal( - checks.every((check) => check.pass), - false, - ); - }); - }); -}); diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/CMakeLists.txt b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/CMakeLists.txt deleted file mode 100644 index ed47a648e..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/CMakeLists.txt +++ /dev/null @@ -1,92 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX C) -enable_language(ASM) - -set(BINARY_NAME "h264_benchmark") - -# Set the C++ standard to C++23. -set(CMAKE_CXX_STANDARD 20) - -set(CMAKE_BUILD_TYPE Debug) - - -add_definitions(-DWEBRTC_POSIX) -add_definitions(-DWEBRTC_LINUX) -add_definitions(-DUSE_UDEV) -add_definitions(-DUSE_AURA=1) -add_definitions(-DUSE_GLIB=1) -add_definitions(-DUSE_OZONE=1) -add_definitions(-D__STDC_CONSTANT_MACROS) -add_definitions(-D__STDC_FORMAT_MACROS) -add_definitions(-D_FILE_OFFSET_BITS=64) -add_definitions(-D_LARGEFILE_SOURCE) -add_definitions(-D_LARGEFILE64_SOURCE) -add_definitions(-D_GNU_SOURCE) -add_definitions(-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_NONE) -add_definitions(-D_GLIBCXX_ASSERTIONS=1) -add_definitions(-DCR_CLANG_REVISION=\"llvmorg-19-init-8091-gab037c4f-1\") -add_definitions(-DCR_SYSROOT_KEY=20230611T210420Z-2) -add_definitions(-DDYNAMIC_ANNOTATIONS_ENABLED=1) -add_definitions(-DWEBRTC_ENABLE_PROTOBUF=0) -add_definitions(-DWEBRTC_STRICT_FIELD_TRIALS=0) -add_definitions(-DWEBRTC_INCLUDE_INTERNAL_AUDIO_DEVICE) -add_definitions(-DRTC_USE_LIBAOM_AV1_ENCODER) -add_definitions(-DRTC_ENABLE_VP9) -add_definitions(-DRTC_DAV1D_IN_INTERNAL_DECODER_FACTORY) -add_definitions(-DWEBRTC_HAVE_SCTP) -add_definitions(-DWEBRTC_USE_H264) -add_definitions(-DWEBRTC_ENABLE_LIBEVENT) -add_definitions(-DWEBRTC_LIBRARY_IMPL) -add_definitions(-DWEBRTC_ENABLE_AVX2) - -include_directories( - "${CMAKE_CURRENT_SOURCE_DIR}/../libwebrtc/linux-x64-debug/include" - "${CMAKE_CURRENT_SOURCE_DIR}/../libwebrtc/linux-x64-release/include/third_party/abseil-cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../libwebrtc/linux-x64-release/include/third_party/libyuv/include" - "${CMAKE_CURRENT_SOURCE_DIR}/../src/nvidia/NvCodec/include" - "${CMAKE_CURRENT_SOURCE_DIR}/../src/nvidia/NvCodec/NvCodec" - "${CMAKE_CURRENT_SOURCE_DIR}/../src" -) - -link_libraries( - "${CMAKE_CURRENT_SOURCE_DIR}/../libwebrtc/linux-x64-release/lib/libwebrtc.a" -) - -find_package (Threads) - -add_executable(${BINARY_NAME} - "test_main.cc" - "benchmark.cc" - "benchmark_openh264.cc" - "video_source.cc" - "fileutils.cc" - "cpu/cpu_linux.cc" - - "benchmark_nvidia.cc" - "../src/nvidia/NvCodec/NvCodec/NvDecoder/NvDecoder.cpp" - "../src/nvidia/NvCodec/NvCodec/NvEncoder/NvEncoder.cpp" - "../src/nvidia/NvCodec/NvCodec/NvEncoder/NvEncoderCuda.cpp" - "../src/nvidia/h264_encoder_impl.cpp" - "../src/nvidia/h264_decoder_impl.cpp" - "../src/nvidia/nvidia_decoder_factory.cpp" - "../src/nvidia/nvidia_encoder_factory.cpp" - "../src/nvidia/cuda_context.cpp" - "../src/nvidia/implib/libcuda.so.init.c" - "../src/nvidia/implib/libcuda.so.tramp.S" - "../src/nvidia/implib/libnvcuvid.so.init.c" - "../src/nvidia/implib/libnvcuvid.so.tramp.S" - - "benchmark_vaapi.cc" - "../src/vaapi/vaapi_display_drm.cpp" - "../src/vaapi/vaapi_h264_encoder_wrapper.cpp" - "../src/vaapi/vaapi_encoder_factory.cpp" - "../src/vaapi/h264_encoder_impl.cpp" - "../src/vaapi/implib/libva-drm.so.init.c" - "../src/vaapi/implib/libva-drm.so.tramp.S" - "../src/vaapi/implib/libva.so.init.c" - "../src/vaapi/implib/libva.so.tramp.S" -) - -target_link_libraries(${BINARY_NAME} ${CMAKE_THREAD_LIBS_INIT}) -target_link_libraries(${BINARY_NAME} dl) diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark.cc deleted file mode 100644 index b66e2ec96..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark.cc +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#include "benchmark.h" - -#include -#include -#include -#include -#if defined(_WIN32) -#include -#endif - -#include "api/video/i420_buffer.h" -#include "common_video/libyuv/include/webrtc_libyuv.h" -#include "fileutils.h" -#include "modules/video_coding/utility/simulcast_rate_allocator.h" -#include "rtc_base/event.h" -#include "video_source.h" - -#define SSIM_CALC 0 // by default, don't compute SSIM - -using namespace webrtc; - -#define EXPECT_EQ (a, b) - -FrameQueueTuple::~FrameQueueTuple() { - if (_codecSpecificInfo != NULL) { - delete _codecSpecificInfo; - } - if (_frame != NULL) { - delete _frame; - } -} - -void FrameQueue::PushFrame(VideoFrame* frame, - webrtc::CodecSpecificInfo* codecSpecificInfo) { - webrtc::MutexLock cs(&_queueRWLock); - _frameBufferQueue.push(new FrameQueueTuple(frame, codecSpecificInfo)); -} - -FrameQueueTuple* FrameQueue::PopFrame() { - webrtc::MutexLock cs(&_queueRWLock); - if (_frameBufferQueue.empty()) { - return NULL; - } - FrameQueueTuple* tuple = _frameBufferQueue.front(); - _frameBufferQueue.pop(); - return tuple; -} - -bool FrameQueue::Empty() { - webrtc::MutexLock cs(&_queueRWLock); - return _frameBufferQueue.empty(); -} - -uint32_t VideoEncodeCompleteCallback::EncodedBytes() { - return _encodedBytes; -} - -webrtc::EncodedImageCallback::Result -VideoEncodeCompleteCallback::OnEncodedImage( - const webrtc::EncodedImage& encodedImage, - const webrtc::CodecSpecificInfo* codecSpecificInfo) { - _test.UpdateEncodedBytes(encodedImage.GetEncodedData()->size()); - _encodedBytes += encodedImage.GetEncodedData()->size(); - - if (_encodedFile != NULL) { - if (fwrite(encodedImage.GetEncodedData()->data(), 1, - encodedImage.GetEncodedData()->size(), - _encodedFile) != encodedImage.GetEncodedData()->size()) { - fprintf(stderr, "Error writing to encoded file %s\n", - _test._outname.c_str()); - } - } - - return webrtc::EncodedImageCallback::Result( - webrtc::EncodedImageCallback::Result::OK); -} - -Benchmark::Benchmark() - : _resultsFileName(webrtc::test::OutputPath() + "benchmark.txt"), - _codecName("Default"), - _env(webrtc::CreateEnvironment()) {} - -Benchmark::Benchmark(std::string name, std::string description) - : _name(name), - _description(description), - _resultsFileName(webrtc::test::OutputPath() + "benchmark.txt"), - _codecName("Default"), - _env(webrtc::CreateEnvironment()) {} - -Benchmark::Benchmark(std::string name, - std::string description, - std::string resultsFileName, - std::string codecName) - : _name(name), - _description(description), - _resultsFileName(resultsFileName), - _codecName(codecName), - _cpu(webrtc::CpuWrapper::CreateCpu()), - _env(webrtc::CreateEnvironment()) {} - -void Benchmark::Perform() { - std::vector sources; - std::vector::iterator it; - - // Configuration -------------------------- - sources.push_back(new const VideoSource( - webrtc::test::ProjectRootPath() + "resources/FourPeople_1280x720_30.yuv", - kWHD)); - //sources.push_back( - // new const VideoSource(webrtc::test::ProjectRootPath() + - // "resources/Big_Buck_Bunny_1920x1080_30.yuv", - // kWFullHD)); - - const VideoSize size[] = {kWHD}; - const int frameRate[] = {30}; - // Specifies the framerates for which to perform a speed test. - const bool speedTestMask[] = {true}; - const int bitRate[] = {500, 1000, 2000, 3000, 4000}; - // Determines the number of iterations to perform to arrive at the speed - // result. - enum { kSpeedTestIterations = 8 }; - // ---------------------------------------- - - const int nFrameRates = sizeof(frameRate) / sizeof(*frameRate); - assert(sizeof(speedTestMask) / sizeof(*speedTestMask) == nFrameRates); - const int nBitrates = sizeof(bitRate) / sizeof(*bitRate); - int testIterations = 1; - - double fps[nBitrates]; - uint32_t cpuUsage[nBitrates]; - double totalEncodeTime[nBitrates]; - double totalDecodeTime[nBitrates]; - - _results.open(_resultsFileName.c_str(), std::fstream::out); - _results << GetMagicStr() << std::endl; - _results << _codecName << std::endl; - - for (it = sources.begin(); it < sources.end(); it++) { - int i = 0; - for (int j = 0; j < nFrameRates; j++) { - _target = *it; - _inname = (*it)->GetFileName(); - std::cout << (*it)->GetName() << ", " - << VideoSource::GetSizeString(size[i]) << ", " << frameRate[j] - << " fps" << ", " << _name << std::endl; - _results << (*it)->GetName() << "," << VideoSource::GetSizeString(size[i]) - << "," << frameRate[j] << " fps" << ", " << _name << std::endl - << "Bitrate [kbps]"; - - if (speedTestMask[j]) { - testIterations = kSpeedTestIterations; - } else { - testIterations = 1; - } - - for (int k = 0; k < nBitrates; k++) { - _bitRate = (bitRate[k]); - double avgFps = 0.0; - uint32_t currCpuUsage = 0; - totalEncodeTime[k] = 0; - - std::cout << "TargetBitrate [kbps]:" << " " << _bitRate << std::endl; - - for (int l = 0; l < testIterations; l++) { - PerformNormalTest(); - uint32_t cpuUsage = _cpu->CpuUsage(); - if (cpuUsage > 0) { - currCpuUsage += cpuUsage; - int coreCount = _cpu->GetNumCores(); - std::string str = "CPU Usage[%]: cores "; - str += std::to_string(coreCount); - str += ", usage " + std::to_string(cpuUsage) + "%" + - ", Test Iteration: " + std::to_string(l + 1) + "/" + - std::to_string(testIterations); - std::cout << str << std::flush; - for (int i = 0; i < str.length(); ++i) { - std::cout << "\b"; - } - } - _appendNext = false; - avgFps += _framecnt / (_totalEncodeTime); - totalEncodeTime[k] += _totalEncodeTime; - } - avgFps /= testIterations; - totalEncodeTime[k] /= testIterations; - currCpuUsage /= testIterations; - - double actualBitRate = ActualBitRate(_framecnt) / 1000.0; - std::cout << "ActualBitRate [kbps]:" << " " << actualBitRate - << std::endl; - _results << "," << actualBitRate; - fps[k] = avgFps; - cpuUsage[k] = currCpuUsage; - } - - std::cout << std::endl << "CpuUsage [%]:"; - _results << std::endl << "CpuUsage [%]"; - for (int k = 0; k < nBitrates; k++) { - std::cout << " " << cpuUsage[k] << "%"; - _results << "," << cpuUsage[k] << "%"; - } - std::cout << std::endl << "Encode Time[ms]:"; - _results << std::endl << "Encode Time[ms]"; - for (int k = 0; k < nBitrates; k++) { - std::cout << " " << totalEncodeTime[k]; - _results << "," << totalEncodeTime[k]; - } - - if (speedTestMask[j]) { - std::cout << std::endl << "Speed [fps]:"; - _results << std::endl << "Speed [fps]"; - for (int k = 0; k < nBitrates; k++) { - std::cout << " " << static_cast(fps[k] + 0.5); - _results << "," << static_cast(fps[k] + 0.5); - } - } - std::cout << std::endl << std::endl; - _results << std::endl << std::endl; - } - i++; - delete *it; - } - _results.close(); -} - -void Benchmark::PerformNormalTest() { - _encoder = GetNewEncoder(_env); - _lengthSourceFrame = _target->GetFrameLength(); - CodecSettings(_target->GetWidth(), _target->GetHeight(), - _target->GetFrameRate(), _bitRate); - Setup(); - std::unique_ptr waitEvent = std::make_unique(); - //_inputVideoBuffer.VerifyAndAllocate(_lengthSourceFrame); - _encoder->InitEncode(&_inst, 4, 1440); - CodecSpecific_InitBitrate(); - //_decoder->InitDecode(&_inst,1); - - FrameQueue frameQueue; - VideoEncodeCompleteCallback encCallback(_encodedFile, &frameQueue, *this); - - _encoder->RegisterEncodeCompleteCallback(&encCallback); - - _totalEncodeTime = _totalDecodeTime = 0; - _totalEncodePipeTime = _totalDecodePipeTime = 0; - bool complete = false; - _framecnt = 0; - _encFrameCnt = 0; - _sumEncBytes = 0; - _lengthEncFrame = 0; - while (!complete) { - complete = Encode(); - _framecnt++; - _encFrameCnt++; - /* - if (!frameQueue.Empty() || complete) { - while (!frameQueue.Empty()) { - _frameToDecode = static_cast(frameQueue.PopFrame()); - int ret = Decode(); - delete _frameToDecode; - _frameToDecode = NULL; - if (ret < 0) { - fprintf(stderr, "\n\nError in decoder: %d\n\n", ret); - exit(EXIT_FAILURE); - } else if (ret == 0) { - _framecnt++; - } else { - fprintf(stderr, "\n\nPositive return value from decode!\n\n"); - } - } - }*/ - // waitEvent->Wait(webrtc::TimeDelta::Seconds(5)); - } - - //_inputVideoBuffer.Free(); - //_encodedVideoBuffer.Free(); - //_decodedVideoBuffer.Free(); - - Teardown(); -} - -void Benchmark::Teardown() { - // Use _sourceFile as a check to prevent multiple Teardown() calls. - if (_sourceFile == NULL) { - return; - } - - _encoder->Release(); - - fclose(_sourceFile); - _sourceFile = NULL; - - delete[] _sourceBuffer; - _sourceBuffer = NULL; -} - -void Benchmark::CodecSpecific_InitBitrate() { - webrtc::SimulcastRateAllocator init_allocator(_env,_inst); - - if (_bitRate == 0) { - VideoBitrateAllocation allocation = - init_allocator.Allocate(VideoBitrateAllocationParameters( - DataRate::KilobitsPerSec(600), _inst.maxFramerate)); - _encoder->SetRates(webrtc::VideoEncoder::RateControlParameters( - allocation, _inst.maxFramerate)); - } else { - VideoBitrateAllocation allocation = - init_allocator.Allocate(VideoBitrateAllocationParameters( - DataRate::BitsPerSec(_bitRate), _inst.maxFramerate)); - _encoder->SetRates(webrtc::VideoEncoder::RateControlParameters( - allocation, _inst.maxFramerate)); - } -} - -bool Benchmark::Encode() { - _lengthEncFrame = 0; - if (_sourceBuffer == NULL) { - _sourceBuffer = new unsigned char[_lengthSourceFrame]; - } - auto size = fread(_sourceBuffer, 1, _lengthSourceFrame, _sourceFile); - if (size <= 0) { - return true; - } - // TODO: build video frame from buffer ptr. - webrtc::scoped_refptr buffer( - webrtc::I420Buffer::Create(_inst.width, _inst.height)); - - buffer->InitializeData(); - - memcpy(buffer->MutableDataY(), _sourceBuffer, _lengthSourceFrame); - - webrtc::VideoFrame inputVideoBuffer = - webrtc::VideoFrame::Builder() - .set_video_frame_buffer(buffer) - .set_rtp_timestamp( - (unsigned int)(_encFrameCnt * 9e4 / _inst.maxFramerate)) - .build(); - - if (feof(_sourceFile) != 0) { - return true; - } - _encodeCompleteTime = 0; - _encodeTimes[inputVideoBuffer.rtp_timestamp()] = tGetTime(); - std::vector frame_types(1, VideoFrameType::kVideoFrameDelta); - - // check SLI queue - _hasReceivedSLI = false; - while (!_signalSLI.empty() && _signalSLI.front().delay == 0) { - // SLI message has arrived at sender side - _hasReceivedSLI = true; - _pictureIdSLI = _signalSLI.front().id; - _signalSLI.pop_front(); - } - // decrement SLI queue times - for (std::list::iterator it = _signalSLI.begin(); - it != _signalSLI.end(); it++) { - (*it).delay--; - } - - // check PLI queue - _hasReceivedPLI = false; - while (!_signalPLI.empty() && _signalPLI.front().delay == 0) { - // PLI message has arrived at sender side - _hasReceivedPLI = true; - _signalPLI.pop_front(); - } - // decrement PLI queue times - for (std::list::iterator it = _signalPLI.begin(); - it != _signalPLI.end(); it++) { - (*it).delay--; - } - - if (_hasReceivedPLI) { - // respond to PLI by encoding a key frame - frame_types[0] = VideoFrameType::kVideoFrameKey; - _hasReceivedPLI = false; - _hasReceivedSLI = false; // don't trigger both at once - } - - int ret = _encoder->Encode(inputVideoBuffer, &frame_types); - - if (_encodeCompleteTime > 0) { - _totalEncodeTime += - _encodeCompleteTime - _encodeTimes[inputVideoBuffer.rtp_timestamp()]; - } else { - _totalEncodeTime += tGetTime() - _encodeTimes[inputVideoBuffer.rtp_timestamp()]; - } - assert(ret >= 0); - return false; -} - -webrtc::CodecSpecificInfo* Benchmark::CopyCodecSpecificInfo( - const webrtc::CodecSpecificInfo* codecSpecificInfo) const { - webrtc::CodecSpecificInfo* info = new webrtc::CodecSpecificInfo; - *info = *codecSpecificInfo; - return info; -} - -void Benchmark::Setup() { - // Use _sourceFile as a check to prevent multiple Setup() calls. - if (_sourceFile != NULL) { - return; - } - - std::stringstream ss; - std::string strTestNo; - ss << "0"; - ss >> strTestNo; - - // Check if settings exist. Otherwise use defaults. - if (_outname == "") { - _outname = - webrtc::test::OutputPath() + "out_normaltest" + strTestNo + ".yuv"; - } - - if (_codecName == "") { - _codecName = - webrtc::test::OutputPath() + "encoded_normaltest" + strTestNo + ".yuv"; - } - - if ((_sourceFile = fopen(_inname.c_str(), "rb")) == NULL) { - printf("Cannot read file %s.\n", _inname.c_str()); - exit(1); - } - - if ((_encodedFile = fopen(_codecName.c_str(), "wb")) == NULL) { - printf("Cannot write encoded file.\n"); - exit(1); - } - - char mode[3] = "wb"; - if (_appendNext) { - strncpy(mode, "ab", 3); - } - - // if ((_decodedFile = fopen(_outname.c_str(), mode)) == NULL) { - // printf("Cannot write file %s.\n", _outname.c_str()); - // exit(1); - // } - - _appendNext = true; -} diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark.h deleted file mode 100644 index 2ae2900b9..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark.h +++ /dev/null @@ -1,214 +0,0 @@ -; /* - * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef WEBRTC_MODULES_VIDEO_CODING_CODECS_TEST_FRAWEWORK_BENCHMARK_H_ -#define WEBRTC_MODULES_VIDEO_CODING_CODECS_TEST_FRAWEWORK_BENCHMARK_H_ - -#include -#include -#include -#include -#include - -#include "cpu/cpu_linux.h" -#include "api/environment/environment_factory.h" -#include "modules/include/module_common_types.h" -#include "modules/video_coding/include/video_codec_interface.h" -#include "rtc_base/synchronization/mutex.h" -#include "system_wrappers/include/clock.h" - - -class VideoSource; -class Benchmark; - -// feedback signal to encoder -struct fbSignal { - fbSignal(int d, uint8_t pid) : delay(d), id(pid) {}; - int delay; - uint8_t id; -}; - -class FrameQueueTuple { - public: - FrameQueueTuple(webrtc::VideoFrame* frame, - const webrtc::CodecSpecificInfo* codecSpecificInfo = NULL) - : _frame(frame), _codecSpecificInfo(codecSpecificInfo) {}; - ~FrameQueueTuple(); - webrtc::VideoFrame* _frame; - const webrtc::CodecSpecificInfo* _codecSpecificInfo; -}; - -class FrameQueue { - public: - FrameQueue() {} - - ~FrameQueue() {} - - void PushFrame(webrtc::VideoFrame* frame, - webrtc::CodecSpecificInfo* codecSpecificInfo = NULL); - FrameQueueTuple* PopFrame(); - bool Empty(); - - private: - webrtc::Mutex _queueRWLock; - std::queue _frameBufferQueue; -}; - -class VideoEncodeCompleteCallback : public webrtc::EncodedImageCallback { - public: - VideoEncodeCompleteCallback(FILE* encodedFile, - FrameQueue* frameQueue, - Benchmark& test) - : _encodedFile(encodedFile), - _frameQueue(frameQueue), - _test(test), - _encodedBytes(0) {} - - webrtc::EncodedImageCallback::Result OnEncodedImage( - const webrtc::EncodedImage& encoded_image, - const webrtc::CodecSpecificInfo* codec_specific_info) override; - - uint32_t EncodedBytes(); - - private: - FILE* _encodedFile; - FrameQueue* _frameQueue; - Benchmark& _test; - uint32_t _encodedBytes; -}; - -class Benchmark { - public: - friend class VideoEncodeCompleteCallback; - - public: - Benchmark(); - virtual void Perform(); - virtual bool IsSupported() = 0; - - protected: - Benchmark(std::string name, std::string description); - Benchmark(std::string name, - std::string description, - std::string resultsFileName, - std::string codecName); - virtual webrtc::VideoEncoder* GetNewEncoder(webrtc::Environment &env) = 0; - virtual void PerformNormalTest(); - virtual void CodecSpecific_InitBitrate(); - static const char* GetMagicStr() { return "#!benchmark1.0"; } - - double ActualBitRate(int nFrames) { - return 8.0 * _sumEncBytes / (nFrames / _inst.maxFramerate); - } - - webrtc::CodecSpecificInfo* CopyCodecSpecificInfo( - const webrtc::CodecSpecificInfo* codecSpecificInfo) const; - - bool Encode(); - - void Setup(); - - void Teardown(); - - void CodecSettings(int width, - int height, - uint32_t frameRate /*=30*/, - uint32_t bitRate /*=0*/) { - if (bitRate > 0) { - _bitRate = bitRate; - } else if (_bitRate == 0) { - _bitRate = 600; - } - _inst.codecType = webrtc::kVideoCodecH264; - _inst.maxFramerate = (unsigned char)frameRate; - _inst.minBitrate = (unsigned char)frameRate; - _inst.startBitrate = (int)_bitRate; - _inst.maxBitrate = 8000; - _inst.width = width; - _inst.height = height; - _inst.numberOfSimulcastStreams = 1; - _inst.simulcastStream[0].width = width; - _inst.simulcastStream[0].height = height; - _inst.simulcastStream[0].maxBitrate = 8000; - _inst.simulcastStream[0].minBitrate = _bitRate; - _inst.simulcastStream[0].targetBitrate = _bitRate; - _inst.simulcastStream[0].maxFramerate = frameRate; - _inst.simulcastStream[0].active = true; - _inst.SetScalabilityMode(webrtc::ScalabilityMode::kL1T1); - _inst.mode = webrtc::VideoCodecMode::kRealtimeVideo; - _inst.qpMax = 56; - _inst.SetFrameDropEnabled(true); - } - - double tGetTime() { - // return time in sec - return ((double)(webrtc::Clock::GetRealTimeClock()->TimeInMilliseconds()) / - 1000); - } - - virtual webrtc::CodecSpecificInfo* CreateEncoderSpecificInfo() const { - return NULL; - }; - - void UpdateEncodedBytes(int encodedBytes) { _sumEncBytes += encodedBytes; } - - const VideoSource* _target; - std::string _resultsFileName; - std::ofstream _results; - std::string _name; - std::string _description; - std::string _codecName; - std::string _inname; - std::string _outname; - webrtc::VideoEncoder* _encoder; - //webrtc::VideoDecoder* _decoder; - uint32_t _bitRate; - bool _appendNext = false; - int _framecnt; - int _encFrameCnt; - double _totalEncodeTime; - double _totalDecodeTime; - double _decodeCompleteTime; - double _encodeCompleteTime; - double _totalEncodePipeTime; - double _totalDecodePipeTime; - webrtc::VideoCodec _inst; - int _sumEncBytes; - - unsigned int _lengthSourceFrame = 0; - unsigned char* _sourceBuffer = nullptr; - - FILE* _encodedFile = nullptr; - unsigned int _lengthEncFrame = 0; - FrameQueueTuple* _frameToDecode = nullptr; - - FILE* _sourceFile = nullptr; - FILE* _decodedFile = nullptr; - - bool _hasReceivedPLI = false; - bool _waitForKey = false; - std::map _encodeTimes; - std::map _decodeTimes; - - bool _missingFrames = false; - std::list _signalSLI; - int _rttFrames = 0; - mutable bool _hasReceivedSLI = false; - mutable bool _hasReceivedRPSI = false; - uint8_t _pictureIdSLI = 0; - uint16_t _pictureIdRPSI = 0; - uint64_t _lastDecRefPictureId = 0; - uint64_t _lastDecPictureId = 0; - std::list _signalPLI; - webrtc::CpuWrapper* _cpu; - webrtc::Environment _env; -}; - -#endif // WEBRTC_MODULES_VIDEO_CODING_CODECS_TEST_FRAWEWORK_BENCHMARK_H_ diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_nvidia.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_nvidia.cc deleted file mode 100644 index 168988bfb..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_nvidia.cc +++ /dev/null @@ -1,51 +0,0 @@ -#include "benchmark_nvidia.h" - -#include "api/environment/environment_factory.h" -#include "modules/video_coding/codecs/h264/include/h264.h" -#include "fileutils.h" - -using namespace webrtc; - -NvidiaBenchmark::NvidiaBenchmark() - : Benchmark("NvidiaBenchmark", - "Nvidia benchmark over a range of test cases", - webrtc::test::OutputPath() + "NvidiaBenchmark.txt", - "nvidia_bitstream_output.h264") {} - -NvidiaBenchmark::NvidiaBenchmark(std::string name, std::string description) - : Benchmark(name, - description, - webrtc::test::OutputPath() + "NvidiaBenchmark.txt", - "nvidia_bitstream_output.h264") {} - -NvidiaBenchmark::NvidiaBenchmark(std::string name, - std::string description, - std::string resultsFileName) - : Benchmark(name, description, resultsFileName, "nvidia_bitstream_output.h264") {} - - -VideoEncoder* NvidiaBenchmark::GetNewEncoder(webrtc::Environment &env) { - if (!NvidiaVideoEncoderFactory::IsSupported()) { - fprintf(stderr, "NVIDIA is not supported on this system.\n"); - return nullptr; - } - - if (!_factory) { - _factory = std::make_unique(); - } - std::map baselineParameters = { - {"profile-level-id", "42e01f"}, - {"level-asymmetry-allowed", "1"}, - {"packetization-mode", "1"}, - }; - auto format = SdpVideoFormat("H264", baselineParameters); - - auto enc = _factory->Create(env, format); - if (!enc) { - fprintf(stderr, "Failed to create H264 encoder.\n"); - return nullptr; - } - _encoder = std::move(enc); - - return _encoder.get(); -} \ No newline at end of file diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_nvidia.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_nvidia.h deleted file mode 100644 index 3c9271743..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_nvidia.h +++ /dev/null @@ -1,24 +0,0 @@ -#include "benchmark.h" -#include "nvidia/nvidia_encoder_factory.h" - -class NvidiaBenchmark : public Benchmark { - public: - NvidiaBenchmark(); - NvidiaBenchmark(std::string name, std::string description); - NvidiaBenchmark(std::string name, - std::string description, - std::string resultsFileName); - - ~NvidiaBenchmark() {} - - bool IsSupported() override { - return webrtc::NvidiaVideoEncoderFactory::IsSupported(); - } - - protected: - webrtc::VideoEncoder* GetNewEncoder(webrtc::Environment &env) override; - - private: - std::unique_ptr _encoder; - std::unique_ptr _factory; -}; \ No newline at end of file diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_openh264.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_openh264.cc deleted file mode 100644 index 383f969f6..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_openh264.cc +++ /dev/null @@ -1,35 +0,0 @@ -#include "benchmark_openh264.h" - -#include "api/environment/environment_factory.h" -#include "modules/video_coding/codecs/h264/include/h264.h" -#include "fileutils.h" - -using namespace webrtc; - -OpenH264Benchmark::OpenH264Benchmark() - : Benchmark("OpenH264Benchmark", - "OpenH264 benchmark over a range of test cases", - webrtc::test::OutputPath() + "OpenH264Benchmark.txt", - "openh264_bitstream_output.h264") {} - -OpenH264Benchmark::OpenH264Benchmark(std::string name, std::string description) - : Benchmark(name, - description, - webrtc::test::OutputPath() + "OpenH264Benchmark.txt", - "openh264_bitstream_output.h264") {} - -OpenH264Benchmark::OpenH264Benchmark(std::string name, - std::string description, - std::string resultsFileName) - : Benchmark(name, description, resultsFileName, "openh264_bitstream_output.h264") {} - -VideoEncoder* OpenH264Benchmark::GetNewEncoder(webrtc::Environment &env) { - auto enc = CreateH264Encoder(env); - if (!enc) { - fprintf(stderr, "Failed to create H264 encoder.\n"); - return nullptr; - } - _encoder = std::move(enc); - - return _encoder.get(); -} \ No newline at end of file diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_openh264.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_openh264.h deleted file mode 100644 index f1eb1abb9..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_openh264.h +++ /dev/null @@ -1,22 +0,0 @@ -#include "benchmark.h" - -class OpenH264Benchmark : public Benchmark { - public: - OpenH264Benchmark(); - OpenH264Benchmark(std::string name, std::string description); - OpenH264Benchmark(std::string name, - std::string description, - std::string resultsFileName); - - ~OpenH264Benchmark() {} - - bool IsSupported() override { - return true; - } - - protected: - webrtc::VideoEncoder* GetNewEncoder(webrtc::Environment &env) override; - - private: - std::unique_ptr _encoder; -}; \ No newline at end of file diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_vaapi.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_vaapi.cc deleted file mode 100644 index 7c3f0971b..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_vaapi.cc +++ /dev/null @@ -1,49 +0,0 @@ -#include "benchmark_vaapi.h" - -#include "api/environment/environment_factory.h" -#include "modules/video_coding/codecs/h264/include/h264.h" -#include "fileutils.h" - -using namespace webrtc; - -VaapiBenchmark::VaapiBenchmark() - : Benchmark("VaapiBenchmark", - "VAAPI benchmark over a range of test cases", - webrtc::test::OutputPath() + "VaapiBenchmark.txt", - "vaapi_bitstream_output.h264") {} - -VaapiBenchmark::VaapiBenchmark(std::string name, std::string description) - : Benchmark(name, - description, - webrtc::test::OutputPath() + "VaapiBenchmark.txt", - "vaapi_bitstream_output.h264") {} - -VaapiBenchmark::VaapiBenchmark(std::string name, - std::string description, - std::string resultsFileName) - : Benchmark(name, description, resultsFileName, "vaapi_bitstream_output.h264") {} - -VideoEncoder* VaapiBenchmark::GetNewEncoder(webrtc::Environment &env) { - if (!VAAPIVideoEncoderFactory::IsSupported()) { - fprintf(stderr, "VAAPI is not supported on this system.\n"); - return nullptr; - } - if (!_factory) { - _factory = std::make_unique(); - } - std::map baselineParameters = { - {"profile-level-id", "4d0032"}, - {"level-asymmetry-allowed", "1"}, - {"packetization-mode", "1"}, - }; - auto format = SdpVideoFormat("H264", baselineParameters); - - auto enc = _factory->Create(env, format); - if (!enc) { - fprintf(stderr, "Failed to create H264 encoder.\n"); - return nullptr; - } - _encoder = std::move(enc); - - return _encoder.get(); -} \ No newline at end of file diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_vaapi.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_vaapi.h deleted file mode 100644 index 95ec057b9..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/benchmark_vaapi.h +++ /dev/null @@ -1,24 +0,0 @@ -#include "benchmark.h" -#include "vaapi/vaapi_encoder_factory.h" - -class VaapiBenchmark : public Benchmark { - public: - VaapiBenchmark(); - VaapiBenchmark(std::string name, std::string description); - VaapiBenchmark(std::string name, - std::string description, - std::string resultsFileName); - - ~VaapiBenchmark() {} - - bool IsSupported() override { - return webrtc::VAAPIVideoEncoderFactory::IsSupported(); - } - - protected: - webrtc::VideoEncoder* GetNewEncoder(webrtc::Environment &env) override; - - private: - std::unique_ptr _encoder; - std::unique_ptr _factory; -}; \ No newline at end of file diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_linux.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_linux.cc deleted file mode 100644 index 6d25b3d41..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_linux.cc +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#include "cpu_linux.h" - -#include -#include -#include -#include - -namespace webrtc { -CpuLinux::CpuLinux() - : m_oldBusyTime(0), - m_oldIdleTime(0), - m_oldBusyTimeMulti(NULL), - m_oldIdleTimeMulti(NULL), - m_idleArray(NULL), - m_busyArray(NULL), - m_resultArray(NULL), - m_numCores(0) { - const int result = GetNumCores(); - if (result != -1) { - m_numCores = result; - m_oldBusyTimeMulti = new long long[m_numCores]; - memset(m_oldBusyTimeMulti, 0, sizeof(long long) * m_numCores); - m_oldIdleTimeMulti = new long long[m_numCores]; - memset(m_oldIdleTimeMulti, 0, sizeof(long long) * m_numCores); - m_idleArray = new long long[m_numCores]; - memset(m_idleArray, 0, sizeof(long long) * m_numCores); - m_busyArray = new long long[m_numCores]; - memset(m_busyArray, 0, sizeof(long long) * m_numCores); - m_resultArray = new uint32_t[m_numCores]; - - GetData(m_oldBusyTime, m_oldIdleTime, m_busyArray, m_idleArray); - } -} - -CpuLinux::~CpuLinux() -{ - delete [] m_oldBusyTimeMulti; - delete [] m_oldIdleTimeMulti; - delete [] m_idleArray; - delete [] m_busyArray; - delete [] m_resultArray; -} - -int32_t CpuLinux::CpuUsage() -{ - uint32_t dummy = 0; - uint32_t* dummyArray = NULL; - return CpuUsageMultiCore(dummy, dummyArray); -} - -int32_t CpuLinux::CpuUsageMultiCore(uint32_t& numCores, - uint32_t*& coreArray) -{ - coreArray = m_resultArray; - numCores = m_numCores; - long long busy = 0; - long long idle = 0; - if (GetData(busy, idle, m_busyArray, m_idleArray) != 0) - return -1; - - long long deltaBusy = busy - m_oldBusyTime; - long long deltaIdle = idle - m_oldIdleTime; - m_oldBusyTime = busy; - m_oldIdleTime = idle; - - int retVal = -1; - if (deltaBusy + deltaIdle == 0) - { - retVal = 0; - } - else - { - retVal = (int)(100 * (deltaBusy) / (deltaBusy + deltaIdle)); - } - - if (coreArray == NULL) - { - return retVal; - } - - for (int32_t i = 0; i < m_numCores; i++) - { - deltaBusy = m_busyArray[i] - m_oldBusyTimeMulti[i]; - deltaIdle = m_idleArray[i] - m_oldIdleTimeMulti[i]; - m_oldBusyTimeMulti[i] = m_busyArray[i]; - m_oldIdleTimeMulti[i] = m_idleArray[i]; - if(deltaBusy + deltaIdle == 0) - { - coreArray[i] = 0; - } - else - { - coreArray[i] = (int)(100 * (deltaBusy) / (deltaBusy+deltaIdle)); - } - } - return retVal; -} - - -int CpuLinux::GetData(long long& busy, long long& idle, long long*& busyArray, - long long*& idleArray) -{ - FILE* fp = fopen("/proc/stat", "r"); - if (!fp) - { - return -1; - } - - char line[100]; - if (fgets(line, 100, fp) == NULL) { - fclose(fp); - return -1; - } - char firstWord[100]; - if (sscanf(line, "%s ", firstWord) != 1) { - fclose(fp); - return -1; - } - if (strncmp(firstWord, "cpu", 3) != 0) { - fclose(fp); - return -1; - } - char sUser[100]; - char sNice[100]; - char sSystem[100]; - char sIdle[100]; - if (sscanf(line, "%s %s %s %s %s ", - firstWord, sUser, sNice, sSystem, sIdle) != 5) { - fclose(fp); - return -1; - } - long long luser = atoll(sUser); - long long lnice = atoll(sNice); - long long lsystem = atoll(sSystem); - long long lidle = atoll (sIdle); - - busy = luser + lnice + lsystem; - idle = lidle; - for (int32_t i = 0; i < m_numCores; i++) - { - if (fgets(line, 100, fp) == NULL) { - fclose(fp); - return -1; - } - if (sscanf(line, "%s %s %s %s %s ", firstWord, sUser, sNice, sSystem, - sIdle) != 5) { - fclose(fp); - return -1; - } - luser = atoll(sUser); - lnice = atoll(sNice); - lsystem = atoll(sSystem); - lidle = atoll (sIdle); - busyArray[i] = luser + lnice + lsystem; - idleArray[i] = lidle; - } - fclose(fp); - return 0; -} - -int CpuLinux::GetNumCores() -{ - FILE* fp = fopen("/proc/stat", "r"); - if (!fp) - { - return -1; - } - // Skip first line - char line[100]; - if (!fgets(line, 100, fp)) - { - fclose(fp); - return -1; - } - int numCores = -1; - char firstWord[100]; - do - { - numCores++; - if (fgets(line, 100, fp)) - { - if (sscanf(line, "%s ", firstWord) != 1) { - firstWord[0] = '\0'; - } - } else { - break; - } - } while (strncmp(firstWord, "cpu", 3) == 0); - fclose(fp); - return numCores; -} - -CpuWrapper* CpuWrapper::CreateCpu() -{ - return new CpuLinux(); -} - -} // namespace webrtc diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_linux.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_linux.h deleted file mode 100644 index 369cc31b0..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_linux.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef WEBRTC_SYSTEM_WRAPPERS_SOURCE_CPU_LINUX_H_ -#define WEBRTC_SYSTEM_WRAPPERS_SOURCE_CPU_LINUX_H_ - -#include "cpu_wrapper.h" - -namespace webrtc { -class CpuLinux : public CpuWrapper { - public: - CpuLinux(); - virtual ~CpuLinux(); - - int32_t CpuUsage() override; - int32_t CpuUsage(int8_t* pProcessName, uint32_t length) override { return 0; } - int32_t CpuUsage(uint32_t dwProcessID) override { return 0; } - - int32_t CpuUsageMultiCore(uint32_t& numCores, uint32_t*& array) override; - - void Reset() override { return; } - void Stop() override { return; } - - int GetNumCores() override; - - private: - int GetData(long long& busy, - long long& idle, - long long*& busyArray, - long long*& idleArray); - - - long long m_oldBusyTime; - long long m_oldIdleTime; - - long long* m_oldBusyTimeMulti; - long long* m_oldIdleTimeMulti; - - long long* m_idleArray; - long long* m_busyArray; - uint32_t* m_resultArray; - uint32_t m_numCores; -}; -} // namespace webrtc - -#endif // WEBRTC_SYSTEM_WRAPPERS_SOURCE_CPU_LINUX_H_ diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_wrapper.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_wrapper.h deleted file mode 100644 index df3c20953..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/cpu/cpu_wrapper.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef WEBRTC_SYSTEM_WRAPPERS_INTERFACE_CPU_WRAPPER_H_ -#define WEBRTC_SYSTEM_WRAPPERS_INTERFACE_CPU_WRAPPER_H_ - -#include - -namespace webrtc { -class CpuWrapper -{ -public: - static CpuWrapper* CreateCpu(); - virtual ~CpuWrapper() {} - - // Returns the average CPU usage for all processors. The CPU usage can be - // between and including 0 to 100 (%) - virtual int32_t CpuUsage() = 0; - virtual int32_t CpuUsage(int8_t* processName, - uint32_t length) = 0; - virtual int32_t CpuUsage(uint32_t dwProcessID) = 0; - - // The CPU usage per core is returned in cpu_usage. The CPU can be between - // and including 0 to 100 (%) - // Note that the pointer passed as cpu_usage is redirected to a local member - // of the CPU wrapper. - // numCores is the number of cores in the cpu_usage array. - // The return value is -1 for failure or 0-100, indicating the average - // CPU usage across all cores. - // Note: on some OSs this class is initialized lazy. This means that it - // might not yet be possible to retrieve any CPU metrics. When this happens - // the return value will be zero (indicating that there is not a failure), - // numCores will be 0 and cpu_usage will be set to NULL (indicating that - // no metrics are available yet). Once the initialization is completed, - // which can take in the order of seconds, CPU metrics can be retrieved. - virtual int32_t CpuUsageMultiCore(uint32_t& numCores, - uint32_t*& cpu_usage) = 0; - - virtual void Reset() = 0; - virtual void Stop() = 0; - - virtual int GetNumCores() = 0; - -protected: - CpuWrapper() {} -}; -} // namespace webrtc -#endif // WEBRTC_SYSTEM_WRAPPERS_INTERFACE_CPU_WRAPPER_H_ diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/fileutils.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/fileutils.cc deleted file mode 100644 index 4e49980e5..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/fileutils.cc +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#include "fileutils.h" - -#ifdef WIN32 -#include -#define GET_CURRENT_DIR _getcwd -#else -#include -#define GET_CURRENT_DIR getcwd -#endif - -#include // To check for directory existence. -#ifndef S_ISDIR // Not defined in stat.h on Windows. -#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) -#endif - -#include -#include - - -namespace webrtc { -namespace test { - -#ifdef WIN32 -static const char* kPathDelimiter = "\\"; -#else -static const char* kPathDelimiter = "/"; -#endif - -#ifdef WEBRTC_ANDROID -static const char* kRootDirName = "/sdcard/"; -static const char* kResourcesDirName = "resources"; -#else -// The file we're looking for to identify the project root dir. -static const char* kProjectRootFileName = "h264_benchmark"; -static const char* kOutputDirName = "out"; -static const char* kFallbackPath = "./"; -static const char* kResourcesDirName = "resources"; -#endif -const char* kCannotFindProjectRootDir = "ERROR_CANNOT_FIND_PROJECT_ROOT_DIR"; - -namespace { -char relative_dir_path[FILENAME_MAX]; -bool relative_dir_path_set = false; -} - -void SetExecutablePath(const std::string& path) { - std::string working_dir = WorkingDir(); - std::string temp_path = path; - - // Handle absolute paths; convert them to relative paths to the working dir. - if (path.find(working_dir) != std::string::npos) { - temp_path = path.substr(working_dir.length() + 1); - } - // Trim away the executable name; only store the relative dir path. - temp_path = temp_path.substr(0, temp_path.find_last_of(kPathDelimiter)); - strncpy(relative_dir_path, temp_path.c_str(), FILENAME_MAX); - relative_dir_path_set = true; -} - -bool FileExists(std::string& file_name) { - struct stat file_info = {0}; - return stat(file_name.c_str(), &file_info) == 0; -} - -#ifdef WEBRTC_ANDROID - -std::string ProjectRootPath() { - return kRootDirName; -} - -std::string OutputPath() { - return kRootDirName; -} - -std::string WorkingDir() { - return kRootDirName; -} - -#else // WEBRTC_ANDROID - -std::string ProjectRootPath() { - std::string path = WorkingDir(); - if (path == kFallbackPath) { - return kCannotFindProjectRootDir; - } - if (relative_dir_path_set) { - path = path + kPathDelimiter + relative_dir_path; - } - // Check for our file that verifies the root dir. - size_t path_delimiter_index = path.find_last_of(kPathDelimiter); - while (path_delimiter_index != std::string::npos) { - std::string root_filename = path + kPathDelimiter + kProjectRootFileName; - if (FileExists(root_filename)) { - return path + kPathDelimiter; - } - // Move up one directory in the directory tree. - path = path.substr(0, path_delimiter_index); - path_delimiter_index = path.find_last_of(kPathDelimiter); - } - // Reached the root directory. - fprintf(stderr, "Cannot find project root directory!\n"); - return kCannotFindProjectRootDir; -} - -std::string OutputPath() { - std::string path = ProjectRootPath(); - if (path == kCannotFindProjectRootDir) { - return kFallbackPath; - } - path += kOutputDirName; - if (!CreateDirectory(path)) { - return kFallbackPath; - } - return path + kPathDelimiter; -} - -std::string WorkingDir() { - char path_buffer[FILENAME_MAX]; - if (!GET_CURRENT_DIR(path_buffer, sizeof(path_buffer))) { - fprintf(stderr, "Cannot get current directory!\n"); - return kFallbackPath; - } else { - return std::string(path_buffer); - } -} - -#endif // !WEBRTC_ANDROID - -bool CreateDirectory(std::string directory_name) { - struct stat path_info = {0}; - // Check if the path exists already: - if (stat(directory_name.c_str(), &path_info) == 0) { - if (!S_ISDIR(path_info.st_mode)) { - fprintf(stderr, "Path %s exists but is not a directory! Remove this " - "file and re-run to create the directory.\n", - directory_name.c_str()); - return false; - } - } else { -#ifdef WIN32 - return _mkdir(directory_name.c_str()) == 0; -#else - return mkdir(directory_name.c_str(), S_IRWXU | S_IRWXG | S_IRWXO) == 0; -#endif - } - return true; -} - -std::string ResourcePath(std::string name, std::string extension) { - std::string platform = "win"; -#ifdef WEBRTC_LINUX - platform = "linux"; -#endif // WEBRTC_LINUX -#ifdef WEBRTC_MAC - platform = "mac"; -#endif // WEBRTC_MAC - -#ifdef WEBRTC_ARCH_64_BITS - std::string architecture = "64"; -#else - std::string architecture = "32"; -#endif // WEBRTC_ARCH_64_BITS - - std::string resources_path = ProjectRootPath() + kResourcesDirName + - kPathDelimiter; - std::string resource_file = resources_path + name + "_" + platform + "_" + - architecture + "." + extension; - if (FileExists(resource_file)) { - return resource_file; - } - // Try without architecture. - resource_file = resources_path + name + "_" + platform + "." + extension; - if (FileExists(resource_file)) { - return resource_file; - } - // Try without platform. - resource_file = resources_path + name + "_" + architecture + "." + extension; - if (FileExists(resource_file)) { - return resource_file; - } - - // Fall back on name without architecture or platform. - return resources_path + name + "." + extension; -} - -size_t GetFileSize(std::string filename) { - FILE* f = fopen(filename.c_str(), "rb"); - size_t size = 0; - if (f != NULL) { - if (fseek(f, 0, SEEK_END) == 0) { - size = ftell(f); - } - fclose(f); - } - return size; -} - -} // namespace test -} // namespace webrtc diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/fileutils.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/fileutils.h deleted file mode 100644 index e642a5f03..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/fileutils.h +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#include - -// File utilities for testing purposes. -// -// The ProjectRootPath() method is a convenient way of getting an absolute -// path to the project source tree root directory. Using this, it is easy to -// refer to test resource files in a portable way. -// -// Notice that even if Windows platforms use backslash as path delimiter, it is -// also supported to use slash, so there's no need for #ifdef checks in test -// code for setting up the paths to the resource files. -// -// Example use: -// Assume we have the following code being used in a test source file: -// const std::string kInputFile = webrtc::test::ProjectRootPath() + -// "test/data/voice_engine/audio_long16.wav"; -// // Use the kInputFile for the tests... -// -// Then here's some example outputs for different platforms: -// Linux: -// * Source tree located in /home/user/webrtc/trunk -// * Test project located in /home/user/webrtc/trunk/src/testproject -// * Test binary compiled as: -// /home/user/webrtc/trunk/out/Debug/testproject_unittests -// Then ProjectRootPath() will return /home/user/webrtc/trunk/ no matter if -// the test binary is executed from standing in either of: -// /home/user/webrtc/trunk -// or -// /home/user/webrtc/trunk/out/Debug -// (or any other directory below the trunk for that matter). -// -// Windows: -// * Source tree located in C:\Users\user\webrtc\trunk -// * Test project located in C:\Users\user\webrtc\trunk\src\testproject -// * Test binary compiled as: -// C:\Users\user\webrtc\trunk\src\testproject\Debug\testproject_unittests.exe -// Then ProjectRootPath() will return C:\Users\user\webrtc\trunk\ when the -// test binary is executed from inside Visual Studio. -// It will also return the same path if the test is executed from a command -// prompt standing in C:\Users\user\webrtc\trunk\src\testproject\Debug -// -// Mac: -// * Source tree located in /Users/user/webrtc/trunk -// * Test project located in /Users/user/webrtc/trunk/src/testproject -// * Test binary compiled as: -// /Users/user/webrtc/trunk/xcodebuild/Debug/testproject_unittests -// Then ProjectRootPath() will return /Users/user/webrtc/trunk/ no matter if -// the test binary is executed from standing in either of: -// /Users/user/webrtc/trunk -// or -// /Users/user/webrtc/trunk/out/Debug -// (or any other directory below the trunk for that matter). - -#ifndef WEBRTC_TEST_TESTSUPPORT_FILEUTILS_H_ -#define WEBRTC_TEST_TESTSUPPORT_FILEUTILS_H_ - -#include - -namespace webrtc { -namespace test { - -// This is the "directory" returned if the ProjectPath() function fails -// to find the project root. -extern const char* kCannotFindProjectRootDir; - -// Finds the root dir of the project, to be able to set correct paths to -// resource files used by tests. -// The implementation is simple: it just looks for the file defined by -// kProjectRootFileName, starting in the current directory (the working -// directory) and then steps upward until it is found (or it is at the root of -// the file system). -// If the current working directory is above the project root dir, it will not -// be found. -// -// If symbolic links occur in the path they will be resolved and the actual -// directory will be returned. -// -// Returns the absolute path to the project root dir (usually the trunk dir) -// WITH a trailing path delimiter. -// If the project root is not found, the string specified by -// kCannotFindProjectRootDir is returned. -std::string ProjectRootPath(); - -// Creates and returns the absolute path to the output directory where log files -// and other test artifacts should be put. The output directory is generally a -// directory named "out" at the top-level of the project, i.e. a subfolder to -// the path returned by ProjectRootPath(). The exception is Android where we use -// /sdcard/ instead. -// -// Details described for ProjectRootPath() apply here too. -// -// Returns the path WITH a trailing path delimiter. If the project root is not -// found, the current working directory ("./") is returned as a fallback. -std::string OutputPath(); - -// Returns a path to a resource file for the currently executing platform. -// Adapts to what filenames are currently present in the -// [project-root]/resources/ dir. -// Returns an absolute path according to this priority list (the directory -// part of the path is left out for readability): -// 1. [name]_[platform]_[architecture].[extension] -// 2. [name]_[platform].[extension] -// 3. [name]_[architecture].[extension] -// 4. [name].[extension] -// Where -// * platform is either of "win", "mac" or "linux". -// * architecture is either of "32" or "64". -// -// Arguments: -// name - Name of the resource file. If a plain filename (no directory path) -// is supplied, the file is assumed to be located in resources/ -// If a directory path is prepended to the filename, a subdirectory -// hierarchy reflecting that path is assumed to be present. -// extension - File extension, without the dot, i.e. "bmp" or "yuv". -std::string ResourcePath(std::string name, std::string extension); - -// Gets the current working directory for the executing program. -// Returns "./" if for some reason it is not possible to find the working -// directory. -std::string WorkingDir(); - -// Creates a directory if it not already exists. -// Returns true if successful. Will print an error message to stderr and return -// false if a file with the same name already exists. -bool CreateDirectory(std::string directory_name); - -// File size of the supplied file in bytes. Will return 0 if the file is -// empty or if the file does not exist/is readable. -size_t GetFileSize(std::string filename); - -// Sets the executable path, i.e. the path to the executable that is being used -// when launching it. This is usually the path relative to the working directory -// but can also be an absolute path. The intention with this function is to pass -// the argv[0] being sent into the main function to make it possible for -// fileutils.h to find the correct project paths even when the working directory -// is outside the project tree (which happens in some cases). -void SetExecutablePath(const std::string& path_to_executable); - -} // namespace test -} // namespace webrtc - -#endif // WEBRTC_TEST_TESTSUPPORT_FILEUTILS_H_ diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/test_main.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/test_main.cc deleted file mode 100644 index 6b8209f6d..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/test_main.cc +++ /dev/null @@ -1,20 +0,0 @@ -#include "benchmark_nvidia.h" -#include "benchmark_openh264.h" -#include "benchmark_vaapi.h" -#include "stdio.h" - -int main(int argc, char** argv) { - - std::vector benchmarks; - benchmarks.push_back(new NvidiaBenchmark()); - //benchmarks.push_back(new VaapiBenchmark()); - benchmarks.push_back(new OpenH264Benchmark()); - - for (auto benchmark : benchmarks) { - if (benchmark->IsSupported()) { - benchmark->Perform(); - } - } - - return 0; -} diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/video_source.cc b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/video_source.cc deleted file mode 100644 index 8273c4d85..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/video_source.cc +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#include "video_source.h" - -#include - -#include "fileutils.h" - -#define ASSERT_TRUE(condition) \ - do { \ - if (!(condition)) { \ - fprintf(stderr, "Assertion failed: %s\n", #condition); \ - abort(); \ - } \ - } while (0) - -VideoSource::VideoSource() -: -_fileName(webrtc::test::ProjectRootPath() + "resources/foreman_cif.yuv"), -_width(352), -_height(288), -_type(webrtc::VideoType::kI420), -_frameRate(30) -{ -} - -VideoSource::VideoSource(std::string fileName, VideoSize size, - int frameRate /*= 30*/, webrtc::VideoType type /*= webrtc::kI420*/) -: -_fileName(fileName), -_type(type), -_frameRate(frameRate) -{ - assert(size != kUndefined && size != kNumberOfVideoSizes); - assert(type != webrtc::VideoType::kUnknown); - assert(frameRate > 0); - if (GetWidthHeight(size, _width, _height) != 0) { - assert(false); - } -} - -VideoSource::VideoSource(std::string fileName, int width, int height, - int frameRate /*= 30*/, webrtc::VideoType type /*= webrtc::kI420*/) -: -_fileName(fileName), -_width(width), -_height(height), -_type(type), -_frameRate(frameRate) -{ - assert(width > 0); - assert(height > 0); - assert(type != webrtc::VideoType::kUnknown); - assert(frameRate > 0); -} - -VideoSize -VideoSource::GetSize() const -{ - return GetSize(_width, _height); -} - -VideoSize -VideoSource::GetSize(uint16_t width, uint16_t height) -{ - if(width == 128 && height == 96) - { - return kSQCIF; - }else if(width == 160 && height == 120) - { - return kQQVGA; - }else if(width == 176 && height == 144) - { - return kQCIF; - }else if(width == 320 && height == 240) - { - return kQVGA; - }else if(width == 352 && height == 288) - { - return kCIF; - }else if(width == 640 && height == 480) - { - return kVGA; - }else if(width == 720 && height == 480) - { - return kNTSC; - }else if(width == 704 && height == 576) - { - return k4CIF; - }else if(width == 800 && height == 600) - { - return kSVGA; - }else if(width == 960 && height == 720) - { - return kHD; - }else if(width == 1024 && height == 768) - { - return kXGA; - }else if(width == 1440 && height == 1080) - { - return kFullHD; - }else if(width == 400 && height == 240) - { - return kWQVGA; - }else if(width == 800 && height == 480) - { - return kWVGA; - }else if(width == 1280 && height == 720) - { - return kWHD; - }else if(width == 1920 && height == 1080) - { - return kWFullHD; - } - return kUndefined; -} - -unsigned int -VideoSource::GetFrameLength() const -{ - return webrtc::CalcBufferSize(_type, _width, _height); -} - -const char* -VideoSource::GetMySizeString() const -{ - return VideoSource::GetSizeString(GetSize()); -} - -const char* -VideoSource::GetSizeString(VideoSize size) -{ - switch (size) - { - case kSQCIF: - return "SQCIF"; - case kQQVGA: - return "QQVGA"; - case kQCIF: - return "QCIF"; - case kQVGA: - return "QVGA"; - case kCIF: - return "CIF"; - case kVGA: - return "VGA"; - case kNTSC: - return "NTSC"; - case k4CIF: - return "4CIF"; - case kSVGA: - return "SVGA"; - case kHD: - return "HD"; - case kXGA: - return "XGA"; - case kFullHD: - return "Full_HD"; - case kWQVGA: - return "WQVGA"; - case kWHD: - return "WHD"; - case kWFullHD: - return "WFull_HD"; - default: - return "Undefined"; - } -} - -std::string -VideoSource::GetFilePath() const -{ - size_t slashPos = _fileName.find_last_of("/\\"); - if (slashPos == std::string::npos) - { - return "."; - } - - return _fileName.substr(0, slashPos); -} - -std::string -VideoSource::GetName() const -{ - // Remove path. - size_t slashPos = _fileName.find_last_of("/\\"); - if (slashPos == std::string::npos) - { - slashPos = 0; - } - else - { - slashPos++; - } - - // Remove extension and underscored suffix if it exists. - return _fileName.substr(slashPos, std::min(_fileName.find_last_of("_"), - _fileName.find_last_of(".")) - slashPos); -} - -void -VideoSource::Convert(const VideoSource &target, bool force /* = false */) const -{ - // Ensure target rate is less than or equal to source - // (i.e. we are only temporally downsampling). - ASSERT_TRUE(target.GetFrameRate() <= _frameRate); - // Only supports YUV420 currently. - ASSERT_TRUE(_type == webrtc::VideoType::kI420 && target.GetType() == webrtc::VideoType::kI420); - if (!force && (FileExists(target.GetFileName().c_str()) || - (target.GetWidth() == _width && target.GetHeight() == _height && target.GetFrameRate() == _frameRate))) - { - // Assume that the filename uniquely defines the content. - // If the file already exists, it is the correct file. - return; - } - FILE *inFile = NULL; - FILE *outFile = NULL; - - inFile = fopen(_fileName.c_str(), "rb"); - ASSERT_TRUE(inFile != NULL); - - outFile = fopen(target.GetFileName().c_str(), "wb"); - ASSERT_TRUE(outFile != NULL); - - FrameDropper fd; - fd.SetFrameRate(target.GetFrameRate(), _frameRate); - - const size_t lengthOutFrame = webrtc::CalcBufferSize(target.GetType(), - target.GetWidth(), target.GetHeight()); - ASSERT_TRUE(lengthOutFrame > 0); - unsigned char *outFrame = new unsigned char[lengthOutFrame]; - - const size_t lengthInFrame = webrtc::CalcBufferSize(_type, _width, _height); - ASSERT_TRUE(lengthInFrame > 0); - unsigned char *inFrame = new unsigned char[lengthInFrame]; - - while (fread(inFrame, 1, lengthInFrame, inFile) == lengthInFrame) - { - if (!fd.DropFrame()) - { - ASSERT_TRUE(target.GetWidth() == _width && - target.GetHeight() == _height); - // Add video interpolator here! - if (fwrite(outFrame, 1, lengthOutFrame, - outFile) != lengthOutFrame) { - return; - } - } - } - - delete inFrame; - delete outFrame; - fclose(inFile); - fclose(outFile); -} - -bool VideoSource::FileExists(const char* fileName) -{ - FILE* fp = NULL; - fp = fopen(fileName, "rb"); - if(fp != NULL) - { - fclose(fp); - return true; - } - return false; -} - - -int -VideoSource::GetWidthHeight( VideoSize size, int & width, int& height) -{ - switch(size) - { - case kSQCIF: - width = 128; - height = 96; - return 0; - case kQQVGA: - width = 160; - height = 120; - return 0; - case kQCIF: - width = 176; - height = 144; - return 0; - case kCGA: - width = 320; - height = 200; - return 0; - case kQVGA: - width = 320; - height = 240; - return 0; - case kSIF: - width = 352; - height = 240; - return 0; - case kWQVGA: - width = 400; - height = 240; - return 0; - case kCIF: - width = 352; - height = 288; - return 0; - case kW288p: - width = 512; - height = 288; - return 0; - case k448p: - width = 576; - height = 448; - return 0; - case kVGA: - width = 640; - height = 480; - return 0; - case k432p: - width = 720; - height = 432; - return 0; - case kW432p: - width = 768; - height = 432; - return 0; - case k4SIF: - width = 704; - height = 480; - return 0; - case kW448p: - width = 768; - height = 448; - return 0; - case kNTSC: - width = 720; - height = 480; - return 0; - case kFW448p: - width = 800; - height = 448; - return 0; - case kWVGA: - width = 800; - height = 480; - return 0; - case k4CIF: - width = 704; - height = 576; - return 0; - case kSVGA: - width = 800; - height = 600; - return 0; - case kW544p: - width = 960; - height = 544; - return 0; - case kW576p: - width = 1024; - height = 576; - return 0; - case kHD: - width = 960; - height = 720; - return 0; - case kXGA: - width = 1024; - height = 768; - return 0; - case kFullHD: - width = 1440; - height = 1080; - return 0; - case kWHD: - width = 1280; - height = 720; - return 0; - case kWFullHD: - width = 1920; - height = 1080; - return 0; - default: - return -1; - } -} - -FrameDropper::FrameDropper() -: -_dropsBetweenRenders(0), -_frameCounter(0) -{ -} - -bool -FrameDropper::DropFrame() -{ - _frameCounter++; - if (_frameCounter > _dropsBetweenRenders) - { - _frameCounter = 0; - return false; - } - return true; -} - -unsigned int -FrameDropper::DropsBetweenRenders() -{ - return _dropsBetweenRenders; -} - -void -FrameDropper::SetFrameRate(double frameRate, double maxFrameRate) -{ - if (frameRate >= 1.0) - { - _dropsBetweenRenders = static_cast(maxFrameRate / frameRate + 0.5) - 1; - } - else - { - _dropsBetweenRenders = 0; - } -} diff --git a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/video_source.h b/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/video_source.h deleted file mode 100644 index c9da346fc..000000000 --- a/fluxer_desktop/native/webrtc-sender/vendor/webrtc-sys/test/video_source.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -#ifndef WEBRTC_MODULES_VIDEO_CODING_CODECS_TEST_FRAMEWORK_VIDEO_SOURCE_H_ -#define WEBRTC_MODULES_VIDEO_CODING_CODECS_TEST_FRAMEWORK_VIDEO_SOURCE_H_ - -#include -#include "common_video/libyuv/include/webrtc_libyuv.h" - -enum VideoSize - { - kUndefined, - kSQCIF, // 128*96 = 12 288 - kQQVGA, // 160*120 = 19 200 - kQCIF, // 176*144 = 25 344 - kCGA, // 320*200 = 64 000 - kQVGA, // 320*240 = 76 800 - kSIF, // 352*240 = 84 480 - kWQVGA, // 400*240 = 96 000 - kCIF, // 352*288 = 101 376 - kW288p, // 512*288 = 147 456 (WCIF) - k448p, // 576*448 = 281 088 - kVGA, // 640*480 = 307 200 - k432p, // 720*432 = 311 040 - kW432p, // 768*432 = 331 776 - k4SIF, // 704*480 = 337 920 - kW448p, // 768*448 = 344 064 - kNTSC, // 720*480 = 345 600 - kFW448p, // 800*448 = 358 400 - kWVGA, // 800*480 = 384 000 - k4CIF, // 704�576 = 405 504 - kSVGA, // 800*600 = 480 000 - kW544p, // 960*544 = 522 240 - kW576p, // 1024*576 = 589 824 (W4CIF) - kHD, // 960*720 = 691 200 - kXGA, // 1024*768 = 786 432 - kWHD, // 1280*720 = 921 600 - kFullHD, // 1440*1080 = 1 555 200 - kWFullHD, // 1920*1080 = 2 073 600 - - kNumberOfVideoSizes - }; - -class VideoSource -{ -public: - VideoSource(); - VideoSource(std::string fileName, VideoSize size, int frameRate = 30, - webrtc::VideoType type = webrtc::VideoType::kI420); - VideoSource(std::string fileName, int width, int height, int frameRate = 30, - webrtc::VideoType type = webrtc::VideoType::kI420); - - std::string GetFileName() const { return _fileName; } - int GetWidth() const { return _width; } - int GetHeight() const { return _height; } - webrtc::VideoType GetType() const { return _type; } - int GetFrameRate() const { return _frameRate; } - - // Returns the file path without a trailing slash. - std::string GetFilePath() const; - - // Returns the filename with the path (including the leading slash) removed. - std::string GetName() const; - - VideoSize GetSize() const; - static VideoSize GetSize(uint16_t width, uint16_t height); - unsigned int GetFrameLength() const; - - // Returns a human-readable size string. - static const char* GetSizeString(VideoSize size); - const char* GetMySizeString() const; - - // Opens the video source, converting and writing to the specified target. - // If force is true, the conversion will be done even if the target file - // already exists. - void Convert(const VideoSource& target, bool force = false) const; - static bool FileExists(const char* fileName); -private: - static int GetWidthHeight( VideoSize size, int& width, int& height); - std::string _fileName; - int _width; - int _height; - webrtc::VideoType _type; - int _frameRate; -}; - -class FrameDropper -{ -public: - FrameDropper(); - bool DropFrame(); - unsigned int DropsBetweenRenders(); - void SetFrameRate(double frameRate, double maxFrameRate); - -private: - unsigned int _dropsBetweenRenders; - unsigned int _frameCounter; -}; - - -#endif // WEBRTC_MODULES_VIDEO_CODING_CODECS_TEST_FRAMEWORK_VIDEO_SOURCE_H_ - diff --git a/fluxer_desktop/native/win-game-capture/index.test.mjs b/fluxer_desktop/native/win-game-capture/index.test.mjs index 6fa4b833f..b037eb43c 100644 --- a/fluxer_desktop/native/win-game-capture/index.test.mjs +++ b/fluxer_desktop/native/win-game-capture/index.test.mjs @@ -1,20 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import assert from 'node:assert/strict'; -import {existsSync} from 'node:fs'; import {createRequire} from 'node:module'; import {afterEach, describe, test} from 'node:test'; const require = createRequire(import.meta.url); const winGameCapture = require('./index.js'); -const startupSupported = winGameCapture.isSupported(); -const startupAvailability = winGameCapture.getAvailability(); -const startupHookAvailable = winGameCapture.isGameCaptureHookAvailable(); -const realBindingSkip = startupSupported - ? false - : 'no native binding loaded at startup (binding-less platform / unbuilt addon)'; - const hasBindingHook = typeof winGameCapture.__setBindingForTests === 'function'; const injectionSkip = hasBindingHook ? false : 'no __setBindingForTests hook exported'; @@ -292,38 +284,6 @@ describe('win-game-capture loader wrapper -- arch path resolvers (platform-porta }); }); -describe('win-game-capture loader wrapper -- real native binding (built Windows box)', () => { - test('getAvailability() reports available with the windows-game-capture backend', {skip: realBindingSkip}, () => { - assert.equal(startupAvailability.available, true); - assert.equal(startupAvailability.backend, 'windows-game-capture'); - }); - - test('resolveGameHookPath() points at an existing host-arch hook DLL', {skip: realBindingSkip}, () => { - const r = winGameCapture.resolveGameHookPath(); - assert.equal(typeof r, 'string', 'expected a hook path on a built Windows box'); - assert.ok(existsSync(r), `hook DLL should exist on disk: ${r}`); - }); - - test('resolveVulkanLayerManifestPath() points at an existing layer manifest', {skip: realBindingSkip}, () => { - const r = winGameCapture.resolveVulkanLayerManifestPath(); - assert.equal(typeof r, 'string', 'expected a Vulkan layer manifest path on a built Windows box'); - assert.ok(existsSync(r), `Vulkan layer manifest should exist on disk: ${r}`); - }); - - test('the native binding reports hook-based game capture as unavailable', {skip: realBindingSkip}, () => { - assert.equal(startupHookAvailable, false, 'hook capture is force-disabled in the native crate'); - }); - - test('requiring the module leaves the Vulkan implicit layer unregistered', {skip: realBindingSkip}, () => { - assert.equal( - winGameCapture.registerVulkanLayerManifest(), - false, - 'the Vulkan implicit layer must not be registered while hook capture is disabled', - ); - assert.equal(winGameCapture.getVulkanLayerRegistrationState().registered, false); - }); -}); - describe('win-game-capture loader wrapper -- injected fake binding', () => { test('isGameCaptureHookAvailable() follows the native hook flag', {skip: injectionSkip}, () => { const {binding} = makeFakeBinding(); diff --git a/fluxer_desktop/native/win-game-capture/package.json b/fluxer_desktop/native/win-game-capture/package.json index d6c8b5457..27a7e5cbd 100644 --- a/fluxer_desktop/native/win-game-capture/package.json +++ b/fluxer_desktop/native/win-game-capture/package.json @@ -35,7 +35,6 @@ "scripts": { "build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon", "test": "cargo test --manifest-path Cargo.toml && node --test index.test.mjs", - "test:fixtures": "node scripts/fixture-smoke.mjs", "test:loader": "node --test index.test.mjs" } } diff --git a/fluxer_desktop/native/win-game-capture/scripts/fixture-smoke.mjs b/fluxer_desktop/native/win-game-capture/scripts/fixture-smoke.mjs deleted file mode 100644 index 300442957..000000000 --- a/fluxer_desktop/native/win-game-capture/scripts/fixture-smoke.mjs +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env node -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {spawn} from 'node:child_process'; -import {existsSync} from 'node:fs'; -import {createRequire} from 'node:module'; -import {dirname, join} from 'node:path'; -import {setTimeout as delay} from 'node:timers/promises'; -import {fileURLToPath} from 'node:url'; - -const require = createRequire(import.meta.url); -const capture = require('../index.js'); - -const ROOT = dirname(dirname(fileURLToPath(import.meta.url))); -const DEFAULT_FIXTURES = ['d3d11-present-fixture']; -const ALL_FIXTURES = [ - 'd3d9-present-fixture', - 'd3d10-present-fixture', - 'd3d11-present-fixture', - 'd3d12-present-fixture', - 'opengl-swapbuffers-fixture', - 'vulkan-present-fixture', -]; -const WIDTH = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_WIDTH ?? '640', 10); -const HEIGHT = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_HEIGHT ?? '360', 10); -const FRAME_RATE = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_FPS ?? '30', 10); -const START_TIMEOUT_MS = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_START_TIMEOUT_MS ?? '15000', 10); -const FRAME_TIMEOUT_MS = Number.parseInt(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_FRAME_TIMEOUT_MS ?? '15000', 10); - -const MIN_OBSERVED_FRAMES = 3; - -const API_NONE = 0; -const TRANSPORT_MEMORY = 0; -const FALLBACK_NONE = 0; -const DXGI_FORMAT_UNKNOWN = 0; -const EXPECTED_ACTIVE_STRATEGY = 'wgc'; - -const EXPECTED_DIAGNOSTICS = { - apiType: API_NONE, - transport: TRANSPORT_MEMORY, - fallbackReason: FALLBACK_NONE, - dxgiFormat: DXGI_FORMAT_UNKNOWN, -}; - -function envFlag(name) { - return /^(1|true|yes|on)$/i.test(process.env[name] ?? ''); -} - -function selectedFixtures() { - const raw = process.argv.slice(2).join(',') || process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURES || ''; - if (!raw.trim()) return DEFAULT_FIXTURES; - const names = raw - .split(',') - .map((name) => name.trim()) - .filter(Boolean); - return names.flatMap((name) => (name === 'all' ? ALL_FIXTURES : [name])); -} - -function fixtureTarget(fixture) { - return fixture === 'i686-present-fixture' ? 'i686-pc-windows-msvc' : null; -} - -function fixtureExePath(fixture) { - const target = fixtureTarget(fixture); - const targetDir = target - ? join(ROOT, 'test-apps', fixture, 'target', target, 'release') - : join(ROOT, 'test-apps', fixture, 'target', 'release'); - return join(targetDir, `${fixture}.exe`); -} - -function envKeyForFixture(fixture) { - return `FLUXER_WIN_GAME_CAPTURE_FIXTURE_ARGS_${fixture.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '_')}`; -} - -function fixtureEnvValue(baseName, fixture) { - const fixtureKey = `${baseName}_${fixture.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '_')}`; - return process.env[fixtureKey] ?? process.env[baseName]; -} - -function splitExtraArgs(raw) { - return (raw ?? '') - .split(/\s+/) - .map((arg) => arg.trim()) - .filter(Boolean); -} - -function extraFixtureArgs(fixture) { - return [ - ...splitExtraArgs(process.env.FLUXER_WIN_GAME_CAPTURE_FIXTURE_ARGS), - ...splitExtraArgs(process.env[envKeyForFixture(fixture)]), - ]; -} - -function diagnosticOverride(baseName, fixture) { - const raw = fixtureEnvValue(baseName, fixture); - if (raw === undefined || raw === '') return undefined; - const value = Number.parseInt(raw, 10); - if (!Number.isFinite(value)) throw new Error(`Invalid ${baseName} override: ${raw}`); - return value; -} - -function runCommand(command, args, options = {}) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd: options.cwd ?? ROOT, - env: process.env, - stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'], - windowsHide: false, - }); - let stdout = ''; - let stderr = ''; - child.stdout?.on('data', (chunk) => { - stdout += chunk.toString(); - if (options.echo) process.stdout.write(chunk); - }); - child.stderr?.on('data', (chunk) => { - stderr += chunk.toString(); - if (options.echo) process.stderr.write(chunk); - }); - child.once('error', reject); - child.once('exit', (code, signal) => { - if (code === 0) { - resolve({stdout, stderr}); - return; - } - reject(new Error(`${command} ${args.join(' ')} failed with ${signal ?? code}\n${stderr || stdout}`)); - }); - }); -} - -async function buildFixture(fixture) { - const manifest = join(ROOT, 'test-apps', fixture, 'Cargo.toml'); - if (!existsSync(manifest)) throw new Error(`unknown fixture: ${fixture}`); - const exe = fixtureExePath(fixture); - if (envFlag('FLUXER_WIN_GAME_CAPTURE_FIXTURE_SKIP_BUILD') && existsSync(exe)) { - console.log(`[fixture-smoke] using existing ${fixture}`); - return exe; - } - const args = ['build', '--release', '--manifest-path', manifest]; - const target = fixtureTarget(fixture); - if (target) args.push('--target', target); - console.log(`[fixture-smoke] building ${fixture}`); - await runCommand('cargo', args, {echo: envFlag('FLUXER_WIN_GAME_CAPTURE_FIXTURE_VERBOSE')}); - if (!existsSync(exe)) throw new Error(`fixture build did not produce ${exe}`); - return exe; -} - -function waitForHwnd(child, fixture) { - return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const timeout = setTimeout(() => { - reject(new Error(`${fixture} did not print HWND within ${START_TIMEOUT_MS}ms\n${stderr || stdout}`)); - }, START_TIMEOUT_MS); - const finish = (hwnd) => { - clearTimeout(timeout); - resolve(hwnd); - }; - child.stdout.on('data', (chunk) => { - const text = chunk.toString(); - stdout += text; - const match = stdout.match(/HWND=(\d+)/); - if (match) finish(match[1]); - }); - child.stderr.on('data', (chunk) => { - stderr += chunk.toString(); - if (envFlag('FLUXER_WIN_GAME_CAPTURE_FIXTURE_VERBOSE')) process.stderr.write(chunk); - }); - child.once('exit', (code, signal) => { - clearTimeout(timeout); - reject(new Error(`${fixture} exited before capture started (${signal ?? code})\n${stderr || stdout}`)); - }); - }); -} - -function observedFrameCount(diagnostics) { - if (!diagnostics) return 0; - const accepted = Number(diagnostics.frameSinkAccepted ?? 0); - const coalesced = Number(diagnostics.frameSinkCoalesced ?? 0); - const droppedWithoutSink = Number(diagnostics.mediaFramesDroppedWithoutSink ?? 0); - const total = accepted + coalesced + droppedWithoutSink; - return Number.isFinite(total) ? total : 0; -} - -async function waitForAdvancingFrames(screenCapture, fixture, stalls) { - let lastDiagnostics = null; - const start = Date.now(); - while (Date.now() - start < FRAME_TIMEOUT_MS) { - lastDiagnostics = screenCapture.getDiagnostics?.() ?? lastDiagnostics; - const frameCount = observedFrameCount(lastDiagnostics); - if (frameCount >= MIN_OBSERVED_FRAMES) { - return {frameCount, diagnostics: lastDiagnostics}; - } - await delay(100); - } - throw new Error( - `${fixture} capture did not deliver advancing frames within ${FRAME_TIMEOUT_MS}ms (frames=${observedFrameCount(lastDiagnostics)}, stalls=${JSON.stringify(stalls)}, diagnostics=${JSON.stringify(lastDiagnostics)})`, - ); -} - -function assertEqualDiagnostic(fixture, diagnostics, key, expected) { - if (diagnostics?.[key] !== expected) { - throw new Error(`${fixture} expected diagnostics.${key}=${expected}, got ${JSON.stringify(diagnostics)}`); - } -} - -function assertFixtureDiagnostics(fixture, diagnostics) { - const expected = {...EXPECTED_DIAGNOSTICS}; - const apiOverride = diagnosticOverride('FLUXER_WIN_GAME_CAPTURE_EXPECT_API_TYPE', fixture); - const transportOverride = diagnosticOverride('FLUXER_WIN_GAME_CAPTURE_EXPECT_TRANSPORT', fixture); - const fallbackOverride = diagnosticOverride('FLUXER_WIN_GAME_CAPTURE_EXPECT_FALLBACK_REASON', fixture); - if (apiOverride !== undefined) expected.apiType = apiOverride; - if (transportOverride !== undefined) expected.transport = transportOverride; - if (fallbackOverride !== undefined) expected.fallbackReason = fallbackOverride; - - if (diagnostics?.activeStrategy !== EXPECTED_ACTIVE_STRATEGY) { - throw new Error( - `${fixture} expected activeStrategy=${EXPECTED_ACTIVE_STRATEGY}, got ${JSON.stringify(diagnostics)}`, - ); - } - assertEqualDiagnostic(fixture, diagnostics, 'apiType', expected.apiType); - assertEqualDiagnostic(fixture, diagnostics, 'transport', expected.transport); - assertEqualDiagnostic(fixture, diagnostics, 'fallbackReason', expected.fallbackReason); - assertEqualDiagnostic(fixture, diagnostics, 'dxgiFormat', expected.dxgiFormat); - assertEqualDiagnostic(fixture, diagnostics, 'injectionMethod', ''); -} - -async function runFixture(fixture) { - const exe = await buildFixture(fixture); - const args = ['--frames', '900', '--width', String(WIDTH), '--height', String(HEIGHT), '--windowed']; - if (fixture === 'vulkan-present-fixture') args.push('--resize-at', '120'); - args.push(...extraFixtureArgs(fixture)); - const child = spawn(exe, args, { - cwd: dirname(exe), - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: false, - }); - let started = false; - try { - const hwnd = await waitForHwnd(child, fixture); - const screenCapture = new capture.ScreenCapture({ - sourceId: `window:${hwnd}:0`, - sourceKind: 'game', - width: WIDTH, - height: HEIGHT, - frameRate: FRAME_RATE, - injectionMethod: process.env.FLUXER_WIN_GAME_CAPTURE_INJECTION_METHOD || 'auto', - }); - const stalls = []; - screenCapture.on('stalled', (message) => stalls.push(message ?? '')); - let result; - try { - result = await screenCapture.start(); - started = true; - const observed = await waitForAdvancingFrames(screenCapture, fixture, stalls); - assertFixtureDiagnostics(fixture, observed.diagnostics); - console.log( - `[fixture-smoke] PASS ${fixture}: start=${JSON.stringify(result)} frames=${observed.frameCount} stalls=${JSON.stringify(stalls)} diagnostics=${JSON.stringify(observed.diagnostics)}`, - ); - } finally { - if (started) await screenCapture.stop().catch(() => {}); - } - } finally { - if (!child.killed) child.kill(); - } -} - -async function main() { - if (process.platform !== 'win32') { - console.log('[fixture-smoke] SKIP: Windows game-capture fixtures only run on Windows'); - return 0; - } - if (!capture.isSupported()) { - throw new Error(`win-game-capture binding unavailable: ${capture.loadError?.message ?? 'unknown error'}`); - } - for (const fixture of selectedFixtures()) { - await runFixture(fixture); - } - return 0; -} - -try { - process.exitCode = await main(); -} catch (error) { - console.error(`[fixture-smoke] FAIL: ${error.stack ?? error.message}`); - process.exitCode = 1; -} diff --git a/fluxer_desktop/native/win-game-capture/src/encoder_attach.rs b/fluxer_desktop/native/win-game-capture/src/encoder_attach.rs index 22a5f0ba9..c610b0598 100644 --- a/fluxer_desktop/native/win-game-capture/src/encoder_attach.rs +++ b/fluxer_desktop/native/win-game-capture/src/encoder_attach.rs @@ -421,103 +421,4 @@ mod tests { let err = EncoderAttachment::try_new(640, 480).err(); assert!(matches!(err, Some(EncoderAttachError::RingInitFailed))); } - - #[cfg(target_os = "windows")] - #[test] - fn windows_attach_then_detach_smoke() { - let attach = match EncoderAttachment::try_new(640, 480) { - Ok(a) => a, - Err(_) => return, - }; - assert!(attach.is_attached()); - assert_eq!(attach.width(), 640); - assert_eq!(attach.height(), 480); - assert_eq!(attach.capacity(), 8); - let stats = attach.stats(); - assert_eq!(stats.frames_submitted, 0, "fresh attach has 0 submissions"); - assert_eq!(stats.failed_blits, 0, "fresh attach has 0 failed blits"); - attach.detach(); - assert!(!attach.is_attached()); - } - - #[cfg(target_os = "windows")] - #[test] - fn windows_submit_notify_records_stats() { - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(_) => return, - }; - for _ in 0..3 { - attach.submit_notify().expect("notify ok"); - } - let stats = attach.stats(); - assert_eq!(stats.frames_submitted, 3); - assert_eq!(stats.frames_dropped, 0); - assert_eq!(stats.ring_full_events, 0); - } - - #[cfg(target_os = "windows")] - #[test] - fn windows_submit_rejected_when_detached() { - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(_) => return, - }; - attach.detach(); - let err = attach.submit_notify().err(); - assert!(matches!(err, Some(EncoderAttachError::NotAttached))); - } - - #[cfg(target_os = "windows")] - #[test] - fn windows_wait_next_ready_returns_committed_frame() { - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(_) => return, - }; - attach.submit_notify().expect("notify ok"); - let ready = attach.wait_next_ready(Duration::from_millis(5)); - let ready = ready.expect("committed frame becomes ready"); - assert_eq!(ready.sequence, 1); - assert_eq!(ready.duplicate_count, 0); - } - - #[cfg(target_os = "windows")] - #[test] - fn windows_wait_next_ready_empty_ring_times_out() { - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(_) => return, - }; - let ready = attach.wait_next_ready(Duration::from_millis(2)); - assert!(ready.is_none()); - assert!(attach.is_attached()); - } - - #[cfg(target_os = "windows")] - #[test] - fn windows_wait_next_ready_after_detach_returns_none() { - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(_) => return, - }; - attach.detach(); - let ready = attach.wait_next_ready(Duration::from_millis(2)); - assert!(ready.is_none()); - assert!(!attach.is_attached()); - } - - #[cfg(target_os = "windows")] - #[test] - fn windows_ring_usable_after_nvenc_pre_register_attempt() { - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(_) => return, - }; - let _ = attach.nvenc_pre_registered(); - attach.submit_notify().expect("notify ok"); - let stats = attach.stats(); - assert_eq!(stats.frames_submitted, 1); - assert_eq!(stats.frames_dropped, 0); - } } diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/Cargo.lock deleted file mode 100644 index 9a46411e1..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/Cargo.lock +++ /dev/null @@ -1,156 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "d3d10-present-fixture" -version = "0.0.0" -dependencies = [ - "windows", - "windows-core", -] - -[[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 = "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 = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/Cargo.toml deleted file mode 100644 index 69d699f5f..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "d3d10-present-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "d3d10-present-fixture" -path = "src/main.rs" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Direct3D", - "Win32_Graphics_Direct3D10", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Dxgi_Common", - "Win32_Graphics_Gdi", - "Win32_System_LibraryLoader", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/src/main.rs deleted file mode 100644 index b4d7ec9c5..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d10-present-fixture/src/main.rs +++ /dev/null @@ -1,389 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("d3d10-present-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() -> windows_core::Result<()> { - windows_impl::run() -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub const COUNTER_BLOCK: u32 = 16; - - pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) { - let width = width.max(1); - let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize; - BARS[index.min(BARS.len() - 1)] - } - - pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) { - let low = (frame_index & 0x00FF_FFFF) as u32; - ( - (low & 0xFF) as u8, - ((low >> 8) & 0xFF) as u8, - ((low >> 16) & 0xFF) as u8, - ) - } -} - -#[cfg(windows)] -mod cli { - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - _ => {} - } - } - options - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::{cli::Options, pattern}; - use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM}; - use windows::Win32::Graphics::Direct3D10::{ - D3D10_DRIVER_TYPE_HARDWARE, D3D10_MAPPED_TEXTURE2D, D3D10_SDK_VERSION, D3D10CreateDevice, - ID3D10Device, ID3D10RenderTargetView, ID3D10Resource, ID3D10Texture2D, - }; - use windows::Win32::Graphics::Dxgi::Common::{ - DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_DESC, - DXGI_RATIONAL, DXGI_SAMPLE_DESC, - }; - use windows::Win32::Graphics::Dxgi::{ - DXGI_PRESENT, DXGI_SWAP_CHAIN_DESC, DXGI_SWAP_EFFECT_DISCARD, - DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory, IDXGISwapChain, - }; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, - GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW, - ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, - WS_POPUP, WS_VISIBLE, - }; - use windows_core::{BOOL, Interface, Result, w}; - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerD3D10Fixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer D3D10 Present Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok(hwnd) - } - } - - struct Renderer { - device: ID3D10Device, - swap_chain: IDXGISwapChain, - rtv: ID3D10RenderTargetView, - width: u32, - height: u32, - staging: ID3D10Texture2D, - format: DXGI_FORMAT, - } - - impl Renderer { - unsafe fn new(hwnd: HWND, width: u32, height: u32) -> Result { - unsafe { - let mut device: Option = None; - D3D10CreateDevice( - None, - D3D10_DRIVER_TYPE_HARDWARE, - HMODULE::default(), - 0, - D3D10_SDK_VERSION, - Some(&mut device), - )?; - let device = device.expect("D3D10CreateDeviceAndSwapChain yielded no device"); - - let dxgi_device: IDXGIDevice = device.cast()?; - let adapter = dxgi_device.GetAdapter()?; - let factory: IDXGIFactory = adapter.GetParent()?; - let (swap_chain, format) = - Self::make_swap_chain(&factory, &device, hwnd, width, height)?; - - let rtv = Self::make_rtv(&device, &swap_chain)?; - let staging = Self::make_staging(&device, width, height, format)?; - - Ok(Self { - device, - swap_chain, - rtv, - width, - height, - staging, - format, - }) - } - } - - unsafe fn make_swap_chain( - factory: &IDXGIFactory, - device: &ID3D10Device, - hwnd: HWND, - width: u32, - height: u32, - ) -> Result<(IDXGISwapChain, DXGI_FORMAT)> { - unsafe { - let mut last = windows_core::HRESULT(0); - for format in [DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM] { - let desc = DXGI_SWAP_CHAIN_DESC { - BufferDesc: DXGI_MODE_DESC { - Width: width, - Height: height, - RefreshRate: DXGI_RATIONAL { - Numerator: 0, - Denominator: 0, - }, - Format: format, - ..Default::default() - }, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: 1, - OutputWindow: hwnd, - Windowed: BOOL(1), - SwapEffect: DXGI_SWAP_EFFECT_DISCARD, - Flags: 0, - }; - let mut swap_chain: Option = None; - let hr = factory.CreateSwapChain(device, &desc, &mut swap_chain); - if hr.is_ok() { - return Ok(( - swap_chain.expect("CreateSwapChain yielded no swapchain"), - format, - )); - } - last = hr; - } - last.ok()?; - unreachable!() - } - } - - unsafe fn make_rtv( - device: &ID3D10Device, - swap_chain: &IDXGISwapChain, - ) -> Result { - unsafe { - let back_buffer: ID3D10Texture2D = swap_chain.GetBuffer(0)?; - let resource: ID3D10Resource = back_buffer.cast()?; - let mut rtv: Option = None; - device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?; - Ok(rtv.expect("CreateRenderTargetView yielded no view")) - } - } - - unsafe fn make_staging( - device: &ID3D10Device, - width: u32, - height: u32, - format: DXGI_FORMAT, - ) -> Result { - use windows::Win32::Graphics::Direct3D10::{ - D3D10_CPU_ACCESS_WRITE, D3D10_TEXTURE2D_DESC, D3D10_USAGE_STAGING, - }; - unsafe { - let desc = D3D10_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: format, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D10_USAGE_STAGING, - BindFlags: 0, - CPUAccessFlags: D3D10_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - device.CreateTexture2D(&desc, None) - } - } - - unsafe fn render_frame(&self, frame_index: u64) -> Result<()> { - use windows::Win32::Graphics::Direct3D10::D3D10_MAP_WRITE; - unsafe { - let (cr, cg, cb) = - pattern::BARS[(frame_index % pattern::BARS.len() as u64) as usize]; - let clear = [cr as f32 / 255.0, cg as f32 / 255.0, cb as f32 / 255.0, 1.0]; - self.device.ClearRenderTargetView(&self.rtv, &clear); - - let mapped: D3D10_MAPPED_TEXTURE2D = self.staging.Map(0, D3D10_MAP_WRITE, 0)?; - let row_pitch = mapped.RowPitch as usize; - let base = mapped.pData as *mut u8; - let counter = pattern::counter_colour(frame_index); - let luma = (frame_index & 0xFF) as u8; - for y in 0..self.height { - let row = base.add(y as usize * row_pitch); - for x in 0..self.width { - let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK - { - counter - } else { - pattern::bar_colour(x, self.width) - }; - let px = row.add(x as usize * 4); - if self.format == DXGI_FORMAT_R8G8B8A8_UNORM { - *px = r; - *px.add(1) = g; - *px.add(2) = b.saturating_add(luma / 4); - } else { - *px = b.saturating_add(luma / 4); - *px.add(1) = g; - *px.add(2) = r; - } - *px.add(3) = 255; - } - } - self.staging.Unmap(0); - - let back_buffer: ID3D10Texture2D = self.swap_chain.GetBuffer(0)?; - let dst: ID3D10Resource = back_buffer.cast()?; - let src: ID3D10Resource = self.staging.cast()?; - self.device.CopyResource(&dst, &src); - self.swap_chain.Present(1, DXGI_PRESENT(0)).ok()?; - let _ = &self.device; - Ok(()) - } - } - } - - pub fn run() -> Result<()> { - let options = Options::from_env(); - unsafe { - let hwnd = create_window(options.width, options.height, options.borderless)?; - - let mut rect = Default::default(); - let _ = GetClientRect(hwnd, &mut rect); - let width = ((rect.right - rect.left).max(1)) as u32; - let height = ((rect.bottom - rect.top).max(1)) as u32; - - let renderer = Renderer::new(hwnd, width, height)?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let mut frame_index: u64 = 0; - let mut msg = MSG::default(); - loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - return Ok(()); - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - renderer.render_frame(frame_index)?; - frame_index += 1; - - if let Some(limit) = options.frames { - if frame_index >= limit { - return Ok(()); - } - } - } - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/Cargo.lock deleted file mode 100644 index 5c3a98846..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/Cargo.lock +++ /dev/null @@ -1,156 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "d3d11-present-fixture" -version = "0.0.0" -dependencies = [ - "windows", - "windows-core", -] - -[[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 = "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 = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/Cargo.toml deleted file mode 100644 index 36b168da7..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "d3d11-present-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "d3d11-present-fixture" -path = "src/main.rs" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Direct3D", - "Win32_Graphics_Direct3D11", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Dxgi_Common", - "Win32_Graphics_Gdi", - "Win32_System_LibraryLoader", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/src/main.rs deleted file mode 100644 index 7b41da2db..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d11-present-fixture/src/main.rs +++ /dev/null @@ -1,496 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("d3d11-present-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() -> windows_core::Result<()> { - windows_impl::run() -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub const COUNTER_BLOCK: u32 = 16; - - pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) { - let width = width.max(1); - let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize; - BARS[index.min(BARS.len() - 1)] - } - - pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) { - let low = (frame_index & 0x00FF_FFFF) as u32; - ( - (low & 0xFF) as u8, - ((low >> 8) & 0xFF) as u8, - ((low >> 16) & 0xFF) as u8, - ) - } -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod cli { - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum PresentFormat { - Bgra8, - Rgba8, - R10G10B10A2, - Rgba16Float, - } - - impl PresentFormat { - fn parse(value: &str) -> Option { - match value { - "bgra8" | "b8g8r8a8" => Some(Self::Bgra8), - "rgba8" | "r8g8b8a8" => Some(Self::Rgba8), - "r10g10b10a2" | "rgb10a2" | "10bit" => Some(Self::R10G10B10A2), - "rgba16f" | "r16g16b16a16f" | "fp16" => Some(Self::Rgba16Float), - _ => None, - } - } - } - - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - pub format: PresentFormat, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - format: PresentFormat::Bgra8, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - "--format" => { - if let Some(format) = args.next().and_then(|v| PresentFormat::parse(&v)) { - options.format = format; - } - } - _ => {} - } - } - options - } - } - - #[cfg(test)] - mod tests { - use super::PresentFormat; - - #[test] - fn parses_explicit_format_modes() { - assert_eq!(PresentFormat::parse("bgra8"), Some(PresentFormat::Bgra8)); - assert_eq!(PresentFormat::parse("rgba8"), Some(PresentFormat::Rgba8)); - assert_eq!( - PresentFormat::parse("r10g10b10a2"), - Some(PresentFormat::R10G10B10A2) - ); - assert_eq!( - PresentFormat::parse("fp16"), - Some(PresentFormat::Rgba16Float) - ); - assert_eq!(PresentFormat::parse("unknown"), None); - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::{ - cli::{Options, PresentFormat}, - pattern, - }; - use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM}; - use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE; - use windows::Win32::Graphics::Direct3D11::{ - D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, - D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView, - ID3D11Resource, ID3D11Texture2D, - }; - use windows::Win32::Graphics::Dxgi::Common::{ - DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, - DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, - DXGI_SAMPLE_DESC, - }; - use windows::Win32::Graphics::Dxgi::{ - DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, DXGI_SWAP_EFFECT_FLIP_DISCARD, - DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory2, IDXGISwapChain1, - }; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, - GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW, - ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, - WS_POPUP, WS_VISIBLE, - }; - use windows_core::{Interface, Result, w}; - - impl PresentFormat { - fn dxgi(self) -> DXGI_FORMAT { - match self { - PresentFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM, - PresentFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM, - PresentFormat::R10G10B10A2 => DXGI_FORMAT_R10G10B10A2_UNORM, - PresentFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT, - } - } - - fn bytes_per_pixel(self) -> usize { - match self { - PresentFormat::Bgra8 | PresentFormat::Rgba8 | PresentFormat::R10G10B10A2 => 4, - PresentFormat::Rgba16Float => 8, - } - } - } - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerD3D11Fixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer D3D11 Present Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok(hwnd) - } - } - - struct Renderer { - device: ID3D11Device, - context: ID3D11DeviceContext, - swap_chain: IDXGISwapChain1, - rtv: ID3D11RenderTargetView, - width: u32, - height: u32, - staging: ID3D11Texture2D, - format: PresentFormat, - } - - impl Renderer { - unsafe fn new(hwnd: HWND, width: u32, height: u32, format: PresentFormat) -> Result { - unsafe { - let mut device: Option = None; - let mut context: Option = None; - D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_HARDWARE, - HMODULE::default(), - D3D11_CREATE_DEVICE_BGRA_SUPPORT, - None, - D3D11_SDK_VERSION, - Some(&mut device), - None, - Some(&mut context), - )?; - let device = device.expect("D3D11CreateDevice yielded no device"); - let context = context.expect("D3D11CreateDevice yielded no context"); - - let dxgi_device: IDXGIDevice = device.cast()?; - let adapter = dxgi_device.GetAdapter()?; - let factory: IDXGIFactory2 = adapter.GetParent()?; - - let desc = DXGI_SWAP_CHAIN_DESC1 { - Width: width, - Height: height, - Format: format.dxgi(), - Stereo: false.into(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: 2, - Scaling: DXGI_SCALING_STRETCH, - SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD, - AlphaMode: DXGI_ALPHA_MODE_IGNORE, - Flags: 0, - }; - let swap_chain = - factory.CreateSwapChainForHwnd(&device, hwnd, &desc, None, None)?; - - let rtv = Self::make_rtv(&device, &swap_chain)?; - let staging = Self::make_staging(&device, width, height, format)?; - - Ok(Self { - device, - context, - swap_chain, - rtv, - width, - height, - staging, - format, - }) - } - } - - unsafe fn make_rtv( - device: &ID3D11Device, - swap_chain: &IDXGISwapChain1, - ) -> Result { - unsafe { - let back_buffer: ID3D11Texture2D = swap_chain.GetBuffer(0)?; - let resource: ID3D11Resource = back_buffer.cast()?; - let mut rtv: Option = None; - device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?; - Ok(rtv.expect("CreateRenderTargetView yielded no view")) - } - } - - unsafe fn make_staging( - device: &ID3D11Device, - width: u32, - height: u32, - format: PresentFormat, - ) -> Result { - use windows::Win32::Graphics::Direct3D11::{ - D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, - }; - unsafe { - let desc = D3D11_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: format.dxgi(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_STAGING, - BindFlags: 0, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - let mut staging: Option = None; - device.CreateTexture2D(&desc, None, Some(&mut staging))?; - Ok(staging.expect("CreateTexture2D yielded no staging texture")) - } - } - - unsafe fn render_frame(&self, frame_index: u64) -> Result<()> { - use windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE; - unsafe { - let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); - self.context - .Map(&self.staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))?; - let row_pitch = mapped.RowPitch as usize; - let base = mapped.pData as *mut u8; - let counter = pattern::counter_colour(frame_index); - let luma = (frame_index & 0xFF) as u8; - let bpp = self.format.bytes_per_pixel(); - for y in 0..self.height { - let row = base.add(y as usize * row_pitch); - for x in 0..self.width { - let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK - { - counter - } else { - pattern::bar_colour(x, self.width) - }; - let px = row.add(x as usize * bpp); - write_pixel(self.format, px, r, g, b.saturating_add(luma / 4)); - } - } - self.context.Unmap(&self.staging, 0); - - let back_buffer: ID3D11Texture2D = self.swap_chain.GetBuffer(0)?; - let dst: ID3D11Resource = back_buffer.cast()?; - let src: ID3D11Resource = self.staging.cast()?; - self.context.CopyResource(&dst, &src); - let _ = &self.rtv; - self.swap_chain.Present(1, Default::default()).ok()?; - let _ = &self.device; - Ok(()) - } - } - } - - unsafe fn write_pixel(format: PresentFormat, px: *mut u8, r: u8, g: u8, b: u8) { - unsafe { - match format { - PresentFormat::Bgra8 => { - *px = b; - *px.add(1) = g; - *px.add(2) = r; - *px.add(3) = 255; - } - PresentFormat::Rgba8 => { - *px = r; - *px.add(1) = g; - *px.add(2) = b; - *px.add(3) = 255; - } - PresentFormat::R10G10B10A2 => { - let packed = scale8_to_10(r) - | (scale8_to_10(g) << 10) - | (scale8_to_10(b) << 20) - | (0x3 << 30); - std::ptr::copy_nonoverlapping(packed.to_le_bytes().as_ptr(), px, 4); - } - PresentFormat::Rgba16Float => { - let highlight = if r == 255 && g == 255 && b > 240 { - 1.25 - } else { - 1.0 - }; - let values = [ - f32_to_f16((r as f32 / 255.0) * highlight), - f32_to_f16((g as f32 / 255.0) * highlight), - f32_to_f16((b as f32 / 255.0) * highlight), - f32_to_f16(1.0), - ]; - for (index, value) in values.iter().enumerate() { - std::ptr::copy_nonoverlapping( - value.to_le_bytes().as_ptr(), - px.add(index * 2), - 2, - ); - } - } - } - } - } - - fn scale8_to_10(value: u8) -> u32 { - (value as u32 * 1023 + 127) / 255 - } - - fn f32_to_f16(value: f32) -> u16 { - let value = value.clamp(0.0, 65504.0); - if value == 0.0 { - return 0; - } - let bits = value.to_bits(); - let exp = ((bits >> 23) & 0xFF) as i32 - 127 + 15; - let mant = bits & 0x7F_FFFF; - if exp <= 0 { - return 0; - } - if exp >= 31 { - return 0x7BFF; - } - ((exp as u16) << 10) | (((mant + 0x1000) >> 13) as u16) - } - - pub fn run() -> Result<()> { - let options = Options::from_env(); - unsafe { - let hwnd = create_window(options.width, options.height, options.borderless)?; - - let mut rect = Default::default(); - let _ = GetClientRect(hwnd, &mut rect); - let width = ((rect.right - rect.left).max(1)) as u32; - let height = ((rect.bottom - rect.top).max(1)) as u32; - - let renderer = Renderer::new(hwnd, width, height, options.format)?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let mut frame_index: u64 = 0; - let mut msg = MSG::default(); - loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - return Ok(()); - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - renderer.render_frame(frame_index)?; - frame_index += 1; - - if let Some(limit) = options.frames { - if frame_index >= limit { - return Ok(()); - } - } - } - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/Cargo.lock deleted file mode 100644 index 92080e69b..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/Cargo.lock +++ /dev/null @@ -1,156 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "d3d12-present-fixture" -version = "0.0.0" -dependencies = [ - "windows", - "windows-core", -] - -[[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 = "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 = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/Cargo.toml deleted file mode 100644 index 2a6cc6287..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "d3d12-present-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "d3d12-present-fixture" -path = "src/main.rs" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Direct3D", - "Win32_Graphics_Direct3D12", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Dxgi_Common", - "Win32_Graphics_Gdi", - "Win32_Security", - "Win32_System_LibraryLoader", - "Win32_System_Threading", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/src/main.rs deleted file mode 100644 index 4b7afa377..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d12-present-fixture/src/main.rs +++ /dev/null @@ -1,582 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("d3d12-present-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() -> windows_core::Result<()> { - windows_impl::run() -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub const COUNTER_BLOCK: u32 = 16; - - pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) { - let width = width.max(1); - let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize; - BARS[index.min(BARS.len() - 1)] - } - - pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) { - let low = (frame_index & 0x00FF_FFFF) as u32; - ( - (low & 0xFF) as u8, - ((low >> 8) & 0xFF) as u8, - ((low >> 16) & 0xFF) as u8, - ) - } -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod cli { - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum PresentFormat { - Bgra8, - Rgba8, - R10G10B10A2, - Rgba16Float, - } - - impl PresentFormat { - fn parse(value: &str) -> Option { - match value { - "bgra8" | "b8g8r8a8" => Some(Self::Bgra8), - "rgba8" | "r8g8b8a8" => Some(Self::Rgba8), - "r10g10b10a2" | "rgb10a2" | "10bit" => Some(Self::R10G10B10A2), - "rgba16f" | "r16g16b16a16f" | "fp16" => Some(Self::Rgba16Float), - _ => None, - } - } - } - - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - pub backbuffers: u32, - pub format: PresentFormat, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - backbuffers: 2, - format: PresentFormat::Bgra8, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--backbuffers" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.backbuffers = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - "--format" => { - if let Some(format) = args.next().and_then(|v| PresentFormat::parse(&v)) { - options.format = format; - } - } - _ => {} - } - } - options.backbuffers = options.backbuffers.max(2); - options - } - } - - #[cfg(test)] - mod tests { - use super::PresentFormat; - - #[test] - fn parses_explicit_format_modes() { - assert_eq!(PresentFormat::parse("bgra8"), Some(PresentFormat::Bgra8)); - assert_eq!(PresentFormat::parse("rgba8"), Some(PresentFormat::Rgba8)); - assert_eq!( - PresentFormat::parse("r10g10b10a2"), - Some(PresentFormat::R10G10B10A2) - ); - assert_eq!( - PresentFormat::parse("fp16"), - Some(PresentFormat::Rgba16Float) - ); - assert_eq!(PresentFormat::parse("unknown"), None); - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::cli::{Options, PresentFormat}; - use super::pattern; - use windows::Win32::Foundation::RECT; - use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, LRESULT, WPARAM}; - use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0}; - use windows::Win32::Graphics::Direct3D12::{ - D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_QUEUE_DESC, D3D12_COMMAND_QUEUE_FLAG_NONE, - D3D12_CPU_DESCRIPTOR_HANDLE, D3D12_DESCRIPTOR_HEAP_DESC, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, - D3D12_DESCRIPTOR_HEAP_TYPE_RTV, D3D12_FENCE_FLAG_NONE, D3D12_RESOURCE_BARRIER, - D3D12_RESOURCE_BARRIER_0, D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, - D3D12_RESOURCE_BARRIER_FLAG_NONE, D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, - D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET, - D3D12_RESOURCE_TRANSITION_BARRIER, D3D12CreateDevice, ID3D12CommandAllocator, - ID3D12CommandQueue, ID3D12DescriptorHeap, ID3D12Device, ID3D12Fence, - ID3D12GraphicsCommandList, ID3D12PipelineState, ID3D12Resource, - }; - use windows::Win32::Graphics::Dxgi::Common::{ - DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, - DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, - DXGI_SAMPLE_DESC, - }; - use windows::Win32::Graphics::Dxgi::{ - CreateDXGIFactory2, DXGI_ADAPTER_FLAG, DXGI_ADAPTER_FLAG_SOFTWARE, - DXGI_CREATE_FACTORY_FLAGS, DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, - DXGI_SWAP_EFFECT_FLIP_DISCARD, DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIAdapter1, - IDXGIFactory4, IDXGISwapChain3, - }; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::System::Threading::{CreateEventW, INFINITE, WaitForSingleObject}; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, - GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW, - ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, - WS_POPUP, WS_VISIBLE, - }; - use windows_core::{Interface, Result, w}; - - impl PresentFormat { - fn dxgi(self) -> DXGI_FORMAT { - match self { - PresentFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM, - PresentFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM, - PresentFormat::R10G10B10A2 => DXGI_FORMAT_R10G10B10A2_UNORM, - PresentFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT, - } - } - } - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerD3D12Fixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer D3D12 Present Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok(hwnd) - } - } - - unsafe fn create_device(factory: &IDXGIFactory4) -> Result { - unsafe { - let mut index = 0u32; - while let Ok(adapter) = factory.EnumAdapters1(index) { - let adapter: IDXGIAdapter1 = adapter; - let desc = adapter.GetDesc1()?; - let is_software = - (DXGI_ADAPTER_FLAG(desc.Flags as i32).0 & DXGI_ADAPTER_FLAG_SOFTWARE.0) != 0; - if !is_software { - let mut device: Option = None; - if D3D12CreateDevice(&adapter, D3D_FEATURE_LEVEL_11_0, &mut device).is_ok() { - if let Some(device) = device { - return Ok(device); - } - } - } - index += 1; - } - let mut device: Option = None; - let _ = D3D_DRIVER_TYPE_HARDWARE; - D3D12CreateDevice(None, D3D_FEATURE_LEVEL_11_0, &mut device)?; - device.ok_or_else(windows_core::Error::from_thread) - } - } - - struct Renderer { - device: ID3D12Device, - queue: ID3D12CommandQueue, - swap_chain: IDXGISwapChain3, - rtv_heap: ID3D12DescriptorHeap, - rtv_descriptor_size: usize, - render_targets: Vec, - allocators: Vec, - list: ID3D12GraphicsCommandList, - fence: ID3D12Fence, - fence_event: HANDLE, - fence_value: u64, - frame_fence_values: Vec, - width: u32, - height: u32, - format: PresentFormat, - } - - impl Renderer { - unsafe fn new( - hwnd: HWND, - width: u32, - height: u32, - backbuffers: u32, - format: PresentFormat, - ) -> Result { - unsafe { - let factory: IDXGIFactory4 = CreateDXGIFactory2(DXGI_CREATE_FACTORY_FLAGS(0))?; - let device = create_device(&factory)?; - - let queue_desc = D3D12_COMMAND_QUEUE_DESC { - Type: D3D12_COMMAND_LIST_TYPE_DIRECT, - Priority: 0, - Flags: D3D12_COMMAND_QUEUE_FLAG_NONE, - NodeMask: 0, - }; - let queue: ID3D12CommandQueue = device.CreateCommandQueue(&queue_desc)?; - - let desc = DXGI_SWAP_CHAIN_DESC1 { - Width: width, - Height: height, - Format: format.dxgi(), - Stereo: false.into(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: backbuffers, - Scaling: DXGI_SCALING_STRETCH, - SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD, - AlphaMode: DXGI_ALPHA_MODE_IGNORE, - Flags: 0, - }; - let swap_chain1 = - factory.CreateSwapChainForHwnd(&queue, hwnd, &desc, None, None)?; - let swap_chain: IDXGISwapChain3 = swap_chain1.cast()?; - - let rtv_heap_desc = D3D12_DESCRIPTOR_HEAP_DESC { - Type: D3D12_DESCRIPTOR_HEAP_TYPE_RTV, - NumDescriptors: backbuffers, - Flags: D3D12_DESCRIPTOR_HEAP_FLAG_NONE, - NodeMask: 0, - }; - let rtv_heap: ID3D12DescriptorHeap = device.CreateDescriptorHeap(&rtv_heap_desc)?; - let rtv_descriptor_size = device - .GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) - as usize; - - let heap_start = rtv_heap.GetCPUDescriptorHandleForHeapStart(); - let mut render_targets = Vec::with_capacity(backbuffers as usize); - let mut allocators = Vec::with_capacity(backbuffers as usize); - for i in 0..backbuffers { - let back_buffer: ID3D12Resource = swap_chain.GetBuffer(i)?; - let handle = D3D12_CPU_DESCRIPTOR_HANDLE { - ptr: heap_start.ptr + i as usize * rtv_descriptor_size, - }; - device.CreateRenderTargetView(&back_buffer, None, handle); - render_targets.push(back_buffer); - - let allocator: ID3D12CommandAllocator = - device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT)?; - allocators.push(allocator); - } - - let list: ID3D12GraphicsCommandList = device.CreateCommandList( - 0, - D3D12_COMMAND_LIST_TYPE_DIRECT, - &allocators[0], - None::<&ID3D12PipelineState>, - )?; - list.Close()?; - - let fence: ID3D12Fence = device.CreateFence(0, D3D12_FENCE_FLAG_NONE)?; - let fence_event = CreateEventW(None, false, false, None)?; - - Ok(Self { - device, - queue, - swap_chain, - rtv_heap, - rtv_descriptor_size, - render_targets, - frame_fence_values: vec![0; backbuffers as usize], - allocators, - list, - fence, - fence_event, - fence_value: 0, - width, - height, - format, - }) - } - } - - unsafe fn transition( - resource: &ID3D12Resource, - before: windows::Win32::Graphics::Direct3D12::D3D12_RESOURCE_STATES, - after: windows::Win32::Graphics::Direct3D12::D3D12_RESOURCE_STATES, - ) -> D3D12_RESOURCE_BARRIER { - D3D12_RESOURCE_BARRIER { - Type: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, - Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, - Anonymous: D3D12_RESOURCE_BARRIER_0 { - Transition: std::mem::ManuallyDrop::new(D3D12_RESOURCE_TRANSITION_BARRIER { - pResource: std::mem::ManuallyDrop::new(Some(resource.clone())), - Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, - StateBefore: before, - StateAfter: after, - }), - }, - } - } - - unsafe fn render_frame(&mut self, frame_index: u64) -> Result<()> { - unsafe { - let buffer_index = self.swap_chain.GetCurrentBackBufferIndex() as usize; - - self.wait_for_fence_value(self.frame_fence_values[buffer_index])?; - - let allocator = &self.allocators[buffer_index]; - let back_buffer = &self.render_targets[buffer_index]; - allocator.Reset()?; - self.list.Reset(allocator, None::<&ID3D12PipelineState>)?; - - let to_rt = Self::transition( - back_buffer, - D3D12_RESOURCE_STATE_PRESENT, - D3D12_RESOURCE_STATE_RENDER_TARGET, - ); - self.list.ResourceBarrier(&[to_rt]); - - let rtv = D3D12_CPU_DESCRIPTOR_HANDLE { - ptr: self.rtv_heap.GetCPUDescriptorHandleForHeapStart().ptr - + buffer_index * self.rtv_descriptor_size, - }; - self.list.OMSetRenderTargets(1, Some(&rtv), false, None); - - self.paint_pattern(rtv, frame_index); - - let to_present = Self::transition( - back_buffer, - D3D12_RESOURCE_STATE_RENDER_TARGET, - D3D12_RESOURCE_STATE_PRESENT, - ); - self.list.ResourceBarrier(&[to_present]); - - self.list.Close()?; - - let command_list: windows::Win32::Graphics::Direct3D12::ID3D12CommandList = - self.list.cast()?; - self.queue.ExecuteCommandLists(&[Some(command_list)]); - - self.swap_chain.Present(1, Default::default()).ok()?; - - self.fence_value += 1; - let signal = self.fence_value; - self.queue.Signal(&self.fence, signal)?; - self.frame_fence_values[buffer_index] = signal; - - Ok(()) - } - } - - unsafe fn paint_pattern(&self, rtv: D3D12_CPU_DESCRIPTOR_HANDLE, frame_index: u64) { - unsafe { - let width = self.width.max(1); - let height = self.height as i32; - let luma = (frame_index & 0xFF) as u8; - - for bar in 0..pattern::BARS.len() as u32 { - let x0 = ((bar as u64 * width as u64) / pattern::BARS.len() as u64) as i32; - let x1 = - (((bar + 1) as u64 * width as u64) / pattern::BARS.len() as u64) as i32; - if x1 <= x0 { - continue; - } - let (r, g, b) = pattern::bar_colour(x0 as u32, width); - let colour = rgba(self.format, r, g, b.saturating_add(luma / 4)); - let rect = RECT { - left: x0, - top: 0, - right: x1, - bottom: height, - }; - self.list.ClearRenderTargetView(rtv, &colour, Some(&[rect])); - } - - let (cr, cg, cb) = pattern::counter_colour(frame_index); - let block = pattern::COUNTER_BLOCK as i32; - let counter_rect = RECT { - left: 0, - top: 0, - right: block.min(self.width as i32), - bottom: block.min(height), - }; - let counter_colour = rgba(self.format, cr, cg, cb); - self.list - .ClearRenderTargetView(rtv, &counter_colour, Some(&[counter_rect])); - } - } - - unsafe fn wait_for_fence_value(&self, value: u64) -> Result<()> { - unsafe { - if value == 0 || self.fence.GetCompletedValue() >= value { - return Ok(()); - } - self.fence.SetEventOnCompletion(value, self.fence_event)?; - WaitForSingleObject(self.fence_event, INFINITE); - Ok(()) - } - } - - unsafe fn flush(&mut self) -> Result<()> { - unsafe { - self.fence_value += 1; - let signal = self.fence_value; - self.queue.Signal(&self.fence, signal)?; - self.wait_for_fence_value(signal) - } - } - } - - impl Drop for Renderer { - fn drop(&mut self) { - unsafe { - let _ = self.flush(); - let _ = CloseHandle(self.fence_event); - let _ = &self.device; - } - } - } - - fn rgba(format: PresentFormat, r: u8, g: u8, b: u8) -> [f32; 4] { - let highlight = - if matches!(format, PresentFormat::Rgba16Float) && r == 255 && g == 255 && b > 240 { - 1.25 - } else { - 1.0 - }; - [ - (r as f32 / 255.0) * highlight, - (g as f32 / 255.0) * highlight, - (b as f32 / 255.0) * highlight, - 1.0, - ] - } - - pub fn run() -> Result<()> { - let options = Options::from_env(); - unsafe { - let hwnd = create_window(options.width, options.height, options.borderless)?; - - let mut rect = Default::default(); - let _ = GetClientRect(hwnd, &mut rect); - let width = ((rect.right - rect.left).max(1)) as u32; - let height = ((rect.bottom - rect.top).max(1)) as u32; - - let mut renderer = - Renderer::new(hwnd, width, height, options.backbuffers, options.format)?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let mut frame_index: u64 = 0; - let mut msg = MSG::default(); - loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - return Ok(()); - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - renderer.render_frame(frame_index)?; - frame_index += 1; - - if let Some(limit) = options.frames { - if frame_index >= limit { - return Ok(()); - } - } - } - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/Cargo.lock deleted file mode 100644 index c72119c16..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/Cargo.lock +++ /dev/null @@ -1,156 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "d3d9-present-fixture" -version = "0.0.0" -dependencies = [ - "windows", - "windows-core", -] - -[[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 = "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 = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/Cargo.toml deleted file mode 100644 index a49444f0e..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "d3d9-present-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "d3d9-present-fixture" -path = "src/main.rs" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Direct3D9", - "Win32_Graphics_Gdi", - "Win32_System_LibraryLoader", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/src/main.rs deleted file mode 100644 index d7def54bb..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/d3d9-present-fixture/src/main.rs +++ /dev/null @@ -1,417 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("d3d9-present-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() -> windows_core::Result<()> { - windows_impl::run() -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub const COUNTER_BLOCK: u32 = 16; - - pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) { - let width = width.max(1); - let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize; - BARS[index.min(BARS.len() - 1)] - } - - pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) { - let low = (frame_index & 0x00FF_FFFF) as u32; - ( - (low & 0xFF) as u8, - ((low >> 8) & 0xFF) as u8, - ((low >> 16) & 0xFF) as u8, - ) - } -} - -#[cfg(windows)] -mod cli { - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - pub enum PresentMode { - Device, - SwapChain, - } - - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - pub multisample: bool, - pub present_mode: PresentMode, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - multisample: false, - present_mode: PresentMode::Device, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - "--multisample" => options.multisample = true, - "--no-multisample" => options.multisample = false, - "--swapchain-present" => options.present_mode = PresentMode::SwapChain, - "--device-present" => options.present_mode = PresentMode::Device, - _ => {} - } - } - options - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::{ - cli::{Options, PresentMode}, - pattern, - }; - use std::ptr::null; - use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; - use windows::Win32::Graphics::Direct3D9::{ - D3D_SDK_VERSION, D3DADAPTER_DEFAULT, D3DCLEAR_TARGET, D3DCREATE_HARDWARE_VERTEXPROCESSING, - D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8, - D3DMULTISAMPLE_2_SAMPLES, D3DMULTISAMPLE_NONE, D3DPRESENT_PARAMETERS, D3DRECT, - D3DSWAPEFFECT_DISCARD, Direct3DCreate9, IDirect3D9, IDirect3DDevice9, - }; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, - GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW, - ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, - WS_POPUP, WS_VISIBLE, - }; - use windows_core::{BOOL, Result, w}; - - const D3DERR_DEVICELOST: i32 = 0x8876_0868u32 as i32; - - fn d3dcolor_xrgb(r: u8, g: u8, b: u8) -> u32 { - 0xFF00_0000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) - } - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerD3D9Fixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer D3D9 Present Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok(hwnd) - } - } - - fn present_params( - hwnd: HWND, - width: u32, - height: u32, - multisample: bool, - ) -> D3DPRESENT_PARAMETERS { - D3DPRESENT_PARAMETERS { - BackBufferWidth: width, - BackBufferHeight: height, - BackBufferFormat: D3DFMT_X8R8G8B8, - BackBufferCount: 1, - MultiSampleType: if multisample { - D3DMULTISAMPLE_2_SAMPLES - } else { - D3DMULTISAMPLE_NONE - }, - MultiSampleQuality: 0, - SwapEffect: D3DSWAPEFFECT_DISCARD, - hDeviceWindow: hwnd, - Windowed: BOOL(1), - EnableAutoDepthStencil: BOOL(0), - ..Default::default() - } - } - - struct Renderer { - _d3d9: IDirect3D9, - device: IDirect3DDevice9, - hwnd: HWND, - width: u32, - height: u32, - device_lost: bool, - multisample: bool, - present_mode: PresentMode, - } - - impl Renderer { - unsafe fn new( - hwnd: HWND, - width: u32, - height: u32, - multisample: bool, - present_mode: PresentMode, - ) -> Result { - unsafe { - let d3d9 = Direct3DCreate9(D3D_SDK_VERSION) - .ok_or_else(|| windows_core::Error::from_thread())?; - - let mut params = present_params(hwnd, width, height, multisample); - - let mut device: Option = None; - let mut created = d3d9 - .CreateDevice( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - hwnd, - D3DCREATE_HARDWARE_VERTEXPROCESSING as u32, - &mut params, - &mut device, - ) - .is_ok(); - if !created { - created = d3d9 - .CreateDevice( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - hwnd, - D3DCREATE_SOFTWARE_VERTEXPROCESSING as u32, - &mut params, - &mut device, - ) - .is_ok(); - } - let device = if created { - device.ok_or_else(|| windows_core::Error::from_thread())? - } else { - return Err(windows_core::Error::from_thread()); - }; - - Ok(Self { - _d3d9: d3d9, - device, - hwnd, - width, - height, - device_lost: false, - multisample, - present_mode, - }) - } - } - - unsafe fn try_reset(&mut self) { - unsafe { - let mut params = - present_params(self.hwnd, self.width, self.height, self.multisample); - if self.device.Reset(&mut params).is_ok() { - self.device_lost = false; - } - } - } - - unsafe fn render_frame(&mut self, frame_index: u64) -> Result<()> { - unsafe { - if self.device_lost { - self.try_reset(); - if self.device_lost { - return self.present(); - } - } - - let width = self.width.max(1); - let height = self.height as i32; - - let bars = pattern::BARS.len() as u32; - for bar in 0..bars { - let x1 = ((bar as u64 * width as u64) / bars as u64) as i32; - let x2 = (((bar + 1) as u64 * width as u64) / bars as u64) as i32; - if x2 <= x1 { - continue; - } - let mid = ((x1 + x2) / 2).max(0) as u32; - let (r, g, b) = pattern::bar_colour(mid, width); - let rect = D3DRECT { - x1, - y1: 0, - x2, - y2: height, - }; - self.device.Clear( - 1, - &rect, - D3DCLEAR_TARGET as u32, - d3dcolor_xrgb(r, g, b), - 1.0, - 0, - )?; - } - - let block = pattern::COUNTER_BLOCK as i32; - let block_x2 = block.min(width as i32); - let block_y2 = block.min(height); - if block_x2 > 0 && block_y2 > 0 { - let (r, g, b) = pattern::counter_colour(frame_index); - let rect = D3DRECT { - x1: 0, - y1: 0, - x2: block_x2, - y2: block_y2, - }; - self.device.Clear( - 1, - &rect, - D3DCLEAR_TARGET as u32, - d3dcolor_xrgb(r, g, b), - 1.0, - 0, - )?; - } - - self.present() - } - } - - unsafe fn present(&mut self) -> Result<()> { - unsafe { - let result = match self.present_mode { - PresentMode::Device => { - self.device.Present(null(), null(), HWND::default(), null()) - } - PresentMode::SwapChain => self.device.GetSwapChain(0).and_then(|swap_chain| { - swap_chain.Present(null(), null(), HWND::default(), null(), 0) - }), - }; - match result { - Ok(()) => Ok(()), - Err(err) if err.code().0 == D3DERR_DEVICELOST => { - self.device_lost = true; - Ok(()) - } - Err(err) => Err(err), - } - } - } - } - - pub fn run() -> Result<()> { - let options = Options::from_env(); - unsafe { - let hwnd = create_window(options.width, options.height, options.borderless)?; - - let mut rect = Default::default(); - let _ = GetClientRect(hwnd, &mut rect); - let width = ((rect.right - rect.left).max(1)) as u32; - let height = ((rect.bottom - rect.top).max(1)) as u32; - - let mut renderer = Renderer::new( - hwnd, - width, - height, - options.multisample, - options.present_mode, - )?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let mut frame_index: u64 = 0; - let mut msg = MSG::default(); - loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - return Ok(()); - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - renderer.render_frame(frame_index)?; - frame_index += 1; - - if let Some(limit) = options.frames { - if frame_index >= limit { - return Ok(()); - } - } - } - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/Cargo.lock deleted file mode 100644 index 46401062b..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/Cargo.lock +++ /dev/null @@ -1,156 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "i686-present-fixture" -version = "0.0.0" -dependencies = [ - "windows", - "windows-core", -] - -[[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 = "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 = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/Cargo.toml deleted file mode 100644 index 1f9346214..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "i686-present-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "i686-present-fixture" -path = "src/main.rs" - -# This fixture is a deterministic Direct3D 11 present fixture whose ONLY purpose -# is to be built for the 32-bit (i686 / WOW64) target so the harness can drive -# the cross-bitness injection paths (an x64 host injecting the 32-bit hook into a -# 32-bit game). The source is target-arch-agnostic -- what makes it "i686" is -# building it for `i686-pc-windows-msvc`: -# -# cargo build --release --manifest-path test-apps/i686-present-fixture/Cargo.toml \ -# --target i686-pc-windows-msvc -# -# It also compile-checks for x86_64-pc-windows-msvc so the standard cargo-check -# matrix stays uniform across fixtures. -[target.'cfg(target_os = "windows")'.dependencies] -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Direct3D", - "Win32_Graphics_Direct3D11", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Dxgi_Common", - "Win32_Graphics_Gdi", - "Win32_System_LibraryLoader", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/src/main.rs deleted file mode 100644 index 7b4f6f50a..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/i686-present-fixture/src/main.rs +++ /dev/null @@ -1,358 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("i686-present-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() -> windows_core::Result<()> { - windows_impl::run() -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub const COUNTER_BLOCK: u32 = 16; - - pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) { - let width = width.max(1); - let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize; - BARS[index.min(BARS.len() - 1)] - } - - pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) { - let low = (frame_index & 0x00FF_FFFF) as u32; - ( - (low & 0xFF) as u8, - ((low >> 8) & 0xFF) as u8, - ((low >> 16) & 0xFF) as u8, - ) - } -} - -#[cfg(windows)] -mod cli { - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - _ => {} - } - } - options - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::{cli::Options, pattern}; - use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM}; - use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE; - use windows::Win32::Graphics::Direct3D11::{ - D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, - D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11RenderTargetView, - ID3D11Resource, ID3D11Texture2D, - }; - use windows::Win32::Graphics::Dxgi::Common::{ - DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC, - }; - use windows::Win32::Graphics::Dxgi::{ - DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1, DXGI_SWAP_EFFECT_FLIP_DISCARD, - DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIDevice, IDXGIFactory2, IDXGISwapChain1, - }; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, - GetClientRect, MSG, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, SW_SHOW, - ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, - WS_POPUP, WS_VISIBLE, - }; - use windows_core::{Interface, Result, w}; - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerI686Fixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer i686 (WOW64) Present Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok(hwnd) - } - } - - struct Renderer { - device: ID3D11Device, - context: ID3D11DeviceContext, - swap_chain: IDXGISwapChain1, - rtv: ID3D11RenderTargetView, - width: u32, - height: u32, - staging: ID3D11Texture2D, - } - - impl Renderer { - unsafe fn new(hwnd: HWND, width: u32, height: u32) -> Result { - unsafe { - let mut device: Option = None; - let mut context: Option = None; - D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_HARDWARE, - HMODULE::default(), - D3D11_CREATE_DEVICE_BGRA_SUPPORT, - None, - D3D11_SDK_VERSION, - Some(&mut device), - None, - Some(&mut context), - )?; - let device = device.expect("D3D11CreateDevice yielded no device"); - let context = context.expect("D3D11CreateDevice yielded no context"); - - let dxgi_device: IDXGIDevice = device.cast()?; - let adapter = dxgi_device.GetAdapter()?; - let factory: IDXGIFactory2 = adapter.GetParent()?; - - let desc = DXGI_SWAP_CHAIN_DESC1 { - Width: width, - Height: height, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - Stereo: false.into(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: 2, - Scaling: DXGI_SCALING_STRETCH, - SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD, - AlphaMode: DXGI_ALPHA_MODE_IGNORE, - Flags: 0, - }; - let swap_chain = - factory.CreateSwapChainForHwnd(&device, hwnd, &desc, None, None)?; - - let rtv = Self::make_rtv(&device, &swap_chain)?; - let staging = Self::make_staging(&device, width, height)?; - - Ok(Self { - device, - context, - swap_chain, - rtv, - width, - height, - staging, - }) - } - } - - unsafe fn make_rtv( - device: &ID3D11Device, - swap_chain: &IDXGISwapChain1, - ) -> Result { - unsafe { - let back_buffer: ID3D11Texture2D = swap_chain.GetBuffer(0)?; - let resource: ID3D11Resource = back_buffer.cast()?; - let mut rtv: Option = None; - device.CreateRenderTargetView(&resource, None, Some(&mut rtv))?; - Ok(rtv.expect("CreateRenderTargetView yielded no view")) - } - } - - unsafe fn make_staging( - device: &ID3D11Device, - width: u32, - height: u32, - ) -> Result { - use windows::Win32::Graphics::Direct3D11::{ - D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, - }; - unsafe { - let desc = D3D11_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_STAGING, - BindFlags: 0, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - let mut staging: Option = None; - device.CreateTexture2D(&desc, None, Some(&mut staging))?; - Ok(staging.expect("CreateTexture2D yielded no staging texture")) - } - } - - unsafe fn render_frame(&self, frame_index: u64) -> Result<()> { - use windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE; - unsafe { - let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); - self.context - .Map(&self.staging, 0, D3D11_MAP_WRITE, 0, Some(&mut mapped))?; - let row_pitch = mapped.RowPitch as usize; - let base = mapped.pData as *mut u8; - let counter = pattern::counter_colour(frame_index); - let luma = (frame_index & 0xFF) as u8; - for y in 0..self.height { - let row = base.add(y as usize * row_pitch); - for x in 0..self.width { - let (r, g, b) = if x < pattern::COUNTER_BLOCK && y < pattern::COUNTER_BLOCK - { - counter - } else { - pattern::bar_colour(x, self.width) - }; - let px = row.add(x as usize * 4); - *px = b.saturating_add(luma / 4); - *px.add(1) = g; - *px.add(2) = r; - *px.add(3) = 255; - } - } - self.context.Unmap(&self.staging, 0); - - let back_buffer: ID3D11Texture2D = self.swap_chain.GetBuffer(0)?; - let dst: ID3D11Resource = back_buffer.cast()?; - let src: ID3D11Resource = self.staging.cast()?; - self.context.CopyResource(&dst, &src); - let _ = &self.rtv; - self.swap_chain.Present(1, Default::default()).ok()?; - let _ = &self.device; - Ok(()) - } - } - } - - pub fn run() -> Result<()> { - let options = Options::from_env(); - unsafe { - let hwnd = create_window(options.width, options.height, options.borderless)?; - - let mut rect = Default::default(); - let _ = GetClientRect(hwnd, &mut rect); - let width = ((rect.right - rect.left).max(1)) as u32; - let height = ((rect.bottom - rect.top).max(1)) as u32; - - let renderer = Renderer::new(hwnd, width, height)?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let mut frame_index: u64 = 0; - let mut msg = MSG::default(); - loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - return Ok(()); - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - renderer.render_frame(frame_index)?; - frame_index += 1; - - if let Some(limit) = options.frames { - if frame_index >= limit { - return Ok(()); - } - } - } - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/Cargo.lock deleted file mode 100644 index 6ff717ca4..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/Cargo.lock +++ /dev/null @@ -1,156 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "opengl-swapbuffers-fixture" -version = "0.0.0" -dependencies = [ - "windows", - "windows-core", -] - -[[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 = "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 = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/Cargo.toml deleted file mode 100644 index db94dd7bf..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "opengl-swapbuffers-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "opengl-swapbuffers-fixture" -path = "src/main.rs" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_Graphics_OpenGL", - "Win32_System_LibraryLoader", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/src/main.rs deleted file mode 100644 index eed6c35a7..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/opengl-swapbuffers-fixture/src/main.rs +++ /dev/null @@ -1,484 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("opengl-swapbuffers-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() -> windows_core::Result<()> { - windows_impl::run() -} - -#[allow(dead_code)] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub const COUNTER_BLOCK: u32 = 16; - - pub fn bar_colour(x: u32, width: u32) -> (u8, u8, u8) { - let width = width.max(1); - let index = ((x as u64 * BARS.len() as u64) / width as u64) as usize; - BARS[index.min(BARS.len() - 1)] - } - - pub fn bar_x_range(index: usize, width: u32) -> (u32, u32) { - let width = width.max(1) as u64; - let len = BARS.len() as u64; - let start = (index as u64 * width) / len; - let end = ((index as u64 + 1) * width) / len; - (start as u32, end.min(width) as u32) - } - - pub fn counter_colour(frame_index: u64) -> (u8, u8, u8) { - let low = (frame_index & 0x00FF_FFFF) as u32; - ( - (low & 0xFF) as u8, - ((low >> 8) & 0xFF) as u8, - ((low >> 16) & 0xFF) as u8, - ) - } - - pub fn clear_colour(frame_index: u64) -> [f32; 3] { - let (r, g, b) = BARS[(frame_index % BARS.len() as u64) as usize]; - [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0] - } -} - -#[cfg(windows)] -mod cli { - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - pub layer: bool, - pub stress_pack_state: bool, - pub resize_at: Option, - pub frame_delay_ms: u64, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - layer: false, - stress_pack_state: false, - resize_at: None, - frame_delay_ms: 0, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - "--layer" => options.layer = true, - "--stress-pack-state" => options.stress_pack_state = true, - "--resize-at" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.resize_at = Some(value); - } - } - "--frame-delay-ms" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frame_delay_ms = value; - } - } - _ => {} - } - } - options - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::cli::Options; - use super::pattern; - use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; - use windows::Win32::Graphics::Gdi::{GetDC, HDC, ReleaseDC, WGL_SWAP_MAIN_PLANE}; - use windows::Win32::Graphics::OpenGL::{ - ChoosePixelFormat, GL_COLOR_BUFFER_BIT, GL_FRONT, GL_PACK_ALIGNMENT, GL_PACK_LSB_FIRST, - GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS, GL_PACK_SWAP_BYTES, - GL_READ_BUFFER, GL_SCISSOR_TEST, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW, PFD_MAIN_PLANE, - PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR, SetPixelFormat, SwapBuffers, - glClear, glClearColor, glDisable, glEnable, glGetIntegerv, glPixelStorei, glReadBuffer, - glScissor, glViewport, wglCreateContext, wglDeleteContext, wglGetProcAddress, - wglMakeCurrent, wglSwapLayerBuffers, - }; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_OWNDC, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, - DispatchMessageW, GetClientRect, MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage, - RegisterClassExW, SW_SHOW, ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, - WS_OVERLAPPEDWINDOW, WS_POPUP, WS_VISIBLE, - }; - use windows_core::{PCSTR, Result, w}; - - const GL_PIXEL_PACK_BUFFER: u32 = 0x88EB; - const GL_PIXEL_PACK_BUFFER_BINDING: u32 = 0x88ED; - const GL_STREAM_READ: u32 = 0x88E1; - - type GlGenBuffersFn = unsafe extern "system" fn(i32, *mut u32); - type GlBindBufferFn = unsafe extern "system" fn(u32, u32); - type GlBufferDataFn = unsafe extern "system" fn(u32, isize, *const core::ffi::c_void, u32); - type GlDeleteBuffersFn = unsafe extern "system" fn(i32, *const u32); - - #[derive(Clone, Copy)] - struct BufferProcs { - gen_buffers: GlGenBuffersFn, - bind_buffer: GlBindBufferFn, - buffer_data: GlBufferDataFn, - delete_buffers: GlDeleteBuffersFn, - } - - struct PackStateStress { - procs: Option, - pbo: u32, - } - - impl PackStateStress { - unsafe fn new() -> Self { - let Some(procs) = (unsafe { BufferProcs::load() }) else { - return Self { - procs: None, - pbo: 0, - }; - }; - let mut pbo = 0u32; - unsafe { - (procs.gen_buffers)(1, &mut pbo); - } - if pbo != 0 { - let storage = [0u8; 16]; - unsafe { - (procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, pbo); - (procs.buffer_data)( - GL_PIXEL_PACK_BUFFER, - storage.len() as isize, - storage.as_ptr().cast(), - GL_STREAM_READ, - ); - (procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, 0); - } - } - Self { - procs: Some(procs), - pbo, - } - } - - fn pbo(&self) -> Option { - (self.pbo != 0).then_some(self.pbo) - } - } - - impl Drop for PackStateStress { - fn drop(&mut self) { - unsafe { - if let Some(procs) = self.procs { - if self.pbo != 0 { - (procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, 0); - (procs.delete_buffers)(1, &self.pbo); - } - } - } - } - } - - impl BufferProcs { - unsafe fn load() -> Option { - Some(Self { - gen_buffers: unsafe { load_gl_proc(b"glGenBuffers\0")? }, - bind_buffer: unsafe { load_gl_proc(b"glBindBuffer\0")? }, - buffer_data: unsafe { load_gl_proc(b"glBufferData\0")? }, - delete_buffers: unsafe { load_gl_proc(b"glDeleteBuffers\0")? }, - }) - } - } - - unsafe fn load_gl_proc(name: &'static [u8]) -> Option { - let proc = unsafe { wglGetProcAddress(PCSTR(name.as_ptr()))? }; - let address = proc as usize; - if address <= 3 || address == usize::MAX { - return None; - } - Some(unsafe { std::mem::transmute_copy::<_, T>(&proc) }) - } - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window(width: u32, height: u32, borderless: bool) -> Result { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerOpenGLFixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer OpenGL SwapBuffers Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok(hwnd) - } - } - - unsafe fn set_pixel_format(hdc: HDC) -> Result<()> { - unsafe { - let mut pfd = PIXELFORMATDESCRIPTOR { - nSize: size_of::() as u16, - nVersion: 1, - dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, - iPixelType: PFD_TYPE_RGBA, - cColorBits: 32, - cDepthBits: 24, - cStencilBits: 8, - iLayerType: PFD_MAIN_PLANE.0 as u8, - ..Default::default() - }; - let format = ChoosePixelFormat(hdc, &pfd); - if format == 0 { - return Err(windows_core::Error::from_thread()); - } - SetPixelFormat(hdc, format, &mut pfd)?; - Ok(()) - } - } - - unsafe fn clear_rect(colour: (u8, u8, u8)) { - unsafe { - let (r, g, b) = colour; - glClearColor(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0); - glClear(GL_COLOR_BUFFER_BIT); - } - } - - unsafe fn render_pattern(frame_index: u64, width: u32, height: u32) { - unsafe { - glViewport(0, 0, width as i32, height as i32); - - glEnable(GL_SCISSOR_TEST); - for index in 0..pattern::BARS.len() { - let (start, end) = pattern::bar_x_range(index, width); - if end <= start { - continue; - } - glScissor(start as i32, 0, (end - start) as i32, height as i32); - clear_rect(pattern::BARS[index]); - } - - let block = pattern::COUNTER_BLOCK.min(width).min(height); - if block > 0 { - let y = height - block; - glScissor(0, y as i32, block as i32, block as i32); - clear_rect(pattern::counter_colour(frame_index)); - } - - glDisable(GL_SCISSOR_TEST); - } - } - - unsafe fn client_size(hwnd: HWND) -> (u32, u32) { - unsafe { - let mut rect = Default::default(); - let _ = GetClientRect(hwnd, &mut rect); - ( - ((rect.right - rect.left).max(1)) as u32, - ((rect.bottom - rect.top).max(1)) as u32, - ) - } - } - - unsafe fn stress_pack_state(stress: &PackStateStress) { - unsafe { - glReadBuffer(GL_FRONT); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glPixelStorei(GL_PACK_ROW_LENGTH, 0); - glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - glPixelStorei(GL_PACK_SKIP_ROWS, 0); - glPixelStorei(GL_PACK_SWAP_BYTES, 1); - glPixelStorei(GL_PACK_LSB_FIRST, 1); - if let (Some(procs), Some(pbo)) = (stress.procs, stress.pbo()) { - (procs.bind_buffer)(GL_PIXEL_PACK_BUFFER, pbo); - } - } - } - - unsafe fn assert_stress_pack_state(stress: &PackStateStress) { - unsafe { - let mut read_buffer = 0i32; - let mut pack_alignment = 0i32; - let mut pack_row_length = 0i32; - let mut pack_skip_pixels = 0i32; - let mut pack_skip_rows = 0i32; - let mut pack_swap_bytes = 0i32; - let mut pack_lsb_first = 0i32; - let mut pack_buffer = 0i32; - - glGetIntegerv(GL_READ_BUFFER, &mut read_buffer); - glGetIntegerv(GL_PACK_ALIGNMENT, &mut pack_alignment); - glGetIntegerv(GL_PACK_ROW_LENGTH, &mut pack_row_length); - glGetIntegerv(GL_PACK_SKIP_PIXELS, &mut pack_skip_pixels); - glGetIntegerv(GL_PACK_SKIP_ROWS, &mut pack_skip_rows); - glGetIntegerv(GL_PACK_SWAP_BYTES, &mut pack_swap_bytes); - glGetIntegerv(GL_PACK_LSB_FIRST, &mut pack_lsb_first); - glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &mut pack_buffer); - - assert_eq!( - read_buffer as u32, GL_FRONT, - "GL_READ_BUFFER was not restored" - ); - assert_eq!(pack_alignment, 1, "GL_PACK_ALIGNMENT was not restored"); - assert_eq!(pack_row_length, 0, "GL_PACK_ROW_LENGTH was not restored"); - assert_eq!(pack_skip_pixels, 0, "GL_PACK_SKIP_PIXELS was not restored"); - assert_eq!(pack_skip_rows, 0, "GL_PACK_SKIP_ROWS was not restored"); - assert_eq!(pack_swap_bytes, 1, "GL_PACK_SWAP_BYTES was not restored"); - assert_eq!(pack_lsb_first, 1, "GL_PACK_LSB_FIRST was not restored"); - if let Some(pbo) = stress.pbo() { - assert_eq!( - pack_buffer as u32, pbo, - "GL_PIXEL_PACK_BUFFER_BINDING was not restored" - ); - } - } - } - - pub fn run() -> Result<()> { - let options = Options::from_env(); - unsafe { - let hwnd = create_window(options.width, options.height, options.borderless)?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let hdc = GetDC(Some(hwnd)); - set_pixel_format(hdc)?; - let context = wglCreateContext(hdc)?; - wglMakeCurrent(hdc, context)?; - let pack_state_stress = PackStateStress::new(); - - let mut frame_index: u64 = 0; - let mut msg = MSG::default(); - let mut resized = false; - 'render: loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - break 'render; - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - if let Some(resize_at) = options.resize_at { - if !resized && frame_index >= resize_at { - let width = options.width.saturating_add(160).max(1) as i32; - let height = options.height.saturating_add(96).max(1) as i32; - let _ = MoveWindow(hwnd, 0, 0, width, height, true); - resized = true; - } - } - - let (width, height) = client_size(hwnd); - render_pattern(frame_index, width, height); - if options.stress_pack_state { - stress_pack_state(&pack_state_stress); - } - - if options.layer { - wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE)?; - } else { - SwapBuffers(hdc)?; - } - - if options.stress_pack_state { - assert_stress_pack_state(&pack_state_stress); - } - - frame_index += 1; - if options.frame_delay_ms > 0 { - std::thread::sleep(std::time::Duration::from_millis(options.frame_delay_ms)); - } - if let Some(limit) = options.frames { - if frame_index >= limit { - break 'render; - } - } - } - - let _ = wglMakeCurrent( - HDC::default(), - windows::Win32::Graphics::OpenGL::HGLRC::default(), - ); - let _ = wglDeleteContext(context); - ReleaseDC(Some(hwnd), hdc); - Ok(()) - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/Cargo.lock b/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/Cargo.lock deleted file mode 100644 index 132bf545e..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/Cargo.lock +++ /dev/null @@ -1,182 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[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 = "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 = "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 = "vulkan-present-fixture" -version = "0.0.0" -dependencies = [ - "ash", - "windows", - "windows-core", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -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-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] diff --git a/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/Cargo.toml b/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/Cargo.toml deleted file mode 100644 index 28654e4f5..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -[package] -name = "vulkan-present-fixture" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -# Empty workspace table keeps this fixture out of any parent Cargo workspace, -# matching the sibling crates (hook/, vulkan-layer/) in this package. -[workspace] -resolver = "2" - -[[bin]] -name = "vulkan-present-fixture" -path = "src/main.rs" - -[target.'cfg(target_os = "windows")'.dependencies] -ash = {version = "0.38.0", default-features = false, features = ["std", "loaded"]} -windows = {version = "0.62.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_System_LibraryLoader", - "Win32_UI_WindowsAndMessaging", -]} -windows-core = "0.62.2" diff --git a/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/src/main.rs b/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/src/main.rs deleted file mode 100644 index 186ea76df..000000000 --- a/fluxer_desktop/native/win-game-capture/test-apps/vulkan-present-fixture/src/main.rs +++ /dev/null @@ -1,681 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#[cfg(not(windows))] -fn main() { - eprintln!("vulkan-present-fixture is only functional on Windows"); -} - -#[cfg(windows)] -fn main() { - if let Err(error) = windows_impl::run() { - eprintln!("vulkan-present-fixture failed: {error}"); - std::process::exit(1); - } -} - -#[cfg_attr(not(windows), allow(dead_code))] -mod pattern { - pub const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; - - pub fn clear_colour(frame_index: u64) -> [f32; 4] { - let (r, g, b) = BARS[(frame_index % BARS.len() as u64) as usize]; - [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0] - } -} - -#[cfg(windows)] -mod cli { - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum FormatChoice { - Bgra, - Rgba, - } - - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum PresentModeChoice { - Fifo, - Mailbox, - Immediate, - } - - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum PresentWaitChoice { - Semaphore, - None, - } - - pub struct Options { - pub frames: Option, - pub width: u32, - pub height: u32, - pub borderless: bool, - pub format: FormatChoice, - pub present_mode: PresentModeChoice, - pub present_wait: PresentWaitChoice, - pub resize_at: Option, - pub resize_width: Option, - pub resize_height: Option, - } - - impl Default for Options { - fn default() -> Self { - Self { - frames: None, - width: 1280, - height: 720, - borderless: false, - format: FormatChoice::Bgra, - present_mode: PresentModeChoice::Fifo, - present_wait: PresentWaitChoice::Semaphore, - resize_at: None, - resize_width: None, - resize_height: None, - } - } - } - - impl Options { - pub fn from_env() -> Self { - let mut options = Options::default(); - let mut args = std::env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--frames" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.frames = Some(value); - } - } - "--width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.width = value; - } - } - "--height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.height = value; - } - } - "--borderless" => options.borderless = true, - "--windowed" => options.borderless = false, - "--format" => { - if let Some(value) = args.next() { - options.format = match value.to_ascii_lowercase().as_str() { - "rgba" => FormatChoice::Rgba, - _ => FormatChoice::Bgra, - }; - } - } - "--present-mode" => { - if let Some(value) = args.next() { - options.present_mode = match value.to_ascii_lowercase().as_str() { - "mailbox" => PresentModeChoice::Mailbox, - "immediate" => PresentModeChoice::Immediate, - _ => PresentModeChoice::Fifo, - }; - } - } - "--present-wait" => { - if let Some(value) = args.next() { - options.present_wait = match value.to_ascii_lowercase().as_str() { - "none" => PresentWaitChoice::None, - _ => PresentWaitChoice::Semaphore, - }; - } - } - "--resize-at" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.resize_at = Some(value); - } - } - "--resize-width" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.resize_width = Some(value); - } - } - "--resize-height" => { - if let Some(value) = args.next().and_then(|v| v.parse().ok()) { - options.resize_height = Some(value); - } - } - _ => {} - } - } - options - } - } -} - -#[cfg(windows)] -mod windows_impl { - use super::cli::{FormatChoice, Options, PresentModeChoice, PresentWaitChoice}; - use super::pattern; - use ash::{khr, vk}; - use std::error::Error; - use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM}; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - use windows::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, - GetClientRect, MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW, - SW_SHOW, ShowWindow, TranslateMessage, WM_DESTROY, WM_QUIT, WNDCLASSEXW, - WS_OVERLAPPEDWINDOW, WS_POPUP, WS_VISIBLE, - }; - use windows::core::w; - - type DynError = Box; - - extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - unsafe { - match msg { - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } - } - - unsafe fn create_window( - width: u32, - height: u32, - borderless: bool, - ) -> Result<(HWND, isize), DynError> { - unsafe { - let instance = GetModuleHandleW(None)?; - let class_name = w!("FluxerVulkanFixture"); - let wc = WNDCLASSEXW { - cbSize: size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance.into(), - lpszClassName: class_name, - ..Default::default() - }; - RegisterClassExW(&wc); - let style = if borderless { - WS_POPUP | WS_VISIBLE - } else { - WS_OVERLAPPEDWINDOW | WS_VISIBLE - }; - let hwnd = CreateWindowExW( - Default::default(), - class_name, - w!("Fluxer Vulkan Present Fixture"), - style, - CW_USEDEFAULT, - CW_USEDEFAULT, - width as i32, - height as i32, - None, - None, - Some(instance.into()), - None, - )?; - let _ = ShowWindow(hwnd, SW_SHOW); - Ok((hwnd, instance.0 as isize)) - } - } - - unsafe fn client_extent(hwnd: HWND) -> vk::Extent2D { - unsafe { - let mut rect = RECT::default(); - let _ = GetClientRect(hwnd, &mut rect); - vk::Extent2D { - width: (rect.right - rect.left).max(1) as u32, - height: (rect.bottom - rect.top).max(1) as u32, - } - } - } - - unsafe fn resize_window(hwnd: HWND, width: u32, height: u32) { - unsafe { - let _ = MoveWindow(hwnd, 0, 0, width as i32, height as i32, true); - } - } - - fn pick_surface_format( - formats: &[vk::SurfaceFormatKHR], - choice: FormatChoice, - ) -> Result { - let preferred: [vk::Format; 2] = match choice { - FormatChoice::Bgra => [vk::Format::B8G8R8A8_UNORM, vk::Format::B8G8R8A8_SRGB], - FormatChoice::Rgba => [vk::Format::R8G8B8A8_UNORM, vk::Format::R8G8B8A8_SRGB], - }; - let any_8bit = [ - vk::Format::B8G8R8A8_UNORM, - vk::Format::R8G8B8A8_UNORM, - vk::Format::B8G8R8A8_SRGB, - vk::Format::R8G8B8A8_SRGB, - ]; - formats - .iter() - .copied() - .find(|f| preferred.contains(&f.format)) - .or_else(|| { - formats - .iter() - .copied() - .find(|f| any_8bit.contains(&f.format)) - }) - .or_else(|| formats.first().copied()) - .ok_or_else(|| "surface advertised no formats".into()) - } - - fn pick_present_mode( - available: &[vk::PresentModeKHR], - choice: PresentModeChoice, - ) -> vk::PresentModeKHR { - let wanted = match choice { - PresentModeChoice::Fifo => vk::PresentModeKHR::FIFO, - PresentModeChoice::Mailbox => vk::PresentModeKHR::MAILBOX, - PresentModeChoice::Immediate => vk::PresentModeKHR::IMMEDIATE, - }; - if available.contains(&wanted) { - wanted - } else { - vk::PresentModeKHR::FIFO - } - } - - struct Swapchain { - handle: vk::SwapchainKHR, - images: Vec, - command_buffers: Vec, - extent: vk::Extent2D, - format: vk::Format, - } - - impl Swapchain { - #[allow(clippy::too_many_arguments)] - unsafe fn create( - device: &ash::Device, - swapchain_device: &khr::swapchain::Device, - surface_instance: &khr::surface::Instance, - physical_device: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - command_pool: vk::CommandPool, - surface_format: vk::SurfaceFormatKHR, - present_mode: vk::PresentModeKHR, - requested_extent: vk::Extent2D, - old: vk::SwapchainKHR, - ) -> Result { - unsafe { - let surface_caps = surface_instance - .get_physical_device_surface_capabilities(physical_device, surface)?; - - let extent = if surface_caps.current_extent.width != u32::MAX { - surface_caps.current_extent - } else { - vk::Extent2D { - width: requested_extent.width.clamp( - surface_caps.min_image_extent.width, - surface_caps.max_image_extent.width, - ), - height: requested_extent.height.clamp( - surface_caps.min_image_extent.height, - surface_caps.max_image_extent.height, - ), - } - }; - - let image_count = - surface_caps - .min_image_count - .max(2) - .min(if surface_caps.max_image_count == 0 { - u32::MAX - } else { - surface_caps.max_image_count - }); - - let swapchain_info = vk::SwapchainCreateInfoKHR::default() - .surface(surface) - .min_image_count(image_count) - .image_format(surface_format.format) - .image_color_space(surface_format.color_space) - .image_extent(extent) - .image_array_layers(1) - .image_usage( - vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_DST, - ) - .image_sharing_mode(vk::SharingMode::EXCLUSIVE) - .pre_transform(surface_caps.current_transform) - .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) - .present_mode(present_mode) - .clipped(true) - .old_swapchain(old); - let handle = swapchain_device.create_swapchain(&swapchain_info, None)?; - let images = swapchain_device.get_swapchain_images(handle)?; - - let alloc_info = vk::CommandBufferAllocateInfo::default() - .command_pool(command_pool) - .level(vk::CommandBufferLevel::PRIMARY) - .command_buffer_count(images.len() as u32); - let command_buffers = device.allocate_command_buffers(&alloc_info)?; - - Ok(Self { - handle, - images, - command_buffers, - extent, - format: surface_format.format, - }) - } - } - - unsafe fn destroy( - self, - device: &ash::Device, - swapchain_device: &khr::swapchain::Device, - command_pool: vk::CommandPool, - ) { - unsafe { - if !self.command_buffers.is_empty() { - device.free_command_buffers(command_pool, &self.command_buffers); - } - swapchain_device.destroy_swapchain(self.handle, None); - } - } - } - - pub fn run() -> Result<(), DynError> { - let options = Options::from_env(); - unsafe { - let (hwnd, hinstance) = - create_window(options.width, options.height, options.borderless)?; - - println!("HWND={}", hwnd.0 as isize); - use std::io::Write; - let _ = std::io::stdout().flush(); - - let entry = ash::Entry::load()?; - - let app_name = c"fluxer-vulkan-present-fixture"; - let app_info = vk::ApplicationInfo::default() - .application_name(app_name) - .api_version(vk::API_VERSION_1_1); - let instance_extensions = [ - khr::surface::NAME.as_ptr(), - khr::win32_surface::NAME.as_ptr(), - ]; - let instance_info = vk::InstanceCreateInfo::default() - .application_info(&app_info) - .enabled_extension_names(&instance_extensions); - let instance = entry.create_instance(&instance_info, None)?; - - let surface_instance = khr::surface::Instance::new(&entry, &instance); - let win32_surface = khr::win32_surface::Instance::new(&entry, &instance); - - let surface_info = vk::Win32SurfaceCreateInfoKHR::default() - .hinstance(hinstance) - .hwnd(hwnd.0 as isize); - let surface = win32_surface.create_win32_surface(&surface_info, None)?; - - let physical_devices = instance.enumerate_physical_devices()?; - let (physical_device, queue_family_index) = physical_devices - .iter() - .find_map(|&physical_device| { - let families = - instance.get_physical_device_queue_family_properties(physical_device); - families.iter().enumerate().find_map(|(index, family)| { - let index = index as u32; - let graphics = family.queue_flags.contains(vk::QueueFlags::GRAPHICS); - let present = surface_instance - .get_physical_device_surface_support(physical_device, index, surface) - .unwrap_or(false); - (graphics && present).then_some((physical_device, index)) - }) - }) - .ok_or("no Vulkan device with a graphics+present queue family")?; - - let queue_priorities = [1.0_f32]; - let queue_info = vk::DeviceQueueCreateInfo::default() - .queue_family_index(queue_family_index) - .queue_priorities(&queue_priorities); - let device_extensions = [khr::swapchain::NAME.as_ptr()]; - let queue_infos = [queue_info]; - let device_info = vk::DeviceCreateInfo::default() - .queue_create_infos(&queue_infos) - .enabled_extension_names(&device_extensions); - let device = instance.create_device(physical_device, &device_info, None)?; - let queue = device.get_device_queue(queue_family_index, 0); - let swapchain_device = khr::swapchain::Device::new(&instance, &device); - - let formats = - surface_instance.get_physical_device_surface_formats(physical_device, surface)?; - let surface_format = pick_surface_format(&formats, options.format)?; - let available_present_modes = surface_instance - .get_physical_device_surface_present_modes(physical_device, surface)?; - let present_mode = pick_present_mode(&available_present_modes, options.present_mode); - eprintln!( - "vulkan-present-fixture: vkFormat={} vkColorSpace={} vkPresentMode={}", - surface_format.format.as_raw(), - surface_format.color_space.as_raw(), - present_mode.as_raw() - ); - - let pool_info = vk::CommandPoolCreateInfo::default() - .queue_family_index(queue_family_index) - .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); - let command_pool = device.create_command_pool(&pool_info, None)?; - - let mut swapchain = Swapchain::create( - &device, - &swapchain_device, - &surface_instance, - physical_device, - surface, - command_pool, - surface_format, - present_mode, - client_extent(hwnd), - vk::SwapchainKHR::null(), - )?; - - let image_available = - device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?; - let render_finished = - device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?; - let in_flight = device.create_fence( - &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED), - None, - )?; - - let subresource_range = vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }; - - let recreate_swapchain = |swapchain: &mut Swapchain| -> Result<(), DynError> { - device.device_wait_idle()?; - let new = Swapchain::create( - &device, - &swapchain_device, - &surface_instance, - physical_device, - surface, - command_pool, - surface_format, - present_mode, - client_extent(hwnd), - swapchain.handle, - )?; - eprintln!( - "vulkan-present-fixture: swapchain recreated extent={}x{} vkFormat={}", - new.extent.width, - new.extent.height, - new.format.as_raw() - ); - let old = std::mem::replace(swapchain, new); - old.destroy(&device, &swapchain_device, command_pool); - Ok(()) - }; - - let mut frame_index: u64 = 0; - let mut resized = false; - let mut msg = MSG::default(); - 'render: loop { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { - if msg.message == WM_QUIT { - break 'render; - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - if let Some(at) = options.resize_at { - if !resized && frame_index >= at { - if let (Some(width), Some(height)) = - (options.resize_width, options.resize_height) - { - resize_window(hwnd, width, height); - } - recreate_swapchain(&mut swapchain)?; - resized = true; - } - } - - device.wait_for_fences(&[in_flight], true, u64::MAX)?; - - let (image_index, suboptimal) = match swapchain_device.acquire_next_image( - swapchain.handle, - u64::MAX, - image_available, - vk::Fence::null(), - ) { - Ok(value) => value, - Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { - recreate_swapchain(&mut swapchain)?; - continue; - } - Err(error) => return Err(error.into()), - }; - - device.reset_fences(&[in_flight])?; - - let command_buffer = swapchain.command_buffers[image_index as usize]; - let image = swapchain.images[image_index as usize]; - device - .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())?; - device.begin_command_buffer( - command_buffer, - &vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), - )?; - - let to_transfer = vk::ImageMemoryBarrier::default() - .src_access_mask(vk::AccessFlags::empty()) - .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE) - .old_layout(vk::ImageLayout::UNDEFINED) - .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) - .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .image(image) - .subresource_range(subresource_range); - device.cmd_pipeline_barrier( - command_buffer, - vk::PipelineStageFlags::TOP_OF_PIPE, - vk::PipelineStageFlags::TRANSFER, - vk::DependencyFlags::empty(), - &[], - &[], - &[to_transfer], - ); - - let clear = pattern::clear_colour(frame_index); - let clear_value = vk::ClearColorValue { float32: clear }; - device.cmd_clear_color_image( - command_buffer, - image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &clear_value, - &[subresource_range], - ); - - let to_present = vk::ImageMemoryBarrier::default() - .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) - .dst_access_mask(vk::AccessFlags::empty()) - .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) - .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) - .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .image(image) - .subresource_range(subresource_range); - device.cmd_pipeline_barrier( - command_buffer, - vk::PipelineStageFlags::TRANSFER, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::DependencyFlags::empty(), - &[], - &[], - &[to_present], - ); - device.end_command_buffer(command_buffer)?; - - let wait_semaphores = [image_available]; - let wait_stages = [vk::PipelineStageFlags::TRANSFER]; - let command_buffers = [command_buffer]; - let mut submit = vk::SubmitInfo::default() - .wait_semaphores(&wait_semaphores) - .wait_dst_stage_mask(&wait_stages) - .command_buffers(&command_buffers); - let signal_semaphores = [render_finished]; - if options.present_wait == PresentWaitChoice::Semaphore { - submit = submit.signal_semaphores(&signal_semaphores); - } - device.queue_submit(queue, &[submit], in_flight)?; - - let swapchains = [swapchain.handle]; - let image_indices = [image_index]; - if options.present_wait == PresentWaitChoice::None { - device.wait_for_fences(&[in_flight], true, u64::MAX)?; - } - let mut present_info = vk::PresentInfoKHR::default() - .swapchains(&swapchains) - .image_indices(&image_indices); - if options.present_wait == PresentWaitChoice::Semaphore { - present_info = present_info.wait_semaphores(&signal_semaphores); - } - let present_result = swapchain_device.queue_present(queue, &present_info); - let needs_recreate = match present_result { - Ok(present_suboptimal) => present_suboptimal || suboptimal, - Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => true, - Err(error) => return Err(error.into()), - }; - if needs_recreate { - recreate_swapchain(&mut swapchain)?; - } - - frame_index += 1; - if let Some(limit) = options.frames { - if frame_index >= limit { - break 'render; - } - } - } - - let _ = device.device_wait_idle(); - device.destroy_fence(in_flight, None); - device.destroy_semaphore(render_finished, None); - device.destroy_semaphore(image_available, None); - swapchain.destroy(&device, &swapchain_device, command_pool); - device.destroy_command_pool(command_pool, None); - surface_instance.destroy_surface(surface, None); - device.destroy_device(None); - instance.destroy_instance(None); - Ok(()) - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/tests/windows_encoder_attach.rs b/fluxer_desktop/native/win-game-capture/tests/windows_encoder_attach.rs deleted file mode 100644 index dd7998305..000000000 --- a/fluxer_desktop/native/win-game-capture/tests/windows_encoder_attach.rs +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#![cfg(target_os = "windows")] -use fluxer_win_game_capture::{EncoderAttachError, EncoderAttachment}; -use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0}; -use windows::Win32::Graphics::Direct3D11::{ - D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_BGRA_SUPPORT, - D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_RESOURCE_MISC_SHARED, D3D11_SDK_VERSION, - D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDevice, ID3D11Device, - ID3D11DeviceContext, ID3D11Multithread, ID3D11Texture2D, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC}; -use windows::core::Interface; - -fn try_create_device() -> Option<(ID3D11Device, ID3D11DeviceContext)> { - let feature_levels = [D3D_FEATURE_LEVEL_11_0]; - let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; - let mut device: Option = None; - let mut context: Option = None; - let result = unsafe { - D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_HARDWARE, - Default::default(), - flags, - Some(&feature_levels), - D3D11_SDK_VERSION, - Some(&mut device), - None, - Some(&mut context), - ) - }; - if result.is_err() { - return None; - } - let dev = device?; - let ctx = context?; - if let Ok(mt) = dev.cast::() { - let _ = unsafe { mt.SetMultithreadProtected(true) }; - } - Some((dev, ctx)) -} - -fn create_bgra_capture_texture( - device: &ID3D11Device, - width: u32, - height: u32, -) -> Option { - let desc = D3D11_TEXTURE2D_DESC { - Width: width, - Height: height, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - CPUAccessFlags: 0, - MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32, - }; - let mut tex = None; - unsafe { device.CreateTexture2D(&desc, None, Some(&mut tex)) }.ok()?; - tex -} - -#[test] -fn windows_attach_construct_and_detach() { - let _device = match try_create_device() { - Some(d) => d, - None => { - eprintln!("skip: no D3D11 device available"); - return; - } - }; - let attach = match EncoderAttachment::try_new(640, 480) { - Ok(a) => a, - Err(e) => { - eprintln!("skip: EncoderAttachment init failed: {e:?}"); - return; - } - }; - assert!(attach.is_attached(), "attached after construction"); - assert_eq!(attach.width(), 640, "width preserved"); - assert_eq!(attach.height(), 480, "height preserved"); - assert_eq!(attach.capacity(), 8, "capacity matches RING_SIZE"); - let stats = attach.stats(); - assert_eq!(stats.frames_submitted, 0, "fresh attach: no submissions"); - assert_eq!(stats.failed_blits, 0, "fresh attach: no failed blits"); - attach.detach(); - assert!(!attach.is_attached(), "post-detach not attached"); -} - -#[test] -fn submit_capture_frame_with_blit_records_stats() { - let (device, _context) = match try_create_device() { - Some(d) => d, - None => { - eprintln!("skip: no D3D11 device available"); - return; - } - }; - let attach = match EncoderAttachment::try_new(640, 480) { - Ok(a) => a, - Err(e) => { - eprintln!("skip: attach init failed: {e:?}"); - return; - } - }; - let capture = match create_bgra_capture_texture(&device, 640, 480) { - Some(t) => t, - None => { - eprintln!("skip: capture texture create failed"); - return; - } - }; - let result = attach.submit_capture_frame_with_blit(&capture, 640, 480); - let stats = attach.stats(); - match result { - Ok(()) => { - assert!( - stats.frames_submitted == 1, - "expected 1 submission, got {}", - stats.frames_submitted - ); - assert!( - stats.failed_blits == 0, - "expected 0 failed blits, got {}", - stats.failed_blits - ); - } - Err(EncoderAttachError::BlitFailed) => { - assert!( - stats.failed_blits >= 1, - "expected >=1 failed blits, got {}", - stats.failed_blits - ); - } - Err(EncoderAttachError::DeviceUnavailable) => { - eprintln!("device unavailable from ring backend (driver constraint)"); - } - Err(other) => { - eprintln!("submit_capture_frame_with_blit unexpected error: {other:?}"); - } - } -} - -#[test] -fn submit_notify_back_pressure_increments_drops_after_capacity() { - let _device = match try_create_device() { - Some(d) => d, - None => { - eprintln!("skip: no D3D11 device available"); - return; - } - }; - let attach = match EncoderAttachment::try_new(64, 64) { - Ok(a) => a, - Err(e) => { - eprintln!("skip: attach init failed: {e:?}"); - return; - } - }; - let total = (attach.capacity() as u64) * 2; - for _ in 0..total { - attach.submit_notify().expect("notify ok"); - } - let stats = attach.stats(); - assert!( - stats.frames_submitted >= attach.capacity() as u64, - "many submissions accepted" - ); - assert!(stats.frames_dropped > 0, "back pressure drops oldest"); - assert_eq!( - stats.ring_full_events, stats.frames_dropped, - "drops==ring_full_events" - ); -} diff --git a/fluxer_desktop/package.json b/fluxer_desktop/package.json index 90004261a..e0406e947 100644 --- a/fluxer_desktop/package.json +++ b/fluxer_desktop/package.json @@ -12,9 +12,7 @@ "build": "node scripts/build.mjs", "set-channel": "cargo run --manifest-path ../tools/ci/Cargo.toml -- build-desktop --step set_build_channel", "test": "cargo test --manifest-path native/rust/Cargo.toml", - "typecheck": "tsgo --noEmit", - "test:native-media": "node scripts/native-media-integration.mjs", - "test:native-media:strict": "node scripts/native-media-integration.mjs --strict" + "typecheck": "tsgo --noEmit" }, "dependencies": { "@fluxer/voice_engine_v2": "workspace:*", diff --git a/fluxer_desktop/scripts/build.mjs b/fluxer_desktop/scripts/build.mjs index ed068b13b..91425a4d5 100644 --- a/fluxer_desktop/scripts/build.mjs +++ b/fluxer_desktop/scripts/build.mjs @@ -136,7 +136,7 @@ function addFilesFromDirectory(files, packageDir, relativeDir, predicate) { function collectRuntimeArtifactPaths(packageDir) { const artifacts = new Set(); - for (const fileName of ['index.js', 'index.d.ts', 'binding.js', 'binding.d.ts', 'loader-diagnostics.cjs']) { + for (const fileName of ['index.js', 'index.d.ts', 'binding.js', 'binding.d.ts', 'loader-diagnostics.cjs', 'pure.cjs']) { if (fs.existsSync(path.join(packageDir, fileName))) { artifacts.add(fileName); } @@ -226,6 +226,7 @@ function expectedNativeRuntimeArtifacts(platform = process.platform, arch = proc artifacts.push({ label: '@fluxer/webauthn', relativePath: `webauthn.${tag}.node`, + runtimeFiles: ['index.js', 'loader-diagnostics.cjs', 'pure.cjs'], }); artifacts.push({ label: '@fluxer/webrtc-sender', @@ -260,6 +261,7 @@ function expectedNativeRuntimeArtifacts(platform = process.platform, arch = proc artifacts.push({ label: '@fluxer/platform-info', relativePath: `platform-info.${tag}.node`, + runtimeFiles: ['index.js', 'loader-diagnostics.cjs', 'pure.cjs'], }); } else if (platform === 'win32') { artifacts.push({ @@ -286,6 +288,7 @@ function expectedNativeRuntimeArtifacts(platform = process.platform, arch = proc artifacts.push({ label: '@fluxer/platform-info', relativePath: `platform-info.${tag}.node`, + runtimeFiles: ['index.js', 'loader-diagnostics.cjs', 'pure.cjs'], }); } else if (platform === 'linux') { artifacts.push({ @@ -319,6 +322,7 @@ function expectedNativeRuntimeArtifacts(platform = process.platform, arch = proc artifacts.push({ label: '@fluxer/platform-info', relativePath: `platform-info.${tag}.node`, + runtimeFiles: ['index.js', 'loader-diagnostics.cjs', 'pure.cjs'], }); } return artifacts; diff --git a/fluxer_desktop/scripts/native-media-integration.mjs b/fluxer_desktop/scripts/native-media-integration.mjs deleted file mode 100644 index e7592dee5..000000000 --- a/fluxer_desktop/scripts/native-media-integration.mjs +++ /dev/null @@ -1,463 +0,0 @@ -#!/usr/bin/env node -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {spawn} from 'node:child_process'; -import {existsSync} from 'node:fs'; -import {mkdir, readFile, writeFile} from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; - -const repoDir = new URL('..', import.meta.url).pathname; -const workspaceDir = path.resolve(repoDir, '..'); -const args = new Set(process.argv.slice(2)); -const mode = args.has('--strict') - ? 'strict' - : args.has('--smoke') - ? 'smoke' - : (process.env.FLUXER_NATIVE_MEDIA_MODE || 'smoke').toLowerCase(); -const strict = mode === 'strict'; -const packageManager = process.env.FLUXER_NATIVE_MEDIA_PACKAGE_MANAGER || 'pnpm'; -const reportDir = - process.env.FLUXER_NATIVE_MEDIA_REPORT_DIR || - path.join(repoDir, 'native-media-reports', new Date().toISOString().replace(/[:.]/g, '-')); -const livekitEnabled = strict || process.env.FLUXER_NATIVE_MEDIA_LIVEKIT !== '0'; -const electronBuildEnabled = process.env.FLUXER_NATIVE_MEDIA_ELECTRON_BUILD === '1' || strict; -const strictCodecMatrix = ['vp8', 'vp9', 'h264', 'hevc', 'av1']; -const strictLiveKitFeatureFlags = [ - 'LIVEKIT_ENABLE_SECOND_PUBLISHER', - 'LIVEKIT_ENABLE_MICROPHONE', - 'LIVEKIT_ENABLE_SCREEN_AUDIO', - 'LIVEKIT_ENABLE_CAMERA', - 'LIVEKIT_ENABLE_DATA_PACKET', - 'LIVEKIT_ENABLE_SUBSCRIPTION_CYCLE', -]; -const strictLiveKitCredentialGroups = [ - ['LIVEKIT_URL', 'LIVEKIT_WS_URL'], - ['LIVEKIT_API_KEY'], - ['LIVEKIT_API_SECRET', 'LIVEKIT_SECRET'], -]; -const validModes = new Set(['smoke', 'strict']); - -function scriptCommand(directory, scriptName) { - return `${packageManager} --dir ${directory} ${scriptName}`; -} - -function shellQuote(value) { - if (process.platform === 'win32') return `"${String(value).replace(/"/g, '""')}"`; - return `'${String(value).replace(/'/g, "'\\''")}'`; -} - -function findToolBinary(name) { - const executable = process.platform === 'win32' ? `${name}.cmd` : name; - const candidates = [ - path.join(repoDir, 'node_modules', '.bin', executable), - path.join(workspaceDir, 'node_modules', '.bin', executable), - path.join(workspaceDir, 'node_modules', '.pnpm', 'node_modules', '.bin', executable), - ]; - const found = candidates.find((candidate) => existsSync(candidate)); - return found ? shellQuote(found) : name; -} - -function withDefaultEnv(defaults) { - const env = {...process.env}; - if (env.CARGO_INCREMENTAL === undefined || env.CARGO_INCREMENTAL === '') { - env.CARGO_INCREMENTAL = '0'; - } - for (const [key, value] of Object.entries(defaults)) { - if (env[key] === undefined || env[key] === '') { - env[key] = String(value); - } - } - return env; -} - -function commandEnv(defaults, overrides = {}) { - const env = withDefaultEnv(defaults); - for (const [key, value] of Object.entries(overrides)) { - env[key] = String(value); - } - return env; -} - -function envExplicitFalse(name) { - const value = process.env[name]; - return value !== undefined && /^(0|false|no|off|disabled|skip)$/i.test(value.trim()); -} - -function envPresent(names) { - return names.some((name) => process.env[name]?.trim()); -} - -function parseCodecEnv(name) { - const value = process.env[name]; - if (!value?.trim()) return null; - return value - .split(',') - .map((entry) => entry.trim().toLowerCase()) - .filter(Boolean); -} - -function codecListIncludes(codecs, codec) { - if (codec === 'hevc') return codecs.includes('hevc') || codecs.includes('h265'); - return codecs.includes(codec); -} - -function platformNativeCommands(platform = process.platform) { - if (platform === 'linux') { - return [ - { - name: 'linux-screen-capture-build', - command: scriptCommand('native/linux-screen-capture', 'build'), - category: 'platform-native', - }, - { - name: 'linux-screen-capture-tests', - command: scriptCommand('native/linux-screen-capture', 'test'), - category: 'platform-native', - }, - { - name: 'linux-audio-capture-build', - command: scriptCommand('native/linux-audio-capture', 'build'), - category: 'platform-native', - }, - { - name: 'linux-audio-capture-tests', - command: scriptCommand('native/linux-audio-capture', 'test'), - category: 'platform-native', - }, - ]; - } - - if (platform === 'darwin') { - return [ - { - name: 'mac-screen-capture-build', - command: scriptCommand('native/mac-screen-capture', 'build'), - category: 'platform-native', - }, - { - name: 'mac-screen-capture-tests', - command: scriptCommand('native/mac-screen-capture', 'test'), - category: 'platform-native', - }, - { - name: 'mac-app-audio-build', - command: scriptCommand('native/mac-app-audio', 'build'), - category: 'platform-native', - }, - { - name: 'mac-app-audio-rust-tests', - command: scriptCommand('native/mac-app-audio', 'test:cargo'), - category: 'platform-native', - }, - { - name: 'mac-app-audio-js-tests', - command: `${findToolBinary('vitest')} run`, - cwd: 'native/mac-app-audio', - category: 'platform-native', - }, - ]; - } - - if (platform === 'win32') { - return [ - { - name: 'win-game-capture-build', - command: scriptCommand('native/win-game-capture', 'build'), - category: 'platform-native', - }, - { - name: 'win-game-capture-tests', - command: scriptCommand('native/win-game-capture', 'test'), - category: 'platform-native', - }, - { - name: 'win-game-capture-fixtures', - command: scriptCommand('native/win-game-capture', 'test:fixtures'), - category: 'platform-native', - }, - { - name: 'win-process-loopback-build', - command: scriptCommand('native/win-process-loopback', 'build'), - category: 'platform-native', - }, - { - name: 'win-process-loopback-tests', - command: scriptCommand('native/win-process-loopback', 'test'), - category: 'platform-native', - }, - ]; - } - - return []; -} - -function commandPlan() { - const commands = [ - { - name: 'main-process-native-media-unit-tests', - command: 'node --test src/main/NativeVoiceEngine.test.mjs src/main/NativeScreenCapture.test.mjs', - category: 'shared', - }, - {name: 'desktop-typecheck', command: `${packageManager} typecheck`, category: 'shared'}, - {name: 'webrtc-sender-build', command: scriptCommand('native/webrtc-sender', 'build'), category: 'native-sender'}, - {name: 'webrtc-sender-tests', command: scriptCommand('native/webrtc-sender', 'test'), category: 'native-sender'}, - ]; - - if (livekitEnabled) { - commands.push({ - name: 'webrtc-sender-livekit-harness', - command: scriptCommand('native/webrtc-sender', 'test:livekit'), - category: 'livekit', - env: commandEnv( - { - LIVEKIT_HARNESS_STRICT: strict ? '1' : '0', - LIVEKIT_ENABLE_SECOND_PUBLISHER: strict ? '1' : '0', - LIVEKIT_ENABLE_SCREEN_AUDIO: strict ? '1' : '0', - LIVEKIT_ENABLE_DATA_PACKET: strict ? '1' : '0', - LIVEKIT_ENABLE_SUBSCRIPTION_CYCLE: strict ? '1' : '0', - LIVEKIT_ENABLE_CAMERA: strict ? '1' : '0', - LIVEKIT_ENABLE_MICROPHONE: '1', - LIVEKIT_SCREEN_CODECS: strict ? strictCodecMatrix.join(',') : 'vp8', - LIVEKIT_EXPECT_SCREEN_CODECS: strict ? strictCodecMatrix.join(',') : 'vp8', - LIVEKIT_HARNESS_REPORT_PATH: path.join(reportDir, 'livekit-harness.json'), - }, - strict - ? { - FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED: '1', - LIVEKIT_REQUIRED: '1', - LIVEKIT_HARNESS_STRICT: '1', - LIVEKIT_ENABLE_SECOND_PUBLISHER: '1', - LIVEKIT_ENABLE_MICROPHONE: '1', - LIVEKIT_ENABLE_SCREEN_AUDIO: '1', - LIVEKIT_ENABLE_CAMERA: '1', - LIVEKIT_ENABLE_DATA_PACKET: '1', - LIVEKIT_ENABLE_SUBSCRIPTION_CYCLE: '1', - } - : {}, - ), - }); - } - - commands.push(...platformNativeCommands()); - - if (electronBuildEnabled) { - commands.push({name: 'electron-build', command: `${packageManager} build`, category: 'electron'}); - } - - return commands; -} - -function makeGate(name, status, details = {}) { - return { - name, - status, - ...details, - }; -} - -function modeGate() { - return makeGate( - 'mode-valid', - validModes.has(mode) ? 'pass' : 'fail', - validModes.has(mode) ? {mode} : {mode, issues: [`FLUXER_NATIVE_MEDIA_MODE must be smoke or strict; got ${mode}`]}, - ); -} - -async function documentationGate() { - const docPath = path.join(repoDir, 'native', 'NATIVE_MEDIA_INTEGRATION.md'); - const packagePath = path.join(repoDir, 'package.json'); - const missing = []; - try { - const [doc, packageJson] = await Promise.all([readFile(docPath, 'utf8'), readFile(packagePath, 'utf8')]); - const packageScripts = JSON.parse(packageJson).scripts ?? {}; - const requiredSnippets = [ - 'FLUXER_NATIVE_MEDIA_MODE=strict', - 'FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED=1', - 'LIVEKIT_URL', - 'LIVEKIT_API_KEY', - 'LIVEKIT_API_SECRET', - 'FLUXER_NATIVE_MEDIA_REPORT_DIR', - 'test:native-media:strict', - 'fail-hard', - 'macOS host', - 'Windows 11 Parallels', - 'Linux Parallels', - ]; - for (const snippet of requiredSnippets) { - if (!doc.includes(snippet)) missing.push(`native/NATIVE_MEDIA_INTEGRATION.md missing ${snippet}`); - } - if (packageScripts['test:native-media'] !== 'node scripts/native-media-integration.mjs') { - missing.push('package.json script test:native-media does not point at native-media-integration.mjs'); - } - if (packageScripts['test:native-media:strict'] !== 'node scripts/native-media-integration.mjs --strict') { - missing.push('package.json script test:native-media:strict does not run strict native media integration'); - } - } catch (error) { - missing.push(`documentation/package script validation failed: ${error.message}`); - } - return makeGate('documentation-current', missing.length === 0 ? 'pass' : strict ? 'fail' : 'warn', {missing}); -} - -function strictPrerequisiteGates() { - if (!strict) { - return [makeGate('strict-prerequisites', 'skip', {reason: 'smoke mode'})]; - } - - const gates = []; - const liveKitIssues = []; - if (envExplicitFalse('FLUXER_NATIVE_MEDIA_LIVEKIT')) { - liveKitIssues.push('FLUXER_NATIVE_MEDIA_LIVEKIT=0 is not allowed in strict mode'); - } - for (const names of strictLiveKitCredentialGroups) { - if (!envPresent(names)) liveKitIssues.push(`missing ${names.join(' or ')}`); - } - for (const flag of strictLiveKitFeatureFlags) { - if (envExplicitFalse(flag)) liveKitIssues.push(`${flag}=0 is not allowed in strict mode`); - } - for (const codecEnvName of ['LIVEKIT_SCREEN_CODECS', 'LIVEKIT_EXPECT_SCREEN_CODECS']) { - const codecs = parseCodecEnv(codecEnvName); - if (!codecs) continue; - for (const codec of strictCodecMatrix) { - if (!codecListIncludes(codecs, codec)) { - liveKitIssues.push(`${codecEnvName} must include ${codec} in strict mode`); - } - } - } - gates.push(makeGate('livekit-required', liveKitIssues.length === 0 ? 'pass' : 'fail', {issues: liveKitIssues})); - - const electronIssues = []; - if (envExplicitFalse('FLUXER_NATIVE_MEDIA_ELECTRON_BUILD')) { - electronIssues.push('FLUXER_NATIVE_MEDIA_ELECTRON_BUILD=0 is not allowed in strict mode'); - } - gates.push( - makeGate('electron-build-required', electronIssues.length === 0 ? 'pass' : 'fail', {issues: electronIssues}), - ); - - const platformCommands = platformNativeCommands(); - gates.push( - makeGate( - 'platform-native-required', - platformCommands.length > 0 ? 'pass' : 'fail', - platformCommands.length > 0 - ? {platform: process.platform, commands: platformCommands.map((command) => command.name)} - : { - platform: process.platform, - issues: [`strict mode is only defined for linux, darwin, and win32; got ${process.platform}`], - }, - ), - ); - - gates.push(makeGate('summary-report-required', 'pass', {summaryPath: path.join(reportDir, 'summary.json')})); - return gates; -} - -function commandPlanGate(commands) { - if (!strict) return makeGate('strict-command-plan', 'skip', {reason: 'smoke mode'}); - - const names = new Set(commands.map((command) => command.name)); - const missing = []; - if (!names.has('webrtc-sender-livekit-harness')) missing.push('webrtc-sender-livekit-harness'); - if (!names.has('electron-build')) missing.push('electron-build'); - for (const command of platformNativeCommands()) { - if (!names.has(command.name)) missing.push(command.name); - } - return makeGate('strict-command-plan', missing.length === 0 ? 'pass' : 'fail', {missing}); -} - -async function evaluateGates(commands) { - return [modeGate(), await documentationGate(), ...strictPrerequisiteGates(), commandPlanGate(commands)]; -} - -function runCommand(step) { - const startedAtMs = Date.now(); - return new Promise((resolve) => { - console.log(`[native-media] ${step.name}: ${step.command}`); - const child = spawn(step.command, { - cwd: step.cwd ? path.join(repoDir, step.cwd) : repoDir, - env: step.env ?? withDefaultEnv({}), - shell: true, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - }); - let stdoutTail = ''; - let stderrTail = ''; - const appendTail = (current, chunk) => `${current}${chunk}`.slice(-12_000); - child.stdout.on('data', (chunk) => { - const text = chunk.toString(); - stdoutTail = appendTail(stdoutTail, text); - process.stdout.write(text); - }); - child.stderr.on('data', (chunk) => { - const text = chunk.toString(); - stderrTail = appendTail(stderrTail, text); - process.stderr.write(text); - }); - child.on('close', (code, signal) => { - const endedAtMs = Date.now(); - resolve({ - name: step.name, - command: step.command, - category: step.category ?? 'other', - status: code === 0 ? 'pass' : 'fail', - code, - signal, - startedAt: new Date(startedAtMs).toISOString(), - endedAt: new Date(endedAtMs).toISOString(), - durationMs: endedAtMs - startedAtMs, - stdoutTail, - stderrTail, - }); - }); - }); -} - -async function writeSummary({startedAtMs, commands, results, gates, failedGate}) { - const failedCommand = results.find((result) => result.status === 'fail'); - const report = { - status: failedGate || failedCommand ? 'fail' : 'pass', - mode, - strict, - platform: process.platform, - arch: process.arch, - hostname: os.hostname(), - startedAt: new Date(startedAtMs).toISOString(), - endedAt: new Date().toISOString(), - reportDir, - gates, - plannedCommands: commands.map((command) => ({ - name: command.name, - command: command.command, - category: command.category ?? 'other', - cwd: command.cwd ?? '.', - })), - commands: results, - }; - const summaryPath = path.join(reportDir, 'summary.json'); - await writeFile(summaryPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); - console.log(`[native-media] summary: ${summaryPath}`); - return report.status === 'pass' ? 0 : 1; -} - -async function main() { - await mkdir(reportDir, {recursive: true}); - const startedAtMs = Date.now(); - const commands = commandPlan(); - const gates = await evaluateGates(commands); - const failedGate = gates.find((gate) => gate.status === 'fail'); - const results = []; - if (failedGate) { - console.error(`[native-media] FAIL: ${failedGate.name}`); - return writeSummary({startedAtMs, commands, results, gates, failedGate}); - } - for (const command of commands) { - const result = await runCommand(command); - results.push(result); - if (result.status !== 'pass') { - break; - } - } - return writeSummary({startedAtMs, commands, results, gates}); -} - -process.exitCode = await main(); diff --git a/fluxer_gateway/test/cluster_multinode_SUITE.erl b/fluxer_gateway/test/cluster_multinode_SUITE.erl deleted file mode 100644 index 4c8798e3d..000000000 --- a/fluxer_gateway/test/cluster_multinode_SUITE.erl +++ /dev/null @@ -1,109 +0,0 @@ -%% SPDX-License-Identifier: AGPL-3.0-or-later - --module(cluster_multinode_SUITE). --typing([eqwalizer]). - --include_lib("common_test/include/ct.hrl"). --export([ - all/0, - init_per_suite/1, - end_per_suite/1, - init_per_testcase/2, - end_per_testcase/2 -]). --export([ - two_nodes_see_each_other/1, - owner_node_agrees_across_cluster/1, - nodedown_shrinks_membership/1, - remote_presence_fanout/1, - remote_dispatch_relay_direct/1, - remote_session_state_transfer/1, - remote_presence_cache_rebalance/1, - remote_guild_handoff_ships_state/1 -]). - -all() -> - [ - two_nodes_see_each_other, - owner_node_agrees_across_cluster, - nodedown_shrinks_membership, - remote_presence_fanout, - remote_dispatch_relay_direct, - remote_session_state_transfer, - remote_presence_cache_rebalance, - remote_guild_handoff_ships_state - ]. - -init_per_suite(Config) -> - case ensure_distribution() of - ok -> - true = erlang:set_cookie(node(), fluxer_cluster_ct), - Config; - {skip, Reason} -> - {skip, Reason} - end. - -ensure_distribution() -> - case net_kernel:get_state() of - #{started := no} -> - try_start_distribution(); - _ -> - ok - end. - -try_start_distribution() -> - Candidates = [ - controller, - list_to_atom("controller@127.0.0.1") - ], - try_start_distribution(Candidates). - -try_start_distribution([]) -> - {skip, - "could not start Erlang distribution (no working name); " - "see ensure_distribution/0 in cluster_multinode_SUITE"}; -try_start_distribution([Name | Rest]) -> - case net_kernel:start([Name, longnames]) of - {ok, _} -> ok; - {error, {already_started, _}} -> ok; - {error, _Reason} -> try_start_distribution(Rest) - end. - -end_per_suite(_Config) -> - ok. - -init_per_testcase(Case, Config) -> - [{case_name, Case} | Config]. - -end_per_testcase(_Case, Config) -> - case proplists:get_value(peers, Config, []) of - Peers when is_list(Peers) -> - lists:foreach(fun cluster_multinode_support:stop_peer/1, Peers); - _ -> - ok - end, - ok. - -two_nodes_see_each_other(Config) -> - cluster_multinode_basic_cases:two_nodes_see_each_other(Config). - -owner_node_agrees_across_cluster(Config) -> - cluster_multinode_basic_cases:owner_node_agrees_across_cluster(Config). - -nodedown_shrinks_membership(Config) -> - cluster_multinode_basic_cases:nodedown_shrinks_membership(Config). - -remote_presence_fanout(Config) -> - cluster_multinode_remote_cases:remote_presence_fanout(Config). - -remote_dispatch_relay_direct(Config) -> - cluster_multinode_remote_cases:remote_dispatch_relay_direct(Config). - -remote_session_state_transfer(Config) -> - cluster_multinode_remote_cases:remote_session_state_transfer(Config). - -remote_presence_cache_rebalance(Config) -> - cluster_multinode_remote_cases:remote_presence_cache_rebalance(Config). - -remote_guild_handoff_ships_state(Config) -> - cluster_multinode_remote_cases:remote_guild_handoff_ships_state(Config). diff --git a/fluxer_gateway/test/cluster_multinode_basic_cases.erl b/fluxer_gateway/test/cluster_multinode_basic_cases.erl deleted file mode 100644 index a953a4fc6..000000000 --- a/fluxer_gateway/test/cluster_multinode_basic_cases.erl +++ /dev/null @@ -1,101 +0,0 @@ -%% SPDX-License-Identifier: AGPL-3.0-or-later - --module(cluster_multinode_basic_cases). --typing([eqwalizer]). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --export([ - nodedown_shrinks_membership/1, - owner_node_agrees_across_cluster/1, - two_nodes_see_each_other/1 -]). - --spec two_nodes_see_each_other(list()) -> ok. -two_nodes_see_each_other(Config0) -> - {ok, Peer1, Node1} = start_peer(node1, Config0), - {ok, Peer2, Node2} = start_peer(node2, Config0), - Config = [{peers, [Peer1, Peer2]} | Config0], - ok = drive_membership(Peer1, [Node1, Node2]), - ok = drive_membership(Peer2, [Node1, Node2]), - Expected = lists:sort([Node1, Node2]), - wait_until( - fun() -> lists:sort(rpc_members(Peer1)) =:= Expected end, - "node1 membership" - ), - wait_until( - fun() -> lists:sort(rpc_members(Peer2)) =:= Expected end, - "node2 membership" - ), - ?assertEqual(2, rpc:call(Node1, gateway_cluster_membership, alive_count, [])), - ?assertEqual(2, rpc:call(Node2, gateway_cluster_membership, alive_count, [])), - ct:log("two_nodes_see_each_other: ~p", [Config]), - ok. - --spec owner_node_agrees_across_cluster(list()) -> ok. -owner_node_agrees_across_cluster(Config0) -> - {ok, Peer1, Node1} = start_peer(node1, Config0), - {ok, Peer2, Node2} = start_peer(node2, Config0), - _Config = [{peers, [Peer1, Peer2]} | Config0], - ok = drive_membership(Peer1, [Node1, Node2]), - ok = drive_membership(Peer2, [Node1, Node2]), - Expected = lists:sort([Node1, Node2]), - wait_until( - fun() -> lists:sort(rpc_members(Peer1)) =:= Expected end, - "convergence n1" - ), - wait_until( - fun() -> lists:sort(rpc_members(Peer2)) =:= Expected end, - "convergence n2" - ), - Keys = [<<"key-", (integer_to_binary(I))/binary>> || I <- lists:seq(1, 50)], - [ - ?assertEqual( - rpc:call(Node1, gateway_node_router, owner_node, [K]), - rpc:call(Node2, gateway_node_router, owner_node, [K]), - {owner_disagreement, K} - ) - || K <- Keys - ], - [assert_owner_in_cluster(Node1, [Node1, Node2], K) || K <- Keys], - ok. - --spec nodedown_shrinks_membership(list()) -> ok. -nodedown_shrinks_membership(Config0) -> - {ok, Peer1, Node1} = start_peer(node1, Config0), - {ok, Peer2, Node2} = start_peer(node2, Config0), - _Config = [{peers, [Peer1]} | Config0], - ok = drive_membership(Peer1, [Node1, Node2]), - ok = drive_membership(Peer2, [Node1, Node2]), - Expected = lists:sort([Node1, Node2]), - wait_until( - fun() -> lists:sort(rpc_members(Peer1)) =:= Expected end, - "initial n1 convergence" - ), - stop_peer(Peer2), - wait_until( - fun() -> rpc_members(Peer1) =:= [Node1] end, - "n1 shrinks back to self after nodedown" - ), - ?assertEqual(1, rpc:call(Node1, gateway_cluster_membership, alive_count, [])), - ok. - -drive_membership(Peer, Nodes) -> - cluster_multinode_support:drive_membership(Peer, Nodes). - -rpc_members(Peer) -> - cluster_multinode_support:rpc_members(Peer). - -start_peer(Name, Config) -> - cluster_multinode_support:start_peer(Name, Config). - -stop_peer(Peer) -> - cluster_multinode_support:stop_peer(Peer). - -wait_until(Fun, Label) -> - cluster_multinode_support:wait_until(Fun, Label). - -assert_owner_in_cluster(Node, ClusterNodes, Key) -> - Owner = rpc:call(Node, gateway_node_router, owner_node, [Key]), - ?assert(lists:member(Owner, ClusterNodes), {owner_not_in_cluster, Owner, Key}). diff --git a/fluxer_gateway/test/cluster_multinode_remote_cases.erl b/fluxer_gateway/test/cluster_multinode_remote_cases.erl deleted file mode 100644 index be92edbd3..000000000 --- a/fluxer_gateway/test/cluster_multinode_remote_cases.erl +++ /dev/null @@ -1,282 +0,0 @@ -%% SPDX-License-Identifier: AGPL-3.0-or-later - --module(cluster_multinode_remote_cases). --typing([eqwalizer]). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --export([ - remote_dispatch_relay_direct/1, - remote_guild_handoff_ships_state/1, - remote_presence_cache_rebalance/1, - remote_presence_fanout/1, - remote_session_state_transfer/1 -]). - --spec remote_presence_fanout(list()) -> ok. -remote_presence_fanout(Config0) -> - {ok, Peer1, Node1} = cluster_multinode_support:start_peer(node1, Config0), - _Config = [{peers, [Peer1]} | Config0], - true = net_kernel:connect_node(Node1), - {ok, _LocalPgPid} = gateway_pg_scope:ensure_presence_scope(), - {ok, _RemotePgPid} = peer:call(Peer1, gateway_pg_scope, ensure_presence_scope, []), - Scope = gateway_pg_scope:presence_scope(), - UserId = erlang:unique_integer([positive]), - Group = {presence, UserId}, - Receiver = peer:call( - Peer1, - erlang, - spawn, - [cluster_multinode_support, presence_receiver_loop, [self()]] - ), - ok = peer:call(Peer1, pg, join, [Scope, Group, Receiver]), - cluster_multinode_support:wait_until( - fun() -> - lists:member(Receiver, pg:get_members(Scope, Group)) - end, - "remote presence pg membership" - ), - {ok, ShardPid} = presence_bus_shard:start_link(0), - Payload = #{<<"status">> => <<"online">>}, - ?assertEqual(ok, gen_server:call(ShardPid, {publish, UserId, Payload}, 5000)), - receive - {remote_presence_received, Node1, UserId, Payload} -> - ok - after 5000 -> - ct:fail({remote_presence_not_delivered, Node1, UserId}) - end, - try - gen_server:stop(ShardPid) - catch - _:_ -> ok - end, - peer:call(Peer1, erlang, send, [Receiver, stop]), - ok. - --spec remote_dispatch_relay_direct(list()) -> ok. -remote_dispatch_relay_direct(Config0) -> - {ok, Peer1, Node1} = cluster_multinode_support:start_peer(node1, Config0), - _Config = [{peers, [Peer1]} | Config0], - true = net_kernel:connect_node(Node1), - Receiver = peer:call( - Peer1, - erlang, - spawn, - [cluster_multinode_support, dispatch_receiver_loop, [self()]] - ), - Payload = #{<<"id">> => <<"1">>}, - ok = gateway_dispatch_relay:dispatch_direct(Receiver, guild_update, Payload), - receive - {remote_dispatch_received, Node1, guild_update, Payload} -> - ok - after 5000 -> - ct:fail({remote_dispatch_not_delivered, Node1}) - end, - peer:call(Peer1, erlang, send, [Receiver, stop]), - ok. - --spec remote_session_state_transfer(list()) -> ok. -remote_session_state_transfer(Config0) -> - {ok, Peer1, Node1} = cluster_multinode_support:start_peer(node1, Config0), - _Config = [{peers, [Peer1]} | Config0], - true = net_kernel:connect_node(Node1), - ok = cluster_multinode_support:ensure_remote_registered( - Peer1, session_state_transfer, session_state_transfer, [] - ), - SessionId = <<"ct-session-transfer">>, - TransferState = #{token_hash => utils:hash_token(<<"resume-token">>), seq => 99}, - ?assertEqual(ok, session_state_transfer:push_state(Node1, SessionId, TransferState)), - ?assertEqual( - {ok, TransferState}, - peer:call(Peer1, session_state_transfer, pop_state, [SessionId]) - ), - ok. - --spec remote_presence_cache_rebalance(list()) -> ok. -remote_presence_cache_rebalance(Config0) -> - {ok, Peer1, Node1} = cluster_multinode_support:start_peer(node1, Config0), - _Config = [{peers, [Peer1]} | Config0], - true = net_kernel:connect_node(Node1), - Members = lists:usort([node(), Node1]), - RoleMembers = #{presence => Members}, - TermKeys = presence_cache_cluster_term_keys(), - LocalTerms = save_local_terms(TermKeys), - RemoteTerms = save_remote_terms(Peer1, TermKeys), - put_presence_cache_cluster_terms(Members, RoleMembers), - put_remote_presence_cache_cluster_terms(Peer1, Members, RoleMembers), - {ok, LocalPresenceStarted} = cluster_multinode_support:ensure_local_registered( - presence_cache, presence_cache, [] - ), - ok = cluster_multinode_support:ensure_remote_registered( - Peer1, presence_cache, presence_cache, [] - ), - Presence = #{<<"status">> => <<"online">>, <<"user">> => #{<<"id">> => <<"1">>}}, - RemoteUserId = cluster_multinode_support:find_role_cache_owner_id(Members, presence, Node1), - try - ok = gen_server:call(presence_cache, {put_local, RemoteUserId, Presence}, 5000), - ?assertMatch({ok, _}, gen_server:call(presence_cache, {get_local, RemoteUserId}, 5000)), - ?assertEqual(ok, presence_cache:rebalance()), - cluster_multinode_support:wait_until( - fun() -> - peer:call( - Peer1, - gen_server, - call, - [presence_cache, {get_local, RemoteUserId}, 5000] - ) =:= {ok, Presence} - end, - "presence cache remote rebalance" - ), - ?assertEqual( - not_found, gen_server:call(presence_cache, {get_local, RemoteUserId}, 5000) - ) - after - restore_local_terms(LocalTerms), - restore_remote_terms(Peer1, RemoteTerms), - cluster_multinode_support:maybe_stop_local(presence_cache, LocalPresenceStarted) - end, - ok. - -presence_cache_cluster_term_keys() -> - [ - {fluxer_gateway, runtime_config}, - {gateway_cluster_membership, members}, - {gateway_cluster_membership, members_by_role} - ]. - -save_local_terms(Keys) -> - [{Key, persistent_term:get(Key, undefined)} || Key <- Keys]. - -save_remote_terms(Peer, Keys) -> - [ - {Key, peer:call(Peer, persistent_term, get, [Key, undefined])} - || Key <- Keys - ]. - -put_presence_cache_cluster_terms(Members, RoleMembers) -> - persistent_term:put( - {fluxer_gateway, runtime_config}, - runtime_config_with_role( - persistent_term:get({fluxer_gateway, runtime_config}, undefined), presence - ) - ), - persistent_term:put({gateway_cluster_membership, members}, Members), - persistent_term:put({gateway_cluster_membership, members_by_role}, RoleMembers). - -put_remote_presence_cache_cluster_terms(Peer, Members, RoleMembers) -> - RemoteRuntime = peer:call( - Peer, persistent_term, get, [{fluxer_gateway, runtime_config}, undefined] - ), - ok = peer:call(Peer, persistent_term, put, [ - {fluxer_gateway, runtime_config}, runtime_config_with_role(RemoteRuntime, presence) - ]), - ok = peer:call(Peer, persistent_term, put, [ - {gateway_cluster_membership, members}, Members - ]), - ok = peer:call(Peer, persistent_term, put, [ - {gateway_cluster_membership, members_by_role}, RoleMembers - ]). - -runtime_config_with_role(Config, Role) when is_map(Config) -> - Config#{gateway_role => Role}; -runtime_config_with_role(_Config, Role) -> - #{gateway_role => Role}. - -restore_local_terms(Terms) -> - lists:foreach(fun restore_local_term/1, Terms). - -restore_local_term({Key, undefined}) -> - persistent_term:erase(Key); -restore_local_term({Key, Value}) -> - persistent_term:put(Key, Value). - -restore_remote_terms(Peer, Terms) -> - lists:foreach(fun(Term) -> restore_remote_term(Peer, Term) end, Terms). - -restore_remote_term(Peer, {Key, undefined}) -> - peer:call(Peer, persistent_term, erase, [Key]); -restore_remote_term(Peer, {Key, Value}) -> - peer:call(Peer, persistent_term, put, [Key, Value]). - --spec remote_guild_handoff_ships_state(list()) -> ok. -remote_guild_handoff_ships_state(Config0) -> - {Peer1, Node1, LocalGuildStarted, GuildId, SessionId, SessionPid, VoiceState, TransferState} = - remote_guild_handoff_setup(Config0), - try - ?assertMatch( - {ok, _}, - gen_server:call( - guild_manager, - {start_transferred, GuildId, TransferState}, - 10000 - ) - ), - ?assert(lists:member(GuildId, guild_manager:local_guild_ids())), - ?assertEqual( - #{attempted => 1, handed_off => 1}, - guild_manager:handoff_to_target(Node1) - ), - cluster_multinode_support:wait_until( - fun() -> - lists:member(GuildId, peer:call(Peer1, guild_manager, local_guild_ids, [])) - end, - "remote guild registered after handoff" - ), - ?assertNot(lists:member(GuildId, guild_manager:local_guild_ids())), - assert_remote_guild_handoff_state(Peer1, GuildId, SessionId, SessionPid, VoiceState) - after - SessionPid ! stop, - cluster_multinode_support:maybe_stop_local(guild_manager, LocalGuildStarted) - end, - ok. - -remote_guild_handoff_setup(Config0) -> - {ok, Peer1, Node1} = cluster_multinode_support:start_peer(node1, Config0), - _Config = [{peers, [Peer1]} | Config0], - true = net_kernel:connect_node(Node1), - process_registry:init(), - passive_sync_registry:init(), - {ok, LocalGuildStarted} = cluster_multinode_support:ensure_local_registered( - guild_manager, guild_manager, [] - ), - ok = peer:call(Peer1, process_registry, init, []), - ok = peer:call(Peer1, passive_sync_registry, init, []), - ok = cluster_multinode_support:ensure_remote_registered( - Peer1, guild_manager, guild_manager, [] - ), - GuildId = erlang:unique_integer([positive]) + 1000000, - SessionId = <<"ct-guild-handoff-session">>, - SessionPid = spawn(cluster_multinode_support, handoff_session_loop, [self()]), - GuildData = cluster_multinode_support:minimal_guild_data(GuildId), - VoiceState = #{<<"channel_id">> => <<"55">>, <<"user_id">> => <<"42">>}, - TransferState = #{ - id => GuildId, - data => GuildData, - sessions => #{ - SessionId => #{ - session_id => SessionId, - user_id => 42, - pid => SessionPid, - mref => make_ref(), - active_guilds => sets:new(), - bot => false, - is_staff => false, - viewable_channels => #{} - } - }, - voice_states => #{<<"voice-ct">> => VoiceState}, - virtual_channel_access => #{55 => sets:from_list([42])} - }, - {Peer1, Node1, LocalGuildStarted, GuildId, SessionId, SessionPid, VoiceState, - TransferState}. - -assert_remote_guild_handoff_state(Peer1, GuildId, SessionId, SessionPid, VoiceState) -> - #{} = RemoteState = cluster_multinode_support:remote_guild_live_state(Peer1, GuildId), - #{} = Sessions = maps:get(sessions, RemoteState), - #{} = VoiceStates = maps:get(voice_states, RemoteState), - #{} = VirtualChannelAccess = maps:get(virtual_channel_access, RemoteState), - #{pid := SessionPid, mref := MonitorRef} = maps:get(SessionId, Sessions), - ?assert(is_reference(MonitorRef)), - ?assertEqual(#{<<"voice-ct">> => VoiceState}, VoiceStates), - ?assertEqual(#{55 => sets:from_list([42])}, VirtualChannelAccess). diff --git a/fluxer_gateway/test/cluster_multinode_support.erl b/fluxer_gateway/test/cluster_multinode_support.erl deleted file mode 100644 index ed058d49d..000000000 --- a/fluxer_gateway/test/cluster_multinode_support.erl +++ /dev/null @@ -1,243 +0,0 @@ -%% SPDX-License-Identifier: AGPL-3.0-or-later - --module(cluster_multinode_support). --typing([eqwalizer]). - --include_lib("eunit/include/eunit.hrl"). - --export([ - dispatch_receiver_loop/1, - drive_membership/2, - ensure_local_registered/3, - ensure_remote_registered/4, - find_cache_owner_id/2, - find_role_cache_owner_id/3, - handoff_session_loop/1, - maybe_stop_local/2, - minimal_guild_data/1, - presence_receiver_loop/1, - remote_guild_live_state/2, - rpc_members/1, - start_peer/2, - stop_peer/1, - wait_until/2 -]). - --type peer_ref() :: pid(). - --export_type([peer_ref/0]). - --spec start_peer(atom(), list()) -> {ok, peer_ref(), node()}. -start_peer(NameSeed, _Config) -> - Name = list_to_atom( - lists:flatten( - io_lib:format( - "~p_~p_~p", - [NameSeed, os:system_time(microsecond), rand:uniform(99999)] - ) - ) - ), - CodePaths = code:get_path(), - {ok, Peer, Node} = peer:start_link(#{ - name => Name, - host => "127.0.0.1", - connection => standard_io, - args => ["-setcookie", atom_to_list(fluxer_cluster_ct)] - }), - ok = peer:call(Peer, code, add_paths, [CodePaths]), - {ok, _} = peer:call( - Peer, - gen_server, - start, - [ - {local, gateway_cluster_membership}, - gateway_cluster_membership, - #{auto_subscribe => false}, - [] - ] - ), - {ok, Peer, Node}. - --spec ensure_local_registered(atom(), module(), [term()]) -> {ok, boolean()}. -ensure_local_registered(Name, Module, Args) -> - case whereis(Name) of - undefined -> - {ok, _Pid} = gen_server:start({local, Name}, Module, Args, []), - {ok, true}; - Pid when is_pid(Pid) -> - {ok, false} - end. - --spec ensure_remote_registered(peer_ref(), atom(), module(), [term()]) -> ok. -ensure_remote_registered(Peer, Name, Module, Args) -> - case peer:call(Peer, erlang, whereis, [Name]) of - undefined -> - {ok, _Pid} = peer:call(Peer, gen_server, start, [{local, Name}, Module, Args, []]), - ok; - Pid when is_pid(Pid) -> - ok - end. - --spec maybe_stop_local(atom(), boolean()) -> ok. -maybe_stop_local(Name, true) -> - case whereis(Name) of - Pid when is_pid(Pid) -> - try - gen_server:stop(Pid) - catch - _:_ -> ok - end, - ok; - undefined -> - ok - end; -maybe_stop_local(_Name, false) -> - ok. - --spec stop_peer(peer_ref()) -> ok. -stop_peer(Peer) -> - try - peer:stop(Peer) - catch - _:_ -> ok - end, - ok. - --spec drive_membership(peer_ref(), [node()]) -> ok. -drive_membership(Peer, Peers) -> - peer:call(Peer, erlang, send, [gateway_cluster_membership, {cluster_peers_changed, Peers}]), - ok. - --spec rpc_members(peer_ref()) -> [node()]. -rpc_members(Peer) -> - peer:call(Peer, gateway_cluster_membership, members, []). - --spec presence_receiver_loop(pid()) -> ok. -presence_receiver_loop(Controller) -> - receive - {presence, UserId, Payload} -> - Controller ! {remote_presence_received, node(), UserId, Payload}, - presence_receiver_loop(Controller); - stop -> - ok - after infinity -> - ok - end. - --spec dispatch_receiver_loop(pid()) -> ok. -dispatch_receiver_loop(Controller) -> - receive - {'$gen_cast', {dispatch, Event, Payload}} -> - Controller ! {remote_dispatch_received, node(), Event, Payload}, - dispatch_receiver_loop(Controller); - stop -> - ok - after infinity -> - ok - end. - --spec handoff_session_loop(pid()) -> ok. -handoff_session_loop(Controller) -> - receive - {'$gen_cast', Msg} -> - Controller ! {handoff_session_cast, Msg}, - handoff_session_loop(Controller); - stop -> - ok; - _Other -> - handoff_session_loop(Controller) - after infinity -> - ok - end. - --spec find_cache_owner_id([node()], node()) -> pos_integer(). -find_cache_owner_id(Members, OwnerNode) -> - PreviousMembers = persistent_term:get({gateway_cluster_membership, members}, undefined), - persistent_term:put({gateway_cluster_membership, members}, Members), - try - hd([ - Id - || Id <- lists:seq(1, 5000), - clustered_ets_cache:resolve_owner_nodes(Id, 1) =:= [OwnerNode] - ]) - after - case PreviousMembers of - undefined -> persistent_term:erase({gateway_cluster_membership, members}); - _ -> persistent_term:put({gateway_cluster_membership, members}, PreviousMembers) - end - end. - --spec find_role_cache_owner_id([node()], atom(), node()) -> pos_integer(). -find_role_cache_owner_id(Members, Role, OwnerNode) -> - MembersKey = {gateway_cluster_membership, members}, - RoleMembersKey = {gateway_cluster_membership, members_by_role}, - PreviousMembers = persistent_term:get(MembersKey, undefined), - PreviousRoleMembers = persistent_term:get(RoleMembersKey, undefined), - persistent_term:put(MembersKey, Members), - persistent_term:put(RoleMembersKey, #{Role => Members}), - try - hd([ - Id - || Id <- lists:seq(1, 5000), - clustered_ets_cache:resolve_owner_nodes(Id, 1, Role) =:= [OwnerNode] - ]) - after - restore_persistent_term(MembersKey, PreviousMembers), - restore_persistent_term(RoleMembersKey, PreviousRoleMembers) - end. - --spec restore_persistent_term(term(), term()) -> ok. -restore_persistent_term(Key, undefined) -> - _ = persistent_term:erase(Key), - ok; -restore_persistent_term(Key, Value) -> - persistent_term:put(Key, Value). - --spec minimal_guild_data(integer()) -> map(). -minimal_guild_data(GuildId) -> - GuildIdBin = integer_to_binary(GuildId), - #{ - <<"guild">> => #{ - <<"id">> => GuildIdBin, - <<"owner_id">> => <<"42">>, - <<"features">> => [] - }, - <<"members">> => [ - #{ - <<"user">> => #{<<"id">> => <<"42">>}, - <<"roles">> => [] - } - ], - <<"roles">> => [], - <<"channels">> => [] - }. - --spec remote_guild_live_state(peer_ref(), integer()) -> term(). -remote_guild_live_state(Peer, GuildId) -> - GuildKey = process_registry:build_process_key(guild, GuildId), - GuildPid = peer:call(Peer, process_registry, registry_whereis, [GuildKey]), - ?assert(is_pid(GuildPid), {remote_guild_not_registered, GuildId}), - peer:call(Peer, gen_server, call, [GuildPid, {get_sessions}, 5000]). - --spec wait_until(fun(() -> boolean()), term()) -> ok. -wait_until(Pred, Label) -> - wait_until(Pred, Label, 50, 100). - --spec wait_until(fun(() -> boolean()), term(), non_neg_integer(), pos_integer()) -> ok. -wait_until(_Pred, Label, 0, _IntervalMs) -> - ct:fail({wait_timeout, Label}); -wait_until(Pred, Label, Retries, IntervalMs) -> - case eval_predicate(Pred) of - true -> - ok; - _ -> - timer:sleep(IntervalMs), - wait_until(Pred, Label, Retries - 1, IntervalMs) - end. - -eval_predicate(Pred) -> - try - Pred() - catch - _:_ -> false - end. diff --git a/fluxer_gateway/test/gateway_handoff_rollout_k8s_smoke.erl b/fluxer_gateway/test/gateway_handoff_rollout_k8s_smoke.erl deleted file mode 100644 index 587317efd..000000000 --- a/fluxer_gateway/test/gateway_handoff_rollout_k8s_smoke.erl +++ /dev/null @@ -1,761 +0,0 @@ -%% SPDX-License-Identifier: AGPL-3.0-or-later - --module(gateway_handoff_rollout_k8s_smoke). --behaviour(gen_server). - --export([ - node_main/0, - controller_main/0, - accept_transfer/1, - count/0, - drain_to/1, - increment_if_owner/1, - seed/2, - snapshot/0, - version/0 -]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --define(SERVER, ?MODULE). --define(METRICS, gateway_handoff_rollout_metrics). --define(NAME_PREFIX, "handoff-rollout"). --define(DEFAULT_CLUSTER, "sessions"). --define(CONTAINER, "node"). - -node_main() -> - try - ok = ensure_node_runtime(), - write_stdout("handoff rollout node ready cluster=~ts node=~p version=~ts~n", [ - cluster_name(), node(), version() - ]), - wait_forever() - catch - Class:Reason:Stacktrace -> - write_stderr("handoff rollout node failed: ~0tp:~0tp~n~0tp~n", [ - Class, Reason, Stacktrace - ]), - halt(1) - end. - -controller_main() -> - try - ok = ensure_controller_runtime(), - run_controller(), - halt(0) - catch - Class:Reason:Stacktrace -> - write_stderr("handoff rollout controller failed: ~0tp:~0tp~n~0tp~n", [ - Class, Reason, Stacktrace - ]), - halt(1) - end. - -seed(Ids, Epoch) when is_list(Ids), is_binary(Epoch) -> - gen_server:call(?SERVER, {seed, Ids, Epoch}, 60000). - -count() -> - gen_server:call(?SERVER, count, 5000). - -snapshot() -> - gen_server:call(?SERVER, snapshot, 10000). - -version() -> - type_conv:ensure_binary(os:getenv("HANDOFF_IMAGE_VERSION"), <<"unknown">>). - -drain_to(TargetNodes) when is_list(TargetNodes) -> - gen_server:call(?SERVER, {drain_to, TargetNodes}, 120000). - -accept_transfer(Id) when is_binary(Id) -> - case session_state_transfer:pop_state(Id) of - {ok, Entity} when is_map(Entity) -> - gen_server:call(?SERVER, {accept, Id, Entity}, 5000); - _ -> - {error, not_found} - end. - -increment_if_owner(Id) when is_binary(Id) -> - gen_server:call(?SERVER, {increment, Id}, 5000). - -init([]) -> - {ok, #{entities => #{}}}. - -handle_call({seed, Ids, Epoch}, _From, State) -> - Entities0 = maps:get(entities, State), - Entities = lists:foldl(fun(Id, Acc) -> seed_entity(Id, Epoch, Acc) end, Entities0, Ids), - {reply, {ok, maps:size(Entities)}, State#{entities := Entities}}; -handle_call(count, _From, State) -> - {reply, maps:size(maps:get(entities, State)), State}; -handle_call(snapshot, _From, State) -> - {reply, maps:values(maps:get(entities, State)), State}; -handle_call({accept, Id, Entity}, _From, State) -> - Entities = maps:get(entities, State), - {reply, ok, State#{entities := Entities#{Id => Entity}}}; -handle_call({increment, Id}, _From, State) -> - handle_increment(Id, State); -handle_call({drain_to, TargetNodes}, _From, State) -> - handle_drain(TargetNodes, State); -handle_call(_Request, _From, State) -> - {reply, ok, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -ensure_node_runtime() -> - ok = ensure_started(crypto), - ok = ensure_session_transfer(), - {ok, _Pid} = gen_server:start_link({local, ?SERVER}, ?MODULE, [], []), - ok. - -ensure_controller_runtime() -> - ok = ensure_started(crypto), - ensure_metrics(), - ok. - -ensure_started(App) -> - case application:ensure_all_started(App) of - {ok, _Apps} -> ok; - {error, {App, {already_started, App}}} -> ok; - {error, {already_started, App}} -> ok; - {error, Reason} -> erlang:error({app_start_failed, App, Reason}) - end. - -ensure_session_transfer() -> - case whereis(session_state_transfer) of - undefined -> - {ok, _Pid} = session_state_transfer:start_link(), - ok; - Pid when is_pid(Pid) -> - ok - end. - -wait_forever() -> - receive - stop -> halt(0) - after 86400000 -> - wait_forever() - end. - -seed_entity(Id, Epoch, Entities) when is_binary(Id) -> - Entity = #{ - id => Id, - seq => 0, - owner_version => version(), - epoch => Epoch, - checksum => entity_checksum(Id, Epoch, 0) - }, - Entities#{Id => Entity}; -seed_entity(_Id, _Epoch, Entities) -> - Entities. - -handle_increment(Id, State) -> - Entities = maps:get(entities, State), - case maps:get(Id, Entities, undefined) of - undefined -> {reply, not_found, State}; - Entity -> reply_incremented(Id, Entity, Entities, State) - end. - -reply_incremented(Id, Entity, Entities, State) -> - Seq = maps:get(seq, Entity, 0) + 1, - Epoch = maps:get(epoch, Entity, <<"seed">>), - Updated = Entity#{ - seq := Seq, - owner_version := version(), - checksum := entity_checksum(Id, Epoch, Seq) - }, - {reply, {ok, Seq}, State#{entities := Entities#{Id := Updated}}}. - -handle_drain(TargetNodes, State) -> - Entities = maps:get(entities, State), - {Moved, Failed, Remaining} = drain_entities(maps:to_list(Entities), TargetNodes, 0, 0, #{}), - Reply = #{moved => Moved, failed => Failed, remaining => maps:size(Remaining)}, - {reply, {ok, Reply}, State#{entities := Remaining}}. - -drain_entities([], _TargetNodes, Moved, Failed, Remaining) -> - {Moved, Failed, Remaining}; -drain_entities([{Id, Entity} | Rest], TargetNodes, Moved, Failed, Remaining) -> - TargetNode = select_target(Id, TargetNodes), - case transfer_entity(TargetNode, Id, Entity) of - ok -> - drain_entities(Rest, TargetNodes, Moved + 1, Failed, Remaining); - {error, _Reason} -> - drain_entities(Rest, TargetNodes, Moved, Failed + 1, Remaining#{Id => Entity}) - end. - -transfer_entity(undefined, _Id, _Entity) -> - {error, no_target}; -transfer_entity(TargetNode, Id, Entity) -> - case session_state_transfer:push_state(TargetNode, Id, Entity) of - ok -> rpc_accept_entity(TargetNode, Id); - {error, Reason} -> {error, Reason} - end. - -rpc_accept_entity(TargetNode, Id) -> - case rpc:call(TargetNode, ?MODULE, accept_transfer, [Id], 10000) of - ok -> ok; - {error, Reason} -> {error, Reason}; - Other -> {error, {accept_failed, TargetNode, Other}} - end. - -select_target(_Id, []) -> - undefined; -select_target(Id, TargetNodes) -> - Index = erlang:phash2(Id, length(TargetNodes)), - lists:nth(Index + 1, TargetNodes). - -entity_checksum(Id, Epoch, Seq) -> - crypto:hash(sha256, term_to_binary({Id, Epoch, Seq})). - -run_controller() -> - Config = controller_config(), - Nodes = prepare_cluster(Config), - Workers = start_workload( - maps:get(workload_concurrency, Config), - maps:get(entity_count, Config), - Nodes - ), - FinalNodes = rollout_cluster(Config, Nodes), - stop_workload(Workers), - ok = verify_rollout(Config, FinalNodes). - -controller_config() -> - Cluster = cluster_name(), - EntityCount = int_env("HANDOFF_ENTITY_COUNT", 600), - #{ - cluster => Cluster, - namespace => namespace(), - replicas => int_env("HANDOFF_REPLICAS", 3), - entity_count => EntityCount, - workload_concurrency => int_env("HANDOFF_WORKLOAD_CONCURRENCY", 8), - max_latency_ms => int_env("HANDOFF_MAX_INCREMENT_LATENCY_MS", 5000), - min_workload_ops => int_env("HANDOFF_MIN_WORKLOAD_OPS", EntityCount), - image_v2 => must_env("HANDOFF_IMAGE_V2") - }. - -prepare_cluster(#{namespace := Namespace, replicas := Replicas, entity_count := EntityCount}) -> - ok = kubectl_wait_initial_rollout(Namespace), - Nodes = current_nodes(Namespace, Replicas), - ok = wait_nodes(Nodes), - ok = seed_cluster(Nodes, EntityCount), - ok = assert_cluster(Nodes, EntityCount), - Nodes. - -rollout_cluster( - #{ - namespace := Namespace, - image_v2 := ImageV2, - replicas := Replicas, - entity_count := EntityCount - }, - Nodes -) -> - ok = set_next_revision(Namespace, ImageV2), - rollout_ordinals(Namespace, Replicas, EntityCount, Nodes). - -verify_rollout( - #{ - cluster := Cluster, - entity_count := EntityCount, - max_latency_ms := MaxLatencyMs, - min_workload_ops := MinWorkloadOps - }, - FinalNodes -) -> - Metrics = metrics(), - ok = assert_workload(Metrics, MaxLatencyMs, MinWorkloadOps), - ok = assert_versions(FinalNodes, <<"v2">>), - FinalSnapshot = cluster_snapshot(FinalNodes), - ok = assert_cluster_snapshot(FinalSnapshot, EntityCount), - ok = assert_workload_effects(FinalSnapshot, Metrics), - write_stdout("handoff rollout local Kubernetes smoke passed cluster=~ts metrics=~0tp~n", [ - Cluster, Metrics - ]), - ok. - -kubectl_wait_initial_rollout(Namespace) -> - kubectl(Namespace, "rollout status statefulset/" ++ statefulset() ++ " --timeout=180s"). - -seed_cluster(Nodes, EntityCount) -> - Partitions = partition_ids(Nodes, EntityCount), - Epoch = iolist_to_binary([cluster_name(), "-seed-v1"]), - lists:foreach(fun({Node, Ids}) -> rpc_expect(Node, seed, [Ids, Epoch]) end, Partitions), - ok. - -partition_ids(Nodes, EntityCount) -> - Empty = [{Node, []} || Node <- Nodes], - lists:foldl(fun add_partition_id/2, Empty, entity_ids(EntityCount)). - -add_partition_id(Id, Partitions) -> - Index = erlang:phash2(Id, length(Partitions)) + 1, - {Node, Ids} = lists:nth(Index, Partitions), - lists:sublist(Partitions, Index - 1) ++ [{Node, [Id | Ids]}] ++ - lists:nthtail(Index, Partitions). - -rollout_ordinals(Namespace, Replicas, EntityCount, Nodes0) -> - lists:foldl( - fun(Ordinal, Nodes) -> rollout_ordinal(Namespace, Ordinal, EntityCount, Nodes) end, - Nodes0, - lists:seq(Replicas - 1, 0, -1) - ). - -rollout_ordinal(Namespace, Ordinal, EntityCount, Nodes0) -> - Source = lists:nth(Ordinal + 1, Nodes0), - Survivors = lists:delete(Source, Nodes0), - ok = put_workload_nodes(Survivors), - ok = drain_and_verify(Source, Survivors), - ok = patch_partition(Namespace, Ordinal), - ok = wait_pod_ready(Namespace, Ordinal), - Nodes1 = replace_ordinal_node(Namespace, Ordinal, Nodes0), - ok = wait_nodes(Nodes1), - ok = wait_node_version(lists:nth(Ordinal + 1, Nodes1), <<"v2">>), - ok = put_workload_nodes(Nodes1), - ok = assert_cluster(Nodes1, EntityCount), - Nodes1. - -drain_and_verify(Source, Survivors) -> - case rpc:call(Source, ?MODULE, drain_to, [Survivors], 120000) of - {ok, #{failed := 0}} -> wait_empty(Source); - Other -> erlang:error({drain_failed, Source, Other}) - end. - -patch_partition(Namespace, Ordinal) -> - Patch = - "{\"spec\":{\"updateStrategy\":{\"rollingUpdate\":{\"partition\":" ++ - integer_to_list(Ordinal) ++ "}}}}", - kubectl( - Namespace, - "patch statefulset " ++ statefulset() ++ " --type merge -p " ++ - shell_quote(Patch) - ). - -wait_pod_ready(Namespace, Ordinal) -> - Pod = pod_name(Ordinal), - ok = kubectl(Namespace, "wait --for=condition=Ready pod/" ++ Pod ++ " --timeout=180s"), - wait_pod_observed_v2(Namespace, Pod). - -wait_pod_observed_v2(Namespace, Pod) -> - wait_until( - fun() -> - Image = string:trim( - kubectl_out( - Namespace, - "get pod " ++ Pod ++ - " -o jsonpath='{.spec.containers[0].image}'" - ) - ), - has_suffix(Image, ":v2") - end, - {pod_image_v2, Pod}, - 60, - 1000 - ). - -replace_ordinal_node(Namespace, Ordinal, Nodes0) -> - Node = pod_node(Namespace, Ordinal), - lists:sublist(Nodes0, Ordinal) ++ [Node] ++ lists:nthtail(Ordinal + 1, Nodes0). - -set_next_revision(Namespace, ImageV2) -> - ok = kubectl( - Namespace, - "set image statefulset/" ++ statefulset() ++ " " ?CONTAINER "=" ++ - ImageV2 - ), - kubectl(Namespace, "set env statefulset/" ++ statefulset() ++ " HANDOFF_IMAGE_VERSION=v2"). - -wait_nodes(Nodes) -> - lists:foreach(fun wait_node/1, Nodes), - ok = put_workload_nodes(Nodes), - ok. - -wait_node(Node) -> - wait_until( - fun() -> node_connectable(Node) andalso rpc_version(Node) =/= undefined end, - {node_ready, Node}, - 120, - 1000 - ). - -node_connectable(Node) -> - case net_kernel:connect_node(Node) of - true -> true; - false -> false; - ignored -> false - end. - -rpc_version(Node) -> - case rpc:call(Node, ?MODULE, version, [], 5000) of - Version when is_binary(Version) -> Version; - _ -> undefined - end. - -wait_node_version(Node, Version) -> - wait_until( - fun() -> rpc_version(Node) =:= Version end, {node_version, Node, Version}, 120, 1000 - ). - -wait_empty(Node) -> - wait_until(fun() -> rpc_count(Node) =:= 0 end, {node_empty, Node}, 120, 500). - -assert_versions(Nodes, Expected) -> - Versions = [{Node, rpc_version(Node)} || Node <- Nodes], - case lists:all(fun({_Node, Version}) -> Version =:= Expected end, Versions) of - true -> ok; - false -> erlang:error({unexpected_versions, Versions}) - end. - -assert_cluster(Nodes, EntityCount) -> - Snapshot = cluster_snapshot(Nodes), - assert_cluster_snapshot(Snapshot, EntityCount). - -assert_cluster_snapshot(Snapshot, EntityCount) -> - ok = assert_entity_count(Snapshot, EntityCount), - ok = assert_unique_entities(Snapshot, EntityCount), - ok = assert_checksums(Snapshot). - -assert_entity_count(Snapshot, EntityCount) -> - case length(Snapshot) of - EntityCount -> ok; - Actual -> erlang:error({entity_count_mismatch, Actual, EntityCount}) - end. - -assert_unique_entities(Snapshot, EntityCount) -> - Unique = lists:usort([maps:get(id, Entity) || Entity <- Snapshot]), - case length(Unique) of - EntityCount -> ok; - Actual -> erlang:error({duplicate_or_missing_entities, Actual, EntityCount}) - end. - -assert_checksums(Snapshot) -> - Bad = [Entity || Entity <- Snapshot, not valid_checksum(Entity)], - case Bad of - [] -> ok; - _ -> erlang:error({bad_entity_checksums, length(Bad)}) - end. - -valid_checksum(#{id := Id, epoch := Epoch, seq := Seq, checksum := Checksum}) -> - entity_checksum(Id, Epoch, Seq) =:= Checksum; -valid_checksum(_Entity) -> - false. - -cluster_snapshot(Nodes) -> - case retry_cluster_snapshot(Nodes, 20) of - {ok, Snapshot} -> Snapshot; - {error, Reason} -> erlang:error(Reason) - end. - -retry_cluster_snapshot(Nodes, Attempts) -> - connect_nodes(Nodes), - case collect_cluster_snapshot(Nodes, []) of - {ok, Snapshot} -> - {ok, Snapshot}; - {error, _Reason} when Attempts > 0 -> - timer:sleep(500), - retry_cluster_snapshot(Nodes, Attempts - 1); - {error, Reason} -> - {error, Reason} - end. - -connect_nodes(Nodes) -> - lists:foreach(fun(Node) -> _ = net_kernel:connect_node(Node) end, Nodes). - -collect_cluster_snapshot([], Acc) -> - {ok, lists:append(lists:reverse(Acc))}; -collect_cluster_snapshot([Node | Rest], Acc) -> - case rpc_snapshot(Node) of - {ok, Snapshot} -> collect_cluster_snapshot(Rest, [Snapshot | Acc]); - {error, Reason} -> {error, Reason} - end. - -rpc_snapshot(Node) -> - case rpc:call(Node, ?MODULE, snapshot, [], 10000) of - Snapshot when is_list(Snapshot) -> {ok, Snapshot}; - Other -> {error, {snapshot_failed, Node, Other}} - end. - -rpc_count(Node) -> - case rpc:call(Node, ?MODULE, count, [], 5000) of - Count when is_integer(Count) -> Count; - _ -> -1 - end. - -rpc_expect(Node, Function, Args) -> - case rpc:call(Node, ?MODULE, Function, Args, 60000) of - {ok, _Value} -> ok; - ok -> ok; - Other -> erlang:error({rpc_failed, Node, Function, Other}) - end. - -start_workload(Concurrency, EntityCount, Nodes) -> - ok = put_workload_nodes(Nodes), - [spawn_link(fun() -> workload_loop(EntityCount) end) || _ <- lists:seq(1, Concurrency)]. - -stop_workload(Workers) -> - ets:insert(?METRICS, {stop, true}), - lists:foreach(fun wait_worker_down/1, Workers). - -wait_worker_down(Pid) -> - Ref = monitor(process, Pid), - receive - {'DOWN', Ref, process, Pid, _Reason} -> ok - after 5000 -> - exit(Pid, kill), - ok - end. - -workload_loop(EntityCount) -> - case metric(stop, false) of - true -> - ok; - false -> - Id = entity_id(rand:uniform(EntityCount)), - run_increment(Id), - workload_loop(EntityCount) - end. - -run_increment(Id) -> - Started = erlang:monotonic_time(microsecond), - case increment_until_found(Id, workload_nodes(), 200) of - ok -> record_increment_latency(Started); - {error, Reason} -> record_increment_error(Id, Reason) - end. - -increment_until_found(_Id, _Nodes, 0) -> - {error, not_found}; -increment_until_found(Id, Nodes, Attempts) -> - case try_increment_nodes(Id, Nodes) of - ok -> - ok; - not_found -> - timer:sleep(25), - increment_until_found(Id, workload_nodes(), Attempts - 1) - end. - -try_increment_nodes(_Id, []) -> - not_found; -try_increment_nodes(Id, [Node | Rest]) -> - case rpc:call(Node, ?MODULE, increment_if_owner, [Id], 250) of - {ok, _Seq} -> ok; - _Other -> try_increment_nodes(Id, Rest) - end. - -record_increment_latency(Started) -> - LatencyUs = erlang:monotonic_time(microsecond) - Started, - ets:update_counter(?METRICS, ops, {2, 1}, {ops, 0}), - update_max_latency(LatencyUs). - -update_max_latency(LatencyUs) -> - Current = metric(max_latency_us, 0), - case LatencyUs > Current of - true -> ets:insert(?METRICS, {max_latency_us, LatencyUs}); - false -> ok - end. - -record_increment_error(Id, Reason) -> - ets:update_counter(?METRICS, errors, {2, 1}, {errors, 0}), - ets:insert(?METRICS, {last_error, {Id, Reason}}). - -assert_workload(Metrics, MaxLatencyMs, MinOps) -> - #{ops := Ops, errors := Errors, max_latency_us := MaxLatencyUs} = Metrics, - MaxLatencyLimitUs = MaxLatencyMs * 1000, - case {Ops >= MinOps, Errors, MaxLatencyUs =< MaxLatencyLimitUs} of - {true, 0, true} -> ok; - _ -> erlang:error({workload_failed, Metrics, MaxLatencyMs, MinOps}) - end. - -assert_workload_effects(Snapshot, #{ops := Ops} = Metrics) -> - SeqTotal = lists:sum([maps:get(seq, Entity, 0) || Entity <- Snapshot]), - case SeqTotal of - Ops -> ok; - _ -> erlang:error({workload_effect_mismatch, SeqTotal, Metrics}) - end. - -metrics() -> - #{ - ops => metric(ops, 0), - errors => metric(errors, 0), - max_latency_us => metric(max_latency_us, 0), - last_error => metric(last_error, undefined) - }. - -ensure_metrics() -> - case ets:info(?METRICS) of - undefined -> ets:new(?METRICS, [named_table, public, set]); - _ -> ok - end, - ets:insert(?METRICS, [{stop, false}, {ops, 0}, {errors, 0}, {max_latency_us, 0}]), - ok. - -put_workload_nodes(Nodes) -> - ets:insert(?METRICS, {nodes, Nodes}), - ok. - -workload_nodes() -> - metric(nodes, []). - -metric(Key, Default) -> - case ets:lookup(?METRICS, Key) of - [{Key, Value}] -> Value; - [] -> Default - end. - -current_nodes(Namespace, Replicas) -> - [pod_node(Namespace, Ordinal) || Ordinal <- lists:seq(0, Replicas - 1)]. - -pod_node(Namespace, Ordinal) -> - Pod = pod_name(Ordinal), - Ip = wait_pod_ip(Namespace, Pod), - list_to_atom("handoff@" ++ Ip). - -wait_pod_ip(Namespace, Pod) -> - wait_until( - fun() -> - valid_ip( - kubectl_out(Namespace, "get pod " ++ Pod ++ " -o jsonpath='{.status.podIP}'") - ) - end, - {pod_ip, Pod}, - 120, - 1000 - ), - string:trim(kubectl_out(Namespace, "get pod " ++ Pod ++ " -o jsonpath='{.status.podIP}'")). - -valid_ip(Value) -> - string:trim(Value) =/= "". - -pod_name(Ordinal) -> - statefulset() ++ "-" ++ integer_to_list(Ordinal). - -entity_ids(EntityCount) -> - [entity_id(I) || I <- lists:seq(1, EntityCount)]. - -entity_id(I) -> - iolist_to_binary([cluster_name(), "-entity-", integer_to_binary(I)]). - -namespace() -> - type_conv:ensure_binary(os:getenv("POD_NAMESPACE"), <<"fluxer-it">>). - -cluster_name() -> - case os:getenv("HANDOFF_CLUSTER_NAME") of - false -> ?DEFAULT_CLUSTER; - "" -> ?DEFAULT_CLUSTER; - Value -> validate_cluster_name(string:trim(Value)) - end. - -validate_cluster_name(Name) -> - case Name =/= [] andalso lists:all(fun valid_cluster_char/1, Name) of - true -> Name; - false -> erlang:error({invalid_cluster_name, Name}) - end. - -valid_cluster_char(Char) when Char >= $a, Char =< $z -> - true; -valid_cluster_char(Char) when Char >= $0, Char =< $9 -> - true; -valid_cluster_char($-) -> - true; -valid_cluster_char(_Char) -> - false. - -statefulset() -> - ?NAME_PREFIX ++ "-" ++ cluster_name(). - -int_env(Name, Default) -> - case os:getenv(Name) of - false -> Default; - "" -> Default; - Value -> list_to_integer(string:trim(Value)) - end. - -must_env(Name) -> - case os:getenv(Name) of - false -> erlang:error({missing_env, Name}); - "" -> erlang:error({missing_env, Name}); - Value -> Value - end. - -kubectl(Namespace, Args) -> - case run_cmd(kubectl_command(Namespace, Args)) of - {ok, _Output} -> ok; - {error, Status, Output} -> erlang:error({kubectl_failed, Status, Args, Output}) - end. - -kubectl_out(Namespace, Args) -> - case run_cmd(kubectl_command(Namespace, Args)) of - {ok, Output} -> Output; - {error, Status, Output} -> erlang:error({kubectl_failed, Status, Args, Output}) - end. - -kubectl_command(Namespace, Args) -> - "kubectl -n " ++ shell_quote(binary_to_list(Namespace)) ++ " " ++ Args. - -run_cmd(Command) -> - Port = open_port({spawn, Command}, [exit_status, stderr_to_stdout]), - collect_cmd(Port, []). - -collect_cmd(Port, Acc) -> - receive - {Port, {data, Data}} -> - collect_cmd(Port, [Data | Acc]); - {Port, {exit_status, 0}} -> - {ok, lists:flatten(lists:reverse(Acc))}; - {Port, {exit_status, Status}} -> - {error, Status, lists:flatten(lists:reverse(Acc))} - after 300000 -> - port_close(Port), - {error, timeout, lists:flatten(lists:reverse(Acc))} - end. - -shell_quote(Value) -> - "'" ++ escape_single_quotes(Value) ++ "'". - -escape_single_quotes([]) -> - []; -escape_single_quotes([$' | Rest]) -> - "'\\''" ++ escape_single_quotes(Rest); -escape_single_quotes([Char | Rest]) -> - [Char | escape_single_quotes(Rest)]. - -has_suffix(Value, Suffix) -> - ValueLength = length(Value), - SuffixLength = length(Suffix), - ValueLength >= SuffixLength andalso - lists:nthtail(ValueLength - SuffixLength, Value) =:= Suffix. - -wait_until(Pred, Label, Retries, IntervalMs) -> - case safe_pred(Pred) of - true -> ok; - false -> wait_again(Pred, Label, Retries, IntervalMs) - end. - -wait_again(_Pred, Label, 0, _IntervalMs) -> - erlang:error({wait_timeout, Label}); -wait_again(Pred, Label, Retries, IntervalMs) -> - timer:sleep(IntervalMs), - wait_until(Pred, Label, Retries - 1, IntervalMs). - -safe_pred(Pred) -> - try Pred() of - true -> true; - _ -> false - catch - _:_ -> false - end. - -write_stdout(Format, Args) -> - write_stream(standard_io, Format, Args). - -write_stderr(Format, Args) -> - write_stream(standard_error, Format, Args). - -write_stream(Device, Format, Args) -> - Output = iolist_to_binary(io_lib:format(Format, Args)), - _ = file:write(Device, Output), - ok. diff --git a/fluxer_gateway/test/gateway_hotpatch_k8s_smoke.erl b/fluxer_gateway/test/gateway_hotpatch_k8s_smoke.erl deleted file mode 100644 index 5289d4907..000000000 --- a/fluxer_gateway/test/gateway_hotpatch_k8s_smoke.erl +++ /dev/null @@ -1,366 +0,0 @@ -%% SPDX-License-Identifier: AGPL-3.0-or-later - --module(gateway_hotpatch_k8s_smoke). --typing([eqwalizer]). - --export([main/0]). - --define(SIGNER_KEY_ID, <<"ops">>). --define(CREATED_BY, <<"local-k8s-hotpatch-smoke">>). --define(PATCH_MODULE, gateway_hotpatch_smoke_patch). --define(PATCH_BASELINE_VERSION, 1). --define(PATCH_TARGET_VERSION, 2). - --spec main() -> no_return(). -main() -> - try - run(), - halt(0) - catch - Class:Reason:Stacktrace -> - write_stderr("hotpatch k8s smoke failed: ~0tp:~0tp~n~0tp~n", [ - Class, Reason, Stacktrace - ]), - halt(1) - end. - --spec run() -> ok. -run() -> - ok = ensure_runtime(), - ok = ensure_patch_module_baseline(), - Config = hotpatch_config(), - PrivateKey = private_key(), - ok = require_enabled(), - ok = require_store(), - ok = prepare_startup_events(Config, PrivateKey), - StartupApplyMs = startup_reconcile(Config), - ok = maybe_assert_patch_version(maps:get(expected_before, Config)), - LiveApplyMs = live_reconcile(Config, PrivateKey), - ok = maybe_assert_patch_version( - maps:get(expected_before, Config) + maps:get(post_start_event_count, Config) - ), - ok = write_success(Config, StartupApplyMs, LiveApplyMs). - --spec ensure_runtime() -> ok. -ensure_runtime() -> - ensure_started(crypto), - ensure_started(ezstd), - _ = fluxer_gateway_env:load(), - ok. - --spec hotpatch_config() -> map(). -hotpatch_config() -> - BuildSha = gateway_hotpatch_runtime:build_sha(), - EventCount = int_env("HOTPATCH_EVENT_COUNT", 8), - PostStartEventCount = int_env("HOTPATCH_POST_START_EVENT_COUNT", 1), - #{ - build_sha => BuildSha, - event_count => EventCount, - post_start_event_count => PostStartEventCount, - expected_before => max(int_env("HOTPATCH_EXPECT_EVENT_COUNT", EventCount), EventCount), - timeout_ms => int_env("HOTPATCH_TIMEOUT_MS", 120000), - max_startup_apply_ms => int_env("HOTPATCH_MAX_STARTUP_APPLY_MS", 30000), - max_live_apply_ms => int_env("HOTPATCH_MAX_LIVE_APPLY_MS", 30000) - }. - --spec prepare_startup_events(map(), binary()) -> ok. -prepare_startup_events(#{build_sha := BuildSha, event_count := EventCount}, PrivateKey) -> - append_events(BuildSha, PrivateKey, EventCount). - --spec startup_reconcile(map()) -> non_neg_integer(). -startup_reconcile(#{ - expected_before := ExpectedBefore, - timeout_ms := TimeoutMs, - max_startup_apply_ms := MaxStartupApplyMs -}) -> - StartupStartedMs = now_ms(), - {ok, _Pid} = gateway_hotpatch_reconciler:start_link(), - ok = wait_ready(ExpectedBefore, TimeoutMs), - StartupApplyMs = elapsed_ms(StartupStartedMs), - ok = assert_elapsed(startup_apply, StartupApplyMs, MaxStartupApplyMs), - StartupApplyMs. - --spec live_reconcile(map(), binary()) -> non_neg_integer(). -live_reconcile( - #{ - build_sha := BuildSha, - post_start_event_count := PostStartEventCount, - expected_before := ExpectedBefore, - timeout_ms := TimeoutMs, - max_live_apply_ms := MaxLiveApplyMs - }, - PrivateKey -) -> - LiveApplyMs = apply_post_start_events( - BuildSha, PrivateKey, PostStartEventCount, ExpectedBefore, TimeoutMs - ), - ok = assert_elapsed(live_apply, LiveApplyMs, MaxLiveApplyMs), - LiveApplyMs. - --spec write_success(map(), non_neg_integer(), non_neg_integer()) -> ok. -write_success(#{build_sha := BuildSha}, StartupApplyMs, LiveApplyMs) -> - Status = gateway_hotpatch_reconciler:status(), - write_stdout( - "hotpatch k8s smoke ok node=~ts build_sha=~ts startup_apply_ms=~p " - "live_apply_ms=~p status=~0tp~n", - [gateway_hotpatch_runtime:node_name(), BuildSha, StartupApplyMs, LiveApplyMs, Status] - ), - ok. - --spec apply_post_start_events( - binary(), binary(), non_neg_integer(), non_neg_integer(), pos_integer() -) -> - non_neg_integer(). -apply_post_start_events(_BuildSha, _PrivateKey, 0, _ExpectedBefore, _TimeoutMs) -> - 0; -apply_post_start_events(BuildSha, PrivateKey, PostStartEventCount, ExpectedBefore, TimeoutMs) -> - ok = append_events(BuildSha, PrivateKey, PostStartEventCount), - StartedMs = now_ms(), - gateway_hotpatch_reconciler:reconcile_async(), - ok = wait_ready(ExpectedBefore + PostStartEventCount, TimeoutMs), - elapsed_ms(StartedMs). - --spec ensure_started(atom()) -> ok. -ensure_started(App) -> - case application:ensure_all_started(App) of - {ok, _Apps} -> ok; - {error, {App, {already_started, App}}} -> ok; - {error, {already_started, App}} -> ok; - {error, Reason} -> erlang:error({app_start_failed, App, Reason}) - end. - --spec require_enabled() -> ok. -require_enabled() -> - case gateway_hotpatch_runtime:is_enabled() of - true -> ok; - false -> erlang:error(hotpatch_not_enabled) - end. - --spec require_store() -> ok. -require_store() -> - case gateway_hotpatch_store:connect() of - ok -> ok; - {error, Reason} -> erlang:error({hotpatch_store_connect_failed, Reason}) - end. - --spec private_key() -> binary(). -private_key() -> - Encoded = must_env("FLUXER_GATEWAY_HOTPATCH_PRIVATE_KEY_BASE64"), - try base64:decode(Encoded) of - PrivateKey when byte_size(PrivateKey) =:= 32 -> - PrivateKey; - PrivateKey -> - erlang:error({invalid_private_key_size, byte_size(PrivateKey)}) - catch - Class:Reason -> erlang:error({invalid_private_key_base64, Class, Reason}) - end. - --spec append_events(binary(), binary(), non_neg_integer()) -> ok. -append_events(_BuildSha, _PrivateKey, 0) -> - ok; -append_events(BuildSha, PrivateKey, Count) when Count > 0 -> - lists:foreach( - fun(_Index) -> - {ok, EventId} = append_event(BuildSha, PrivateKey), - write_stdout("appended hotpatch event ~ts~n", [gateway_hotpatch_loader:hex(EventId)]) - end, - lists:seq(1, Count) - ). - --spec append_event(binary(), binary()) -> {ok, binary()}. -append_event(BuildSha, PrivateKey) -> - {ok, Bundle} = patch_bundle(BuildSha), - {ok, CompressedBundle} = gateway_hotpatch_bundle:compress_term(Bundle), - {ok, Signature} = gateway_hotpatch_bundle:sign(CompressedBundle, PrivateKey), - BundleSha256 = gateway_hotpatch_bundle:bundle_hash(CompressedBundle), - case - gateway_hotpatch_store:append_event( - BuildSha, - ?CREATED_BY, - ?SIGNER_KEY_ID, - Signature, - BundleSha256, - CompressedBundle - ) - of - {ok, EventId} -> {ok, EventId}; - {error, Reason} -> erlang:error({append_event_failed, Reason}) - end. - --spec patch_bundle(binary()) -> {ok, map()}. -patch_bundle(BuildSha) -> - Module = ?PATCH_MODULE, - ok = ensure_patch_module_loaded(), - {ok, CurrentMd5} = gateway_hotpatch_loader:current_md5(Module), - TargetBeam = compile_patch_module(?PATCH_TARGET_VERSION), - {ok, TargetMd5} = gateway_hotpatch_loader:beam_md5(TargetBeam), - {ok, BeamZstd} = compress_beam(TargetBeam), - {ok, #{ - version => 1, - build_sha => BuildSha, - modules => [ - #{ - module => Module, - expected_current_md5 => CurrentMd5, - target_md5 => TargetMd5, - beam_zstd => BeamZstd - } - ] - }}. - --spec ensure_patch_module_baseline() -> ok. -ensure_patch_module_baseline() -> - Module = ?PATCH_MODULE, - Beam = compile_patch_module(?PATCH_BASELINE_VERSION), - BeamPath = patch_beam_path(Module), - ok = file:write_file(BeamPath, Beam), - _ = code:soft_purge(Module), - _ = code:purge(Module), - _ = code:delete(Module), - _ = code:purge(Module), - case code:load_abs(filename:rootname(BeamPath)) of - {module, Module} -> ok; - {error, Reason} -> erlang:error({patch_baseline_load_failed, Reason}) - end. - --spec ensure_patch_module_loaded() -> ok. -ensure_patch_module_loaded() -> - case code:is_loaded(?PATCH_MODULE) of - false -> ensure_patch_module_baseline(); - {_File, _Loaded} -> ok - end. - --spec compile_patch_module(pos_integer()) -> binary(). -compile_patch_module(Version) -> - Module = ?PATCH_MODULE, - Source = patch_source_path(Module), - SourceText = io_lib:format( - "-module(~p).~n-export([version/0]).~nversion() -> ~p.~n", - [Module, Version] - ), - ok = file:write_file(Source, SourceText), - case compile:file(Source, [binary, return_errors, return_warnings]) of - {ok, Module, Beam} when is_binary(Beam) -> Beam; - {ok, Module, Beam, _Warnings} when is_binary(Beam) -> Beam; - Error -> erlang:error({patch_module_compile_failed, Module, Version, Error}) - end. - --spec patch_source_path(atom()) -> file:filename(). -patch_source_path(Module) -> - filename:join(patch_compile_dir(), atom_to_list(Module) ++ ".erl"). - --spec patch_beam_path(atom()) -> file:filename(). -patch_beam_path(Module) -> - filename:join(patch_compile_dir(), atom_to_list(Module) ++ ".beam"). - --spec patch_compile_dir() -> file:filename(). -patch_compile_dir() -> - Root = - case os:getenv("TMPDIR") of - false -> "/tmp"; - Value -> Value - end, - Dir = filename:join(Root, "fluxer_hotpatch_k8s_smoke"), - ok = filelib:ensure_dir(filename:join(Dir, "placeholder")), - Dir. - --spec maybe_assert_patch_version(non_neg_integer()) -> ok. -maybe_assert_patch_version(0) -> - ok; -maybe_assert_patch_version(_ExpectedAppliedCount) -> - assert_patch_version(?PATCH_TARGET_VERSION). - --spec assert_patch_version(pos_integer()) -> ok. -assert_patch_version(ExpectedVersion) -> - case erlang:apply(?PATCH_MODULE, version, []) of - ExpectedVersion -> ok; - OtherVersion -> erlang:error({patch_version_mismatch, OtherVersion, ExpectedVersion}) - end. - --spec compress_beam(binary()) -> {ok, binary()}. -compress_beam(Beam) -> - try erlang:apply(ezstd, compress, [Beam, 3]) of - Compressed when is_binary(Compressed) -> {ok, Compressed}; - {error, Reason} -> erlang:error({beam_compress_failed, Reason}); - Other -> erlang:error({beam_compress_failed, Other}) - catch - Class:Reason -> erlang:error({beam_compress_failed, Class, Reason}) - end. - --spec wait_ready(non_neg_integer(), pos_integer()) -> ok. -wait_ready(ExpectedAppliedCount, TimeoutMs) -> - Deadline = erlang:monotonic_time(millisecond) + TimeoutMs, - wait_ready_until(ExpectedAppliedCount, Deadline). - --spec wait_ready_until(non_neg_integer(), integer()) -> ok. -wait_ready_until(ExpectedAppliedCount, Deadline) -> - Status = gateway_hotpatch_reconciler:status(), - Ready = maps:get(ready, Status, false), - AppliedCount = maps:get(applied_count, Status, 0), - case Ready andalso AppliedCount >= ExpectedAppliedCount of - true -> - ok; - false -> - case erlang:monotonic_time(millisecond) >= Deadline of - true -> - erlang:error({hotpatch_not_ready, ExpectedAppliedCount, Status}); - false -> - timer:sleep(500), - gateway_hotpatch_reconciler:reconcile_async(), - wait_ready_until(ExpectedAppliedCount, Deadline) - end - end. - --spec assert_elapsed(atom(), non_neg_integer(), integer()) -> ok. -assert_elapsed(_Label, _ElapsedMs, MaxMs) when MaxMs =< 0 -> - ok; -assert_elapsed(_Label, ElapsedMs, MaxMs) when ElapsedMs =< MaxMs -> - ok; -assert_elapsed(Label, ElapsedMs, MaxMs) -> - erlang:error({hotpatch_elapsed_ms_exceeded, Label, ElapsedMs, MaxMs}). - --spec now_ms() -> integer(). -now_ms() -> - erlang:monotonic_time(millisecond). - --spec elapsed_ms(integer()) -> non_neg_integer(). -elapsed_ms(StartedMs) -> - max(0, now_ms() - StartedMs). - --spec int_env(string(), integer()) -> integer(). -int_env(Name, Default) -> - case os:getenv(Name) of - false -> - Default; - "" -> - Default; - Value -> - try list_to_integer(string:trim(Value)) of - Parsed -> Parsed - catch - error:badarg -> erlang:error({invalid_integer_env, Name, Value}) - end - end. - --spec must_env(string()) -> string(). -must_env(Name) -> - case os:getenv(Name) of - false -> erlang:error({missing_env, Name}); - "" -> erlang:error({missing_env, Name}); - Value -> Value - end. - --spec write_stdout(io:format(), [term()]) -> ok. -write_stdout(Format, Args) -> - write_stream(standard_io, Format, Args). - --spec write_stderr(io:format(), [term()]) -> ok. -write_stderr(Format, Args) -> - write_stream(standard_error, Format, Args). - --spec write_stream(file:io_device() | standard_io | standard_error, io:format(), [term()]) -> - ok. -write_stream(Device, Format, Args) -> - Output = iolist_to_binary(io_lib:format(Format, Args)), - _ = file:write(Device, Output), - ok. diff --git a/fluxer_gifs/src/klipy.rs b/fluxer_gifs/src/klipy.rs index 462a06df5..6f98aa7cd 100644 --- a/fluxer_gifs/src/klipy.rs +++ b/fluxer_gifs/src/klipy.rs @@ -889,131 +889,4 @@ mod tests { assert_eq!(gif.proxy_src, gif.media["webp"].proxy_src); assert_eq!(gif.media["gif"].width, 320); } - - #[tokio::test] - #[ignore] - async fn live_search_exposes_animated_image_variants() { - let api_key = std::env::var("FLUXER_KLIPY_API_KEY") - .or_else(|_| std::env::var("KLIPY_API_KEY")) - .expect("FLUXER_KLIPY_API_KEY or KLIPY_API_KEY set"); - let client = - KlipyClient::new(MediaProxyUrlBuilder::from_env().expect("media proxy env configured")) - .expect("KLIPY client"); - - let gifs = client - .search(&api_key, "move faster", "en_US", "US", 1) - .await - .expect("KLIPY search"); - let gif = gifs.first().expect("at least one GIF"); - - assert!(gif.media.contains_key("webm")); - assert!(gif.media.contains_key("webp")); - assert!(gif.media.contains_key("gif")); - assert!(gif.media.contains_key("tinygif") || gif.media.contains_key("nanogif")); - } - - #[tokio::test] - #[ignore] - async fn live_accepts_es_419_locale_for_picker_endpoints() { - let api_key = std::env::var("FLUXER_KLIPY_API_KEY") - .or_else(|_| std::env::var("KLIPY_API_KEY")) - .expect("FLUXER_KLIPY_API_KEY or KLIPY_API_KEY set"); - let locale = normalize_locale("es-419"); - assert_eq!(locale, "es"); - - for (endpoint, params) in [ - ( - "featured", - vec![ - ("country", "US"), - ("locale", locale.as_str()), - ("limit", "1"), - ], - ), - ( - "categories", - vec![ - ("country", "US"), - ("locale", locale.as_str()), - ("type", "featured"), - ], - ), - ( - "search", - vec![ - ("country", "US"), - ("locale", locale.as_str()), - ("q", "cat"), - ("limit", "1"), - ], - ), - ( - "autocomplete", - vec![("locale", locale.as_str()), ("q", "cat")], - ), - ] { - assert_live_klipy_endpoint_accepts(endpoint, &api_key, ¶ms).await; - } - } - - async fn assert_live_klipy_endpoint_accepts( - endpoint: &str, - api_key: &str, - params: &[(&str, &str)], - ) { - let mut url = Url::parse(&format!("{KLIPY_BASE_URL}/{endpoint}")).expect("KLIPY URL"); - { - let mut query = url.query_pairs_mut(); - query.append_pair("client_key", CLIENT_KEY); - query.append_pair("contentfilter", DEFAULT_CONTENT_FILTER); - query.append_pair("key", api_key); - for (key, value) in params { - query.append_pair(key, value); - } - } - - let status = reqwest::Client::builder() - .user_agent(FLUXER_USER_AGENT) - .build() - .expect("KLIPY HTTP client") - .get(url) - .send() - .await - .expect("KLIPY live request") - .status(); - assert!( - status.is_success(), - "KLIPY {endpoint} request failed with status {status}" - ); - } - - #[tokio::test] - #[ignore] - async fn live_resolves_klipy_url_with_direct_lookup() { - let api_key = std::env::var("FLUXER_KLIPY_API_KEY") - .or_else(|_| std::env::var("KLIPY_API_KEY")) - .expect("FLUXER_KLIPY_API_KEY or KLIPY_API_KEY set"); - let client = - KlipyClient::new(MediaProxyUrlBuilder::from_env().expect("media proxy env configured")) - .expect("KLIPY client"); - - let gif = client - .resolve_by_url( - &api_key, - "https://klipy.com/gifs/goatplaybanjo-chat-4", - "en-US", - "US", - ) - .await - .expect("KLIPY direct lookup") - .expect("resolved GIF"); - - assert_eq!(gif.slug, "goatplaybanjo-chat-4"); - assert_eq!(gif.provider, KLIPY_PROVIDER_NAME); - assert_eq!(gif.url, "https://klipy.com/gifs/goatplaybanjo-chat-4"); - assert!(gif.width > 0); - assert!(gif.height > 0); - assert!(gif.media.contains_key("webm") || gif.media.contains_key("mp4")); - assert!(gif.proxy_src.starts_with("http")); - } } diff --git a/fluxer_media_proxy/Cargo.toml b/fluxer_media_proxy/Cargo.toml index 3efb8f087..185d86371 100644 --- a/fluxer_media_proxy/Cargo.toml +++ b/fluxer_media_proxy/Cargo.toml @@ -13,10 +13,6 @@ path = "src/lib.rs" name = "fluxer-media-proxy" path = "src/main.rs" -[[bin]] -name = "fluxer-media-proxy-s3-fixture" -path = "src/bin/s3_fixture.rs" - [[bench]] name = "core" harness = false diff --git a/fluxer_media_proxy/scripts/media_benchmark.sh b/fluxer_media_proxy/scripts/media_benchmark.sh deleted file mode 100755 index 27b8c59f5..000000000 --- a/fluxer_media_proxy/scripts/media_benchmark.sh +++ /dev/null @@ -1,770 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -cd "${ROOT_DIR}" - -WORKSPACE_DIR="$(cd "${ROOT_DIR}/.." && pwd)" - -for tool in awk cargo curl date ffmpeg file grep mktemp paste sed seq sort tail tr uname xargs; do - if ! command -v "${tool}" >/dev/null 2>&1; then - printf 'missing required tool: %s\n' "${tool}" >&2 - exit 1 - fi -done - -BENCH_ITERATIONS="${BENCH_ITERATIONS:-2}" -BENCH_ONLINE="${BENCH_ONLINE:-1}" -BENCH_REAL_VIDEO="${BENCH_REAL_VIDEO:-1}" -BENCH_FULL="${BENCH_FULL:-0}" -BENCH_PARALLEL="${BENCH_PARALLEL:-1}" -BENCH_CONCURRENCY="${BENCH_CONCURRENCY:-4}" -BENCH_CONCURRENT_REQUESTS="${BENCH_CONCURRENT_REQUESTS:-8}" -BENCH_NATIVE_TRANSFORMS="${BENCH_NATIVE_TRANSFORMS:-4}" -BENCH_EXTERNAL="${BENCH_EXTERNAL:-1}" -BENCH_COMPAT_MATRIX="${BENCH_COMPAT_MATRIX:-1}" -BENCH_PORT="${BENCH_PORT:-19110}" -BENCH_RESULTS_DIR="${BENCH_RESULTS_DIR:-${ROOT_DIR}/bench-results}" -BENCH_CACHE_DIR="${BENCH_CACHE_DIR:-${ROOT_DIR}/.benchmark-cache/media}" -BENCH_DOWNLOAD_TIMEOUT="${BENCH_DOWNLOAD_TIMEOUT:-180}" -BENCH_STRICT_MIME="${BENCH_STRICT_MIME:-0}" - -RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)" -RUN_DIR="${BENCH_RESULTS_DIR}/${RUN_ID}" -WORK_DIR="$(mktemp -d /tmp/fluxer-media-bench.XXXXXX)" -STORAGE_ROOT="${WORK_DIR}/storage" -OUTPUT_DIR="${RUN_DIR}/outputs" -CSV="${RUN_DIR}/results.csv" -PARALLEL_CSV="${RUN_DIR}/parallel-results.csv" -SYSTEM_INFO="${RUN_DIR}/system.txt" -PROXY_LOG="${RUN_DIR}/proxy.log" -PROXY_PID="" -SERVER_URL="http://127.0.0.1:${BENCH_PORT}" -SECRET_KEY="benchmark-secret" - -cleanup() { - if [[ -n "${PROXY_PID}" ]]; then - kill "${PROXY_PID}" 2>/dev/null || true - wait "${PROXY_PID}" 2>/dev/null || true - fi - rm -rf "${WORK_DIR}" -} -trap cleanup EXIT - -mkdir -p "${RUN_DIR}" "${OUTPUT_DIR}" "${BENCH_CACHE_DIR}" -printf 'case,source,kind,input_format,operation,iteration,status,time_total_ms,size_download_bytes,content_type,output_file,validation\n' >"${CSV}" -printf 'case,source,kind,input_format,operation,requests,concurrency,successes,total_time_ms,requests_per_second,avg_latency_ms,total_bytes,validation\n' >"${PARALLEL_CSV}" - -note() { - printf '[bench] %s\n' "$*" >&2 -} - -has_encoder() { - ffmpeg -hide_banner -encoders 2>/dev/null | awk '{print $2}' | grep -qx "$1" -} - -has_muxer() { - ffmpeg -hide_banner -muxers 2>/dev/null | awk '{print $2}' | grep -qx "$1" -} - -wait_for_url() { - local url="$1" - for _ in $(seq 1 160); do - if curl -fsS "${url}" >/dev/null 2>&1; then - return 0 - fi - sleep 0.25 - done - printf 'timed out waiting for %s\n' "${url}" >&2 - return 1 -} - -header_value() { - local header_file="$1" - local name="$2" - sed -n "s/^[[:space:]]*${name}:[[:space:]]*//Ip" "${header_file}" | tail -n 1 | tr -d '\r' -} - -mime_matches() { - local actual="$1" - local expected="$2" - [[ -z "${expected}" || "${actual,,}" == "${expected,,}"* ]] -} - -validate_output() { - local file_path="$1" - local expected_mime="$2" - local content_type="$3" - if [[ ! -s "${file_path}" ]]; then - printf 'empty-output' - return - fi - if ! mime_matches "${content_type}" "${expected_mime}"; then - printf 'mime-mismatch:%s' "${content_type:-missing}" - return - fi - case "${expected_mime}" in - image/webp) - file "${file_path}" | grep -q 'Web/P image' && printf 'ok' || printf 'file-mismatch' - ;; - image/jpeg) - file "${file_path}" | grep -q 'JPEG image data' && printf 'ok' || printf 'file-mismatch' - ;; - image/png) - file "${file_path}" | grep -q 'PNG image data' && printf 'ok' || printf 'file-mismatch' - ;; - image/gif) - file "${file_path}" | grep -q 'GIF image data' && printf 'ok' || printf 'file-mismatch' - ;; - image/avif) - file "${file_path}" | grep -Eqi 'AVIF|ISO Media' && printf 'ok' || printf 'file-mismatch' - ;; - video/mp4) - file "${file_path}" | grep -Eqi 'ISO Media|MP4' && printf 'ok' || printf 'file-mismatch' - ;; - video/webm) - file "${file_path}" | grep -qi 'WebM' && printf 'ok' || printf 'file-mismatch' - ;; - audio/ogg) - file "${file_path}" | grep -Eqi 'Ogg|Vorbis|Opus' && printf 'ok' || printf 'file-mismatch' - ;; - audio/wav) - file "${file_path}" | grep -Eqi 'WAVE|WAV|RIFF' && printf 'ok' || printf 'file-mismatch' - ;; - text/plain) - printf 'ok' - ;; - *) - printf 'ok' - ;; - esac -} - -safe_name() { - printf '%s' "$1" | sed 's/[^A-Za-z0-9_.-]/_/g' -} - -write_system_info() { - { - printf 'run_id=%s\n' "${RUN_ID}" - printf 'date_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" - printf 'uname=%s\n' "$(uname -a)" - printf 'rustc=%s\n' "$(rustc --version)" - printf 'ffmpeg=%s\n' "$(ffmpeg -hide_banner -version | sed -n '1p')" - printf 'bench_iterations=%s\n' "${BENCH_ITERATIONS}" - printf 'bench_online=%s\n' "${BENCH_ONLINE}" - printf 'bench_real_video=%s\n' "${BENCH_REAL_VIDEO}" - printf 'bench_full=%s\n' "${BENCH_FULL}" - printf 'bench_parallel=%s\n' "${BENCH_PARALLEL}" - printf 'bench_external=%s\n' "${BENCH_EXTERNAL}" - printf 'bench_compat_matrix=%s\n' "${BENCH_COMPAT_MATRIX}" - printf 'bench_concurrency=%s\n' "${BENCH_CONCURRENCY}" - printf 'bench_concurrent_requests=%s\n' "${BENCH_CONCURRENT_REQUESTS}" - printf 'bench_native_transforms=%s\n' "${BENCH_NATIVE_TRANSFORMS}" - printf 'encoders=' - ffmpeg -hide_banner -encoders 2>/dev/null | awk '{print $2}' | grep -E '^(libx264|libvpx-vp9|libwebp|libwebp_anim|libaom-av1|gif|png|mjpeg)$' | sort | paste -sd ',' - || true - printf '\n' - } >"${SYSTEM_INFO}" -} - -copy_cdn() { - local key="$1" - local source="$2" - mkdir -p "$(dirname "${STORAGE_ROOT}/cdn/${key}")" - cp "${source}" "${STORAGE_ROOT}/cdn/${key}" -} - -copy_upload() { - local key="$1" - local source="$2" - mkdir -p "$(dirname "${STORAGE_ROOT}/uploads/${key}")" - cp "${source}" "${STORAGE_ROOT}/uploads/${key}" -} - -download_fixture() { - local name="$1" - local url="$2" - local max_bytes="$3" - local dest="$4" - local cached="${BENCH_CACHE_DIR}/${name}" - if [[ -s "${cached}" ]]; then - cp "${cached}" "${dest}" - return 0 - fi - if [[ "${BENCH_ONLINE}" != "1" ]]; then - note "skipping ${name}; not cached and BENCH_ONLINE=0" - return 1 - fi - local tmp="${cached}.tmp" - note "downloading ${name} into ${BENCH_CACHE_DIR}" - if ! curl -fL --retry 0 --connect-timeout 10 --max-time "${BENCH_DOWNLOAD_TIMEOUT}" --max-filesize "${max_bytes}" -o "${tmp}" "${url}"; then - rm -f "${tmp}" - note "skipping ${name}; download failed" - return 1 - fi - mv "${tmp}" "${cached}" - cp "${cached}" "${dest}" -} - -generate_fixtures() { - mkdir -p "${WORK_DIR}/fixtures" "${STORAGE_ROOT}/cdn/attachments/bench" "${STORAGE_ROOT}/uploads" - if ! has_encoder libx264; then - printf 'missing required ffmpeg encoder: libx264\n' >&2 - exit 1 - fi - printf '0123456789abcdef\n' >"${WORK_DIR}/fixtures/range.txt" - copy_cdn "attachments/bench/text/range.txt" "${WORK_DIR}/fixtures/range.txt" - - note "generating synthetic high-resolution images" - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=3840x2160:rate=1:duration=1 \ - -frames:v 1 "${WORK_DIR}/fixtures/synthetic-4k.png" - copy_cdn "attachments/bench/images/synthetic-4k.png" "${WORK_DIR}/fixtures/synthetic-4k.png" - copy_cdn "avatars/42/benchpng" "${WORK_DIR}/fixtures/synthetic-4k.png" - copy_upload "upload-image.png" "${WORK_DIR}/fixtures/synthetic-4k.png" - - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=3840x2160:rate=1:duration=1 \ - -frames:v 1 -q:v 2 "${WORK_DIR}/fixtures/synthetic-4k.jpg" - copy_cdn "attachments/bench/images/synthetic-4k.jpg" "${WORK_DIR}/fixtures/synthetic-4k.jpg" - - if has_encoder libwebp; then - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=3840x2160:rate=1:duration=1 \ - -frames:v 1 -c:v libwebp -q:v 80 "${WORK_DIR}/fixtures/synthetic-4k.webp" - copy_cdn "attachments/bench/images/synthetic-4k.webp" "${WORK_DIR}/fixtures/synthetic-4k.webp" - fi - - if has_encoder libaom-av1 && has_muxer avif; then - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1920x1080:rate=1:duration=1 \ - -frames:v 1 -c:v libaom-av1 -still-picture 1 -cpu-used 8 -crf 36 -b:v 0 \ - "${WORK_DIR}/fixtures/synthetic-1080p.avif" || true - if [[ -s "${WORK_DIR}/fixtures/synthetic-1080p.avif" ]]; then - copy_cdn "attachments/bench/images/synthetic-1080p.avif" "${WORK_DIR}/fixtures/synthetic-1080p.avif" - fi - fi - - note "generating synthetic animated GIF" - local gif_size="1280x720" - if [[ "${BENCH_FULL}" == "1" ]]; then - gif_size="1920x1080" - fi - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i "testsrc2=size=${gif_size}:rate=15:duration=2" \ - -vf "fps=15" "${WORK_DIR}/fixtures/synthetic-${gif_size}.gif" - copy_cdn "attachments/bench/gifs/synthetic-${gif_size}.gif" "${WORK_DIR}/fixtures/synthetic-${gif_size}.gif" - - note "generating synthetic videos" - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=2 \ - -f lavfi -i sine=frequency=440:duration=2 \ - -c:v libx264 -preset veryfast -pix_fmt yuv420p -c:a aac -movflags +faststart \ - "${WORK_DIR}/fixtures/synthetic-1080p.mp4" - copy_cdn "attachments/bench/videos/synthetic-1080p.mp4" "${WORK_DIR}/fixtures/synthetic-1080p.mp4" - copy_upload "upload-video.mp4" "${WORK_DIR}/fixtures/synthetic-1080p.mp4" - - if [[ "${BENCH_FULL}" == "1" ]]; then - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=3840x2160:rate=30:duration=2 \ - -f lavfi -i sine=frequency=660:duration=2 \ - -c:v libx264 -preset ultrafast -pix_fmt yuv420p -c:a aac -movflags +faststart \ - "${WORK_DIR}/fixtures/synthetic-4k.mp4" - copy_cdn "attachments/bench/videos/synthetic-4k.mp4" "${WORK_DIR}/fixtures/synthetic-4k.mp4" - fi - - if has_encoder libvpx-vp9; then - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1280x720:rate=30:duration=2 \ - -c:v libvpx-vp9 -deadline realtime -cpu-used 8 -b:v 2M -an \ - "${WORK_DIR}/fixtures/synthetic-720p.webm" - copy_cdn "attachments/bench/videos/synthetic-720p.webm" "${WORK_DIR}/fixtures/synthetic-720p.webm" - fi - - if [[ "${BENCH_COMPAT_MATRIX}" == "1" ]]; then - note "generating synthetic video compatibility matrix" - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=2 \ - -c:v libx264 -preset veryfast -pix_fmt yuv420p -f mov \ - "${WORK_DIR}/fixtures/compat-h264-1080p.mov" - copy_cdn "attachments/bench/compat/videos/h264-1080p.mov" "${WORK_DIR}/fixtures/compat-h264-1080p.mov" - - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=2 \ - -c:v libx264 -preset veryfast -pix_fmt yuv420p -f matroska \ - "${WORK_DIR}/fixtures/compat-h264-1080p.mkv" - copy_cdn "attachments/bench/compat/videos/h264-1080p.mkv" "${WORK_DIR}/fixtures/compat-h264-1080p.mkv" - - if has_encoder libx265; then - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=2 \ - -c:v libx265 -preset ultrafast -x265-params log-level=error -pix_fmt yuv420p -f mov \ - "${WORK_DIR}/fixtures/compat-hevc-1080p.mov" - copy_cdn "attachments/bench/compat/videos/hevc-1080p.mov" "${WORK_DIR}/fixtures/compat-hevc-1080p.mov" - fi - - if has_encoder prores_ks; then - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=1 \ - -c:v prores_ks -profile:v 0 -pix_fmt yuv422p10le -f mov \ - "${WORK_DIR}/fixtures/compat-prores-1080p.mov" - copy_cdn "attachments/bench/compat/videos/prores-1080p.mov" "${WORK_DIR}/fixtures/compat-prores-1080p.mov" - fi - - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1280x720:rate=30:duration=2 \ - -c:v mpeg4 -pix_fmt yuv420p -f avi \ - "${WORK_DIR}/fixtures/compat-mpeg4-720p.avi" - copy_cdn "attachments/bench/compat/videos/mpeg4-720p.avi" "${WORK_DIR}/fixtures/compat-mpeg4-720p.avi" - - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i testsrc2=size=1280x720:rate=30:duration=2 \ - -c:v mpeg2video -pix_fmt yuv420p -f mpegts \ - "${WORK_DIR}/fixtures/compat-mpeg2-720p.ts" - copy_cdn "attachments/bench/compat/videos/mpeg2-720p.ts" "${WORK_DIR}/fixtures/compat-mpeg2-720p.ts" - fi - - note "generating synthetic audio" - ffmpeg -hide_banner -loglevel error -y \ - -f lavfi -i sine=frequency=440:duration=30 \ - -c:a pcm_s16le "${WORK_DIR}/fixtures/synthetic-30s.wav" - copy_cdn "attachments/bench/audio/synthetic-30s.wav" "${WORK_DIR}/fixtures/synthetic-30s.wav" - - if download_fixture "picsum-id10-4k.jpg" \ - "https://picsum.photos/id/10/3840/2160.jpg" \ - 20000000 "${WORK_DIR}/fixtures/real-photo-4k.jpg"; then - copy_cdn "attachments/bench/online/real-photo-4k.jpg" "${WORK_DIR}/fixtures/real-photo-4k.jpg" - fi - - if download_fixture "google-webp-gallery-1.png" \ - "https://www.gstatic.com/webp/gallery3/1.png" \ - 10000000 "${WORK_DIR}/fixtures/google-webp-gallery-1.png"; then - copy_cdn "attachments/bench/online/google-webp-gallery-1.png" "${WORK_DIR}/fixtures/google-webp-gallery-1.png" - fi - - if download_fixture "google-webp-gallery-1.webp" \ - "https://www.gstatic.com/webp/gallery3/1_webp_ll.webp" \ - 10000000 "${WORK_DIR}/fixtures/google-webp-gallery-1.webp"; then - copy_cdn "attachments/bench/online/google-webp-gallery-1.webp" "${WORK_DIR}/fixtures/google-webp-gallery-1.webp" - fi - - if download_fixture "google-webp-animated-1.gif" \ - "https://www.gstatic.com/webp/animated/1.gif" \ - 20000000 "${WORK_DIR}/fixtures/google-webp-animated-1.gif"; then - copy_cdn "attachments/bench/online/google-webp-animated-1.gif" "${WORK_DIR}/fixtures/google-webp-animated-1.gif" - fi - - if [[ "${BENCH_FULL}" == "1" ]]; then - if download_fixture "fronalpstock-big.jpg" \ - "https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg" \ - 60000000 "${WORK_DIR}/fixtures/fronalpstock-big.jpg"; then - copy_cdn "attachments/bench/online/fronalpstock-big.jpg" "${WORK_DIR}/fixtures/fronalpstock-big.jpg" - fi - - if download_fixture "rotating-earth.gif" \ - "https://upload.wikimedia.org/wikipedia/commons/e/e1/Rotating_earth_animated.gif" \ - 80000000 "${WORK_DIR}/fixtures/rotating-earth.gif"; then - copy_cdn "attachments/bench/online/rotating-earth.gif" "${WORK_DIR}/fixtures/rotating-earth.gif" - fi - fi - - if [[ "${BENCH_REAL_VIDEO}" == "1" ]]; then - if download_fixture "sample-cat-1920x1080-mov" \ - "https://disk.sample.cat/samples/mov/1416529-hd_1920_1080_30fps.mov" \ - 40000000 "${WORK_DIR}/fixtures/sample-cat-1080p.mov"; then - copy_cdn "attachments/bench/online/sample-cat-1080p.mov" "${WORK_DIR}/fixtures/sample-cat-1080p.mov" - fi - if download_fixture "mdn-flower.mp4" \ - "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" \ - 20000000 "${WORK_DIR}/fixtures/mdn-flower.mp4"; then - copy_cdn "attachments/bench/online/mdn-flower.mp4" "${WORK_DIR}/fixtures/mdn-flower.mp4" - fi - if download_fixture "mdn-flower.webm" \ - "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" \ - 20000000 "${WORK_DIR}/fixtures/mdn-flower.webm"; then - copy_cdn "attachments/bench/online/mdn-flower.webm" "${WORK_DIR}/fixtures/mdn-flower.webm" - fi - if download_fixture "big-buck-bunny-720p-10s.mp4" \ - "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4" \ - 20000000 "${WORK_DIR}/fixtures/big-buck-bunny-720p-10s.mp4"; then - copy_cdn "attachments/bench/online/big-buck-bunny-720p-10s.mp4" "${WORK_DIR}/fixtures/big-buck-bunny-720p-10s.mp4" - fi - if [[ "${BENCH_FULL}" == "1" ]]; then - if download_fixture "wikimedia-big-buck-bunny-medium-ogv" \ - "https://commons.wikimedia.org/wiki/Special:Redirect/file/Big_Buck_Bunny_medium.ogv" \ - 180000000 "${WORK_DIR}/fixtures/big-buck-bunny-medium.ogv"; then - copy_cdn "attachments/bench/online/big-buck-bunny-medium.ogv" "${WORK_DIR}/fixtures/big-buck-bunny-medium.ogv" - fi - fi - fi - - if download_fixture "maple-leaf-ragq.ogg" \ - "https://commons.wikimedia.org/wiki/Special:Redirect/file/Maple_Leaf_RagQ.ogg" \ - 20000000 "${WORK_DIR}/fixtures/maple-leaf-ragq.ogg"; then - copy_cdn "attachments/bench/online/maple-leaf-ragq.ogg" "${WORK_DIR}/fixtures/maple-leaf-ragq.ogg" - fi - - if [[ "${BENCH_FULL}" == "1" ]]; then - if download_fixture "candles-green-heads-and-skulls.ogg" \ - "https://commons.wikimedia.org/wiki/Special:Redirect/file/018-_Candles_Green,_Heads_and_Skulls.ogg" \ - 160000000 "${WORK_DIR}/fixtures/candles-green-heads-and-skulls.ogg"; then - copy_cdn "attachments/bench/online/candles-green-heads-and-skulls.ogg" "${WORK_DIR}/fixtures/candles-green-heads-and-skulls.ogg" - fi - fi -} - -start_proxy() { - note "building release binary" - (cd "${WORKSPACE_DIR}" && cargo build --release -p fluxer-media-proxy) - note "starting proxy on ${SERVER_URL}" - env \ - "FLUXER_MEDIA_PROXY_SECRET_KEY=${SECRET_KEY}" \ - "FLUXER_MEDIA_PROXY_STORAGE_BACKEND=local" \ - "FLUXER_MEDIA_PROXY_STORAGE_ROOT=${STORAGE_ROOT}" \ - "FLUXER_MEDIA_PROXY_MAX_NATIVE_TRANSFORMS=${BENCH_NATIVE_TRANSFORMS}" \ - "FLUXER_MEDIA_PROXY_HOST=127.0.0.1" \ - "FLUXER_MEDIA_PROXY_PORT=${BENCH_PORT}" \ - "${WORKSPACE_DIR}/target/release/fluxer-media-proxy" >"${PROXY_LOG}" 2>&1 & - PROXY_PID="$!" - wait_for_url "${SERVER_URL}/_health" -} - -run_case() { - local case_name="$1" - local source="$2" - local kind="$3" - local input_format="$4" - local operation="$5" - local url="$6" - local expected_status="$7" - local expected_mime="$8" - local extension="$9" - local extra_header="${10:-}" - local request_args=() - if [[ -n "${extra_header}" ]]; then - request_args=(-H "${extra_header}") - fi - - local output_base - output_base="$(safe_name "${case_name}")" - note "benchmarking ${case_name}" - curl -fsS "${request_args[@]}" "${url}" -o /dev/null >/dev/null 2>&1 || true - for iteration in $(seq 1 "${BENCH_ITERATIONS}"); do - local headers="${WORK_DIR}/${output_base}-${iteration}.headers" - local output="${OUTPUT_DIR}/${output_base}-${iteration}.${extension}" - local metrics status seconds bytes ms content_type validation - metrics="$(curl -sS "${request_args[@]}" -D "${headers}" -o "${output}" -w '%{http_code} %{time_total} %{size_download}' "${url}")" - status="$(printf '%s' "${metrics}" | awk '{print $1}')" - seconds="$(printf '%s' "${metrics}" | awk '{print $2}')" - bytes="$(printf '%s' "${metrics}" | awk '{print $3}')" - ms="$(awk -v s="${seconds}" 'BEGIN { printf "%.3f", s * 1000 }')" - content_type="$(header_value "${headers}" "content-type")" - validation="$(validate_output "${output}" "${expected_mime}" "${content_type}")" - printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \ - "${case_name}" "${source}" "${kind}" "${input_format}" "${operation}" "${iteration}" "${status}" "${ms}" "${bytes}" "${content_type}" "${output}" "${validation}" >>"${CSV}" - if [[ "${status}" != "${expected_status}" ]]; then - printf 'case %s returned HTTP %s, expected %s\n' "${case_name}" "${status}" "${expected_status}" >&2 - return 1 - fi - if [[ "${BENCH_STRICT_MIME}" == "1" && "${validation}" != "ok" ]]; then - printf 'case %s validation failed: %s\n' "${case_name}" "${validation}" >&2 - return 1 - fi - done -} - -run_post_case() { - local case_name="$1" - local source="$2" - local kind="$3" - local input_format="$4" - local operation="$5" - local url="$6" - local payload="$7" - local expected_status="$8" - local expected_mime="$9" - local extension="${10}" - - local output_base - output_base="$(safe_name "${case_name}")" - note "benchmarking ${case_name}" - curl -fsS -X POST -H "Authorization: Bearer ${SECRET_KEY}" -H 'Content-Type: application/json' --data "${payload}" "${url}" -o /dev/null >/dev/null 2>&1 || true - for iteration in $(seq 1 "${BENCH_ITERATIONS}"); do - local headers="${WORK_DIR}/${output_base}-${iteration}.headers" - local output="${OUTPUT_DIR}/${output_base}-${iteration}.${extension}" - local metrics status seconds bytes ms content_type validation - metrics="$(curl -sS -X POST -H "Authorization: Bearer ${SECRET_KEY}" -H 'Content-Type: application/json' --data "${payload}" -D "${headers}" -o "${output}" -w '%{http_code} %{time_total} %{size_download}' "${url}")" - status="$(printf '%s' "${metrics}" | awk '{print $1}')" - seconds="$(printf '%s' "${metrics}" | awk '{print $2}')" - bytes="$(printf '%s' "${metrics}" | awk '{print $3}')" - ms="$(awk -v s="${seconds}" 'BEGIN { printf "%.3f", s * 1000 }')" - content_type="$(header_value "${headers}" "content-type")" - validation="$(validate_output "${output}" "${expected_mime}" "${content_type}")" - printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \ - "${case_name}" "${source}" "${kind}" "${input_format}" "${operation}" "${iteration}" "${status}" "${ms}" "${bytes}" "${content_type}" "${output}" "${validation}" >>"${CSV}" - if [[ "${status}" != "${expected_status}" ]]; then - printf 'case %s returned HTTP %s, expected %s\n' "${case_name}" "${status}" "${expected_status}" >&2 - return 1 - fi - if [[ "${BENCH_STRICT_MIME}" == "1" && "${validation}" != "ok" ]]; then - printf 'case %s validation failed: %s\n' "${case_name}" "${validation}" >&2 - return 1 - fi - done -} - -run_if_exists() { - local storage_path="$1" - shift - if [[ -s "${STORAGE_ROOT}/cdn/${storage_path}" ]]; then - run_case "$@" - else - note "skipping $1; missing ${storage_path}" - fi -} - -external_url() { - local upstream="$1" - cargo run --quiet --manifest-path "${WORKSPACE_DIR}/Cargo.toml" --package fluxer-dev -- \ - media-proxy sign-external-url \ - --secret-key "${SECRET_KEY}" \ - --server-url "${SERVER_URL}" \ - "${upstream}" -} - -run_parallel_case() { - local case_name="$1" - local source="$2" - local kind="$3" - local input_format="$4" - local operation="$5" - local url="$6" - local expected_status="$7" - local expected_mime="$8" - local extension="$9" - if [[ "${BENCH_PARALLEL}" != "1" ]]; then - return 0 - fi - - local output_base tmp start_ns end_ns total_ms successes total_bytes avg_latency rps validation - output_base="$(safe_name "${case_name}")" - tmp="${WORK_DIR}/${output_base}-parallel.txt" - note "parallel benchmark ${case_name} (${BENCH_CONCURRENT_REQUESTS} requests, ${BENCH_CONCURRENCY} concurrent)" - start_ns="$(date +%s%N)" - if ! seq 1 "${BENCH_CONCURRENT_REQUESTS}" | xargs -P "${BENCH_CONCURRENCY}" -I '{}' sh -c ' - headers="$1/'"${output_base}"'-parallel-{}.headers" - output="$2/'"${output_base}"'-parallel-{}.'"${extension}"'" - curl -sS -D "${headers}" -o "${output}" -w "%{http_code} %{time_total} %{size_download}\n" "$3" - ' sh "${WORK_DIR}" "${OUTPUT_DIR}" "${url}" >"${tmp}"; then - note "one or more parallel curl workers failed for ${case_name}" - fi - end_ns="$(date +%s%N)" - total_ms="$(awk -v start="${start_ns}" -v end="${end_ns}" 'BEGIN { printf "%.3f", (end - start) / 1000000 }')" - successes="$(awk -v status="${expected_status}" '$1 == status { count++ } END { print count + 0 }' "${tmp}")" - total_bytes="$(awk '{ sum += $3 } END { print sum + 0 }' "${tmp}")" - avg_latency="$(awk '{ sum += $2; count++ } END { if (count == 0) print "0.000"; else printf "%.3f", (sum / count) * 1000 }' "${tmp}")" - rps="$(awk -v req="${BENCH_CONCURRENT_REQUESTS}" -v ms="${total_ms}" 'BEGIN { if (ms == 0) print "0.000"; else printf "%.3f", req / (ms / 1000) }')" - local first_output="${OUTPUT_DIR}/${output_base}-parallel-1.${extension}" - local first_headers="${WORK_DIR}/${output_base}-parallel-1.headers" - validation="$(validate_output "${first_output}" "${expected_mime}" "$(header_value "${first_headers}" "content-type")")" - printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \ - "${case_name}" "${source}" "${kind}" "${input_format}" "${operation}" "${BENCH_CONCURRENT_REQUESTS}" "${BENCH_CONCURRENCY}" "${successes}" "${total_ms}" "${rps}" "${avg_latency}" "${total_bytes}" "${validation}" >>"${PARALLEL_CSV}" - if [[ "${successes}" != "${BENCH_CONCURRENT_REQUESTS}" ]]; then - printf 'parallel case %s had %s/%s successful responses\n' "${case_name}" "${successes}" "${BENCH_CONCURRENT_REQUESTS}" >&2 - return 1 - fi - if [[ "${BENCH_STRICT_MIME}" == "1" && "${validation}" != "ok" ]]; then - printf 'parallel case %s validation failed: %s\n' "${case_name}" "${validation}" >&2 - return 1 - fi -} - -run_benchmarks() { - run_case "range_text_full" "synthetic" "stream" "txt" "full_stream" \ - "${SERVER_URL}/attachments/bench/text/range.txt" "200" "application/octet-stream" "txt" - run_case "range_text_partial" "synthetic" "stream" "txt" "range_stream" \ - "${SERVER_URL}/attachments/bench/text/range.txt" "206" "application/octet-stream" "txt" "Range: bytes=2-7" - run_case "synthetic_audio_wav_full" "synthetic" "audio" "wav" "full_stream" \ - "${SERVER_URL}/attachments/bench/audio/synthetic-30s.wav" "200" "audio/wav" "wav" - run_case "synthetic_audio_wav_range" "synthetic" "audio" "wav" "range_stream" \ - "${SERVER_URL}/attachments/bench/audio/synthetic-30s.wav" "206" "audio/wav" "wav" "Range: bytes=0-65535" - run_case "synthetic_mp4_1080p_full" "synthetic" "video" "mp4" "full_stream" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-1080p.mp4" "200" "video/mp4" "mp4" - run_case "synthetic_mp4_1080p_range" "synthetic" "video" "mp4" "range_stream" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-1080p.mp4" "206" "video/mp4" "mp4" "Range: bytes=0-65535" - - run_case "synthetic_png_4k_to_webp_512" "synthetic" "image" "png" "resize_to_webp_512" \ - "${SERVER_URL}/attachments/bench/images/synthetic-4k.png?width=512&format=webp&quality=high" "200" "image/webp" "webp" - run_case "synthetic_png_4k_to_jpeg_2048" "synthetic" "image" "png" "resize_to_jpeg_2048" \ - "${SERVER_URL}/attachments/bench/images/synthetic-4k.png?width=2048&format=jpeg&quality=high" "200" "image/jpeg" "jpg" - run_case "synthetic_jpeg_4k_to_webp_1024" "synthetic" "image" "jpeg" "resize_to_webp_1024" \ - "${SERVER_URL}/attachments/bench/images/synthetic-4k.jpg?width=1024&format=webp&quality=high" "200" "image/webp" "webp" - run_case "avatar_png_4k_to_webp_512" "synthetic" "avatar" "png" "avatar_square_webp_512" \ - "${SERVER_URL}/avatars/42/benchpng.png?size=512&format=webp" "200" "image/webp" "webp" - - run_if_exists "attachments/bench/images/synthetic-4k.webp" \ - "synthetic_webp_4k_to_png_1024" "synthetic" "image" "webp" "resize_to_png_1024" \ - "${SERVER_URL}/attachments/bench/images/synthetic-4k.webp?width=1024&format=png" "200" "image/png" "png" - run_if_exists "attachments/bench/images/synthetic-1080p.avif" \ - "synthetic_avif_1080p_to_webp_768" "synthetic" "image" "avif" "resize_to_webp_768" \ - "${SERVER_URL}/attachments/bench/images/synthetic-1080p.avif?width=768&format=webp" "200" "image/webp" "webp" - - run_case "synthetic_gif_to_gif_animated_512" "synthetic" "gif" "gif" "animated_resize_to_gif_512" \ - "${SERVER_URL}/attachments/bench/gifs/$(cd "${STORAGE_ROOT}/cdn/attachments/bench/gifs" && ls synthetic-*.gif | sort | tail -n 1)?width=512&format=webp&animated=true" "200" "image/gif" "gif" - run_case "synthetic_gif_to_webp_static_512" "synthetic" "gif" "gif" "static_resize_to_webp_512" \ - "${SERVER_URL}/attachments/bench/gifs/$(cd "${STORAGE_ROOT}/cdn/attachments/bench/gifs" && ls synthetic-*.gif | sort | tail -n 1)?width=512&format=webp" "200" "image/webp" "webp" - - run_case "synthetic_mp4_1080p_to_jpeg" "synthetic" "video" "mp4" "thumbnail_to_jpeg" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-1080p.mp4?format=jpeg" "200" "image/jpeg" "jpg" - run_case "synthetic_mp4_1080p_to_webp" "synthetic" "video" "mp4" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-1080p.mp4?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/videos/synthetic-720p.webm" \ - "synthetic_webm_720p_to_jpeg" "synthetic" "video" "webm" "thumbnail_to_jpeg" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-720p.webm?format=jpeg" "200" "image/jpeg" "jpg" - run_if_exists "attachments/bench/videos/synthetic-4k.mp4" \ - "synthetic_mp4_4k_to_jpeg" "synthetic" "video" "mp4" "thumbnail_to_jpeg" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-4k.mp4?format=jpeg" "200" "image/jpeg" "jpg" - - if [[ "${BENCH_COMPAT_MATRIX}" == "1" ]]; then - run_if_exists "attachments/bench/compat/videos/h264-1080p.mov" \ - "compat_h264_mov_1080p_to_webp" "synthetic" "video" "mov" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/compat/videos/h264-1080p.mov?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/compat/videos/hevc-1080p.mov" \ - "compat_hevc_mov_1080p_to_webp" "synthetic" "video" "mov" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/compat/videos/hevc-1080p.mov?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/compat/videos/prores-1080p.mov" \ - "compat_prores_mov_1080p_to_webp" "synthetic" "video" "mov" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/compat/videos/prores-1080p.mov?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/compat/videos/h264-1080p.mkv" \ - "compat_h264_mkv_1080p_to_webp" "synthetic" "video" "mkv" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/compat/videos/h264-1080p.mkv?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/compat/videos/mpeg4-720p.avi" \ - "compat_mpeg4_avi_720p_to_webp" "synthetic" "video" "avi" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/compat/videos/mpeg4-720p.avi?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/compat/videos/mpeg2-720p.ts" \ - "compat_mpeg2_ts_720p_to_webp" "synthetic" "video" "ts" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/compat/videos/mpeg2-720p.ts?format=webp" "200" "image/webp" "webp" - fi - - run_if_exists "attachments/bench/online/real-photo-4k.jpg" \ - "real_photo_jpeg_4k_to_webp_1024" "online" "image" "jpeg" "resize_to_webp_1024" \ - "${SERVER_URL}/attachments/bench/online/real-photo-4k.jpg?width=1024&format=webp&quality=high" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/real-photo-4k.jpg" \ - "real_photo_jpeg_4k_to_jpeg_2048" "online" "image" "jpeg" "resize_to_jpeg_2048" \ - "${SERVER_URL}/attachments/bench/online/real-photo-4k.jpg?width=2048&format=jpeg&quality=high" "200" "image/jpeg" "jpg" - run_if_exists "attachments/bench/online/google-webp-gallery-1.png" \ - "real_google_png_to_webp_1024" "online" "image" "png" "resize_to_webp_1024" \ - "${SERVER_URL}/attachments/bench/online/google-webp-gallery-1.png?width=1024&format=webp&quality=high" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/google-webp-gallery-1.webp" \ - "real_google_webp_to_jpeg_1024" "online" "image" "webp" "resize_to_jpeg_1024" \ - "${SERVER_URL}/attachments/bench/online/google-webp-gallery-1.webp?width=1024&format=jpeg&quality=high" "200" "image/jpeg" "jpg" - run_if_exists "attachments/bench/online/google-webp-animated-1.gif" \ - "real_google_gif_to_gif_animated_512" "online" "gif" "gif" "animated_resize_to_gif_512" \ - "${SERVER_URL}/attachments/bench/online/google-webp-animated-1.gif?width=512&format=webp&animated=true" "200" "image/gif" "gif" - run_if_exists "attachments/bench/online/google-webp-animated-1.gif" \ - "real_google_gif_to_webp_static_512" "online" "gif" "gif" "static_resize_to_webp_512" \ - "${SERVER_URL}/attachments/bench/online/google-webp-animated-1.gif?width=512&format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/fronalpstock-big.jpg" \ - "real_fronalpstock_jpeg_to_webp_1024" "online-full" "image" "jpeg" "resize_to_webp_1024" \ - "${SERVER_URL}/attachments/bench/online/fronalpstock-big.jpg?width=1024&format=webp&quality=high" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/rotating-earth.gif" \ - "real_rotating_earth_gif_to_gif_animated_512" "online-full" "gif" "gif" "animated_resize_to_gif_512" \ - "${SERVER_URL}/attachments/bench/online/rotating-earth.gif?width=512&format=webp&animated=true" "200" "image/gif" "gif" - - run_if_exists "attachments/bench/online/mdn-flower.mp4" \ - "real_mdn_flower_mp4_to_jpeg" "online" "video" "mp4" "thumbnail_to_jpeg" \ - "${SERVER_URL}/attachments/bench/online/mdn-flower.mp4?format=jpeg" "200" "image/jpeg" "jpg" - run_if_exists "attachments/bench/online/sample-cat-1080p.mov" \ - "real_sample_cat_mov_1080p_to_webp" "online" "video" "mov" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/online/sample-cat-1080p.mov?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/mdn-flower.webm" \ - "real_mdn_flower_webm_to_webp" "online" "video" "webm" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/online/mdn-flower.webm?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/big-buck-bunny-720p-10s.mp4" \ - "real_big_buck_bunny_720p_mp4_to_jpeg" "online" "video" "mp4" "thumbnail_to_jpeg" \ - "${SERVER_URL}/attachments/bench/online/big-buck-bunny-720p-10s.mp4?format=jpeg" "200" "image/jpeg" "jpg" - run_if_exists "attachments/bench/online/big-buck-bunny-medium.ogv" \ - "real_big_buck_bunny_ogv_to_webp" "online-full" "video" "ogv" "thumbnail_to_webp" \ - "${SERVER_URL}/attachments/bench/online/big-buck-bunny-medium.ogv?format=webp" "200" "image/webp" "webp" - run_if_exists "attachments/bench/online/maple-leaf-ragq.ogg" \ - "real_maple_leaf_ogg_full" "online" "audio" "ogg" "full_stream" \ - "${SERVER_URL}/attachments/bench/online/maple-leaf-ragq.ogg" "200" "audio/ogg" "ogg" - run_if_exists "attachments/bench/online/maple-leaf-ragq.ogg" \ - "real_maple_leaf_ogg_range" "online" "audio" "ogg" "range_stream" \ - "${SERVER_URL}/attachments/bench/online/maple-leaf-ragq.ogg" "206" "audio/ogg" "ogg" "Range: bytes=0-65535" - run_if_exists "attachments/bench/online/candles-green-heads-and-skulls.ogg" \ - "real_long_audio_ogg_range" "online-full" "audio" "ogg" "range_stream" \ - "${SERVER_URL}/attachments/bench/online/candles-green-heads-and-skulls.ogg" "206" "audio/ogg" "ogg" "Range: bytes=0-1048575" - - if [[ "${BENCH_EXTERNAL}" == "1" && "${BENCH_ONLINE}" == "1" ]]; then - if external_photo_url="$(external_url "https://picsum.photos/id/10/3840/2160.jpg")"; then - run_case "external_real_photo_jpeg_full" "online" "external" "jpeg" "external_full_stream" \ - "${external_photo_url}" "200" "image/jpeg" "jpg" - run_case "external_real_photo_jpeg_range" "online" "external" "jpeg" "external_range_stream" \ - "${external_photo_url}" "206" "image/jpeg" "jpg" "Range: bytes=0-65535" - else - note "skipping external cases; external URL signer unavailable" - fi - if external_audio_url="$(external_url "https://commons.wikimedia.org/wiki/Special:Redirect/file/Maple_Leaf_RagQ.ogg")"; then - run_case "external_real_audio_ogg_range" "online" "external" "ogg" "external_range_stream" \ - "${external_audio_url}" "206" "audio/ogg" "ogg" "Range: bytes=0-65535" - fi - if external_long_video_url="$(external_url "https://commons.wikimedia.org/wiki/Special:Redirect/file/Big_Buck_Bunny_medium.ogv")"; then - run_case "external_long_video_ogv_range" "online" "external" "ogv" "external_range_stream" \ - "${external_long_video_url}" "206" "video/ogg" "ogv" "Range: bytes=0-1048575" - fi - fi - - run_post_case "upload_image_metadata_png_4k" "synthetic" "metadata" "png" "metadata_upload" \ - "${SERVER_URL}/_metadata" '{"version":2,"type":"upload","upload_filename":"upload-image.png","filename":"upload-image.png","nsfw":"allow"}' "200" "application/json" "json" - run_post_case "upload_video_thumbnail_mp4_1080p" "synthetic" "thumbnail" "mp4" "thumbnail_upload" \ - "${SERVER_URL}/_thumbnail" '{"upload_filename":"upload-video.mp4"}' "200" "image/webp" "webp" - run_post_case "upload_video_frames_mp4_1080p" "synthetic" "frames" "mp4" "frames_upload" \ - "${SERVER_URL}/_frames" '{"version":2,"type":"upload","upload_filename":"upload-video.mp4","nsfw":"allow"}' "200" "application/json" "json" - - run_parallel_case "parallel_png_4k_to_webp_512" "synthetic" "image" "png" "resize_to_webp_512" \ - "${SERVER_URL}/attachments/bench/images/synthetic-4k.png?width=512&format=webp&quality=high" "200" "image/webp" "webp" - run_parallel_case "parallel_mp4_1080p_to_jpeg" "synthetic" "video" "mp4" "thumbnail_to_jpeg" \ - "${SERVER_URL}/attachments/bench/videos/synthetic-1080p.mp4?format=jpeg" "200" "image/jpeg" "jpg" -} - -print_summary() { - printf '\nSerial latency summary (ms):\n' - printf "%-46s %8s %10s %10s %10s\n" "case" "n" "min" "avg" "max" - awk -F, ' - NR > 1 { - case_name = $1 - value = $8 + 0 - count[case_name]++ - sum[case_name] += value - if (!(case_name in min) || value < min[case_name]) min[case_name] = value - if (!(case_name in max) || value > max[case_name]) max[case_name] = value - validation[case_name] = validation[case_name] "," $12 - } - END { - for (case_name in count) { - printf "%-46s %8d %10.3f %10.3f %10.3f\n", case_name, count[case_name], min[case_name], sum[case_name] / count[case_name], max[case_name] - } - } - ' "${CSV}" | sort - - if [[ "${BENCH_PARALLEL}" == "1" ]]; then - printf '\nParallel throughput summary:\n' - awk -F, ' - NR > 1 { - printf "%-36s requests=%s concurrency=%s successes=%s total_ms=%s rps=%s avg_latency_ms=%s validation=%s\n", $1, $6, $7, $8, $9, $10, $11, $13 - } - ' "${PARALLEL_CSV}" - fi - printf '\nWrote results to %s\n' "${RUN_DIR}" -} - -write_system_info -generate_fixtures -start_proxy -run_benchmarks -print_summary diff --git a/fluxer_media_proxy/src/bin/s3_fixture.rs b/fluxer_media_proxy/src/bin/s3_fixture.rs deleted file mode 100644 index e6275b6f5..000000000 --- a/fluxer_media_proxy/src/bin/s3_fixture.rs +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use fluxer_media_proxy::{config::Config, storage::Store}; -use std::{env, path::PathBuf}; - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> anyhow::Result<()> { - let cfg = Config::load_from_env()?; - let store = Store::new(cfg); - let image_path = required_path("S3_FIXTURE_IMAGE")?; - let video_path = required_path("S3_FIXTURE_VIDEO")?; - let image = tokio::fs::read(&image_path).await?; - let video = tokio::fs::read(&video_path).await?; - - store.ensure_bucket("cdn").await?; - store.ensure_bucket("uploads").await?; - store.ensure_bucket("static").await?; - - store - .write_object( - "cdn", - "themes/test.css", - b"body{color:#123456}\n", - "text/css; charset=utf-8", - ) - .await?; - store - .write_object( - "cdn", - "attachments/1/2/file.txt", - b"0123456789abcdef", - "text/plain", - ) - .await?; - store - .write_object("cdn", "attachments/1/3/image.png", &image, "image/png") - .await?; - store - .write_object("cdn", "attachments/1/4/video.mp4", &video, "video/mp4") - .await?; - store - .write_object("cdn", "avatars/42/abc.png", &image, "image/png") - .await?; - store - .write_object("cdn", "metadata/image.png", &image, "image/png") - .await?; - store - .write_object("uploads", "upload-image.png", &image, "image/png") - .await?; - store - .write_object("uploads", "upload-video.mp4", &video, "video/mp4") - .await?; - tracing::info!("seeded S3 media proxy fixtures"); - Ok(()) -} - -fn required_path(name: &str) -> anyhow::Result { - env::var_os(name) - .map(PathBuf::from) - .ok_or_else(|| anyhow::anyhow!("{name} is required")) -} diff --git a/fluxer_svc/src/postgres.rs b/fluxer_svc/src/postgres.rs index be3415ccb..391f7410c 100644 --- a/fluxer_svc/src/postgres.rs +++ b/fluxer_svc/src/postgres.rs @@ -477,9 +477,6 @@ fn decode_value(value: Value, date_mode: DecodeDateMode) -> anyhow::Result anyhow::Result<()> { - let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let kv_table = format!("fluxer_kv_rust_test_{}_{}", std::process::id(), nanos); - let host = env::var("FLUXER_POSTGRES_TEST_HOST").unwrap_or_else(|_| { - if Path::new("/.dockerenv").exists() { - "host.docker.internal".to_owned() - } else { - "127.0.0.1".to_owned() - } - }); - let port = env::var("FLUXER_POSTGRES_PORT") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(5432); - let config = PostgresConfig { - url: env::var("FLUXER_POSTGRES_URL").ok(), - host, - port, - database: env::var("FLUXER_POSTGRES_DATABASE").unwrap_or_else(|_| "fluxer".to_owned()), - username: env::var("FLUXER_POSTGRES_USERNAME").unwrap_or_else(|_| "fluxer".to_owned()), - password: env::var("FLUXER_POSTGRES_PASSWORD") - .ok() - .or_else(|| Some("fluxer".to_owned())), - ssl: false, - ssl_ca: None, - max_connections: 2, - kv_table: kv_table.clone(), - }; - - let pool = connect(&config).await?; - let kv = KvClient::new(pool.clone(), &kv_table)?; - let client = pool.get().await?; - let table = quote_identifier(&kv_table)?; - let user_key = kv_key(&[KeyPart::BigInt(42)])?; - let message_partition = kv_key(&[KeyPart::BigInt(42)])?; - let message_key_1 = kv_key(&[KeyPart::BigInt(42), KeyPart::Number(1)])?; - let message_key_2 = kv_key(&[KeyPart::BigInt(42), KeyPart::Number(2)])?; - client - .batch_execute(&format!("DROP TABLE IF EXISTS {table}")) - .await?; - client - .batch_execute(&format!( - "CREATE TABLE {table} ( - table_name text NOT NULL, - partition_key text NOT NULL, - row_key text NOT NULL, - row_data jsonb NOT NULL, - expires_at timestamptz, - updated_at timestamptz NOT NULL DEFAULT now(), - PRIMARY KEY (table_name, row_key) - )" - )) - .await?; - let insert = format!( - "INSERT INTO {table} (table_name, partition_key, row_key, row_data, expires_at, updated_at) VALUES ($1, $2, $3, $4::jsonb, NULL, now())" - ); - - client - .execute( - &insert, - &[ - &"rust_users", - &user_key, - &user_key, - &json!({"user_id": {"__fluxer_type": "bigint", "value": "42"}, "username": "ada"}), - ], - ) - .await?; - client - .execute( - &insert, - &[ - &"rust_messages", - &message_partition, - &message_key_1, - &json!({"message_id": {"__fluxer_type": "bigint", "value": "1"}, "body": "first"}), - ], - ) - .await?; - client - .execute( - &insert, - &[ - &"rust_messages", - &message_partition, - &message_key_2, - &json!({"message_id": {"__fluxer_type": "bigint", "value": "2"}, "body": "second"}), - ], - ) - .await?; - - let user = kv.get_row("rust_users", &user_key).await?.unwrap(); - assert_eq!(user["username"], json!("ada")); - - let rows = kv - .get_partition_rows("rust_messages", &message_partition) - .await?; - assert_eq!(2, rows.len()); - - let prefix = format!("{message_partition}\u{001f}"); - let rows = kv.get_row_key_prefix_rows("rust_messages", &prefix).await?; - assert_eq!(2, rows.len()); - - kv.delete_row("rust_users", &user_key).await?; - assert!(kv.get_row("rust_users", &user_key).await?.is_none()); - - client - .batch_execute(&format!("DROP TABLE IF EXISTS {table}")) - .await?; - Ok(()) - } } diff --git a/fluxer_unfurl/src/resolvers/fxtwitter.rs b/fluxer_unfurl/src/resolvers/fxtwitter.rs index 37e4e67d2..5bdb41795 100644 --- a/fluxer_unfurl/src/resolvers/fxtwitter.rs +++ b/fluxer_unfurl/src/resolvers/fxtwitter.rs @@ -1743,65 +1743,4 @@ mod tests { "**[\u{1F4AC}](https://x.com/intent/tweet?in_reply_to=1299530165463199747) 135\\.1K\u{2002}[\u{1F501}](https://x.com/intent/retweet?tweet_id=1299530165463199747) 1\\.76M\u{2002}[\u{2764}\u{FE0F}](https://x.com/intent/like?tweet_id=1299530165463199747) 6\\.59M\u{2002}**" ); } - - #[tokio::test] - #[ignore = "hits the live FxTwitter API"] - async fn live_example_pipeline_matches_reference() { - let client = reqwest::Client::builder() - .user_agent("Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)") - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - let res = http_fetch::fetch_url( - &client, - &format!("{FXTWITTER_API_BASE}/AdolphsonFalkk/status/2050662857763688642"), - API_MAX_BYTES, - API_TIMEOUT, - ) - .await - .expect("fetch fxtwitter api"); - assert_eq!(res.status, 200); - let tweet = serde_json::from_slice::(&res.bytes) - .expect("parse api json") - .tweet - .expect("tweet present"); - - assert_eq!( - tweet.author.as_ref().and_then(|a| a.screen_name.as_deref()), - Some("AdolphsonFalkk") - ); - assert_eq!( - tweet - .created_timestamp - .and_then(format_timestamp) - .as_deref(), - Some("2026-05-02T19:44:59Z") - ); - - let media = select_media_source(&tweet).expect("media source"); - assert!( - media - .photos - .as_ref() - .is_some_and(|photos| !photos.is_empty()) - ); - - let description = build_description(&tweet); - assert!( - description.starts_with( - "No that's fine I just imagined it would be a bit bigger\n\n> **[Quoting](https://x.com/ScandinavianAE/status/2050646038420045877) Scandinavian Aesthetics \\([@ScandinavianAE](https://x.com/ScandinavianAE)\\)**\n> \u{FE00}\n> Sweden \u{1F1F8}\u{1F1EA}\n\n**[\u{1F4AC}](https://x.com/intent/tweet?in_reply_to=2050662857763688642) " - ), - "description was: {description:?}" - ); - assert!( - description.contains( - "[\u{1F501}](https://x.com/intent/retweet?tweet_id=2050662857763688642) " - ) - ); - assert!(description.contains( - "[\u{2764}\u{FE0F}](https://x.com/intent/like?tweet_id=2050662857763688642) " - )); - assert!(description.contains("\u{1F441}\u{FE0F} ")); - assert!(description.ends_with("\u{2002}**")); - } } diff --git a/fluxer_unfurl/src/resolvers/klipy.rs b/fluxer_unfurl/src/resolvers/klipy.rs index 27b6cf3ab..d6055a17c 100644 --- a/fluxer_unfurl/src/resolvers/klipy.rs +++ b/fluxer_unfurl/src/resolvers/klipy.rs @@ -463,66 +463,6 @@ mod tests { assert!(media.thumbnail.is_none()); } - #[tokio::test] - #[ignore = "hits the live KLIPY API and local media proxy"] - async fn live_klipy_embed_resolves_real_media() { - let api_key = std::env::var("FLUXER_KLIPY_API_KEY").expect("FLUXER_KLIPY_API_KEY set"); - let media_proxy_endpoint = - std::env::var("FLUXER_MEDIA_PROXY_ENDPOINT").expect("FLUXER_MEDIA_PROXY_ENDPOINT set"); - let media_proxy_secret = std::env::var("FLUXER_MEDIA_PROXY_SECRET_KEY") - .expect("FLUXER_MEDIA_PROXY_SECRET_KEY set"); - let media_proxy_public_endpoint = std::env::var("FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT").ok(); - let raw_url = std::env::var("FLUXER_KLIPY_LIVE_URL") - .unwrap_or_else(|_| "https://klipy.com/gifs/goatplaybanjo-chat-4".to_owned()); - - let resolver = KlipyResolver; - let original_url = Url::parse(&raw_url).expect("valid live KLIPY URL"); - let url = resolver - .transform_url(&original_url) - .unwrap_or_else(|| original_url.clone()); - let media_proxy = crate::media_proxy::MediaProxyClient::new_with_public_endpoint( - &media_proxy_endpoint, - &media_proxy_secret, - media_proxy_public_endpoint.as_deref(), - reqwest::Client::new(), - ); - let ctx = ResolveContext { - url, - original_url: original_url.clone(), - http_client: reqwest::Client::new(), - nsfw_mode: crate::types::NsfwMode::Allow, - media_proxy: &media_proxy, - static_cdn_endpoint: "", - youtube_api_key: None, - klipy_api_key: Some(api_key), - }; - - let result = resolver.resolve(&ctx).await.expect("resolve KLIPY embed"); - assert_eq!(result.embeds.len(), 1); - let embed = &result.embeds[0]; - assert_eq!(embed.embed_type, "gifv"); - assert_eq!(embed.url.as_deref(), Some(original_url.as_str())); - assert_eq!( - embed - .provider - .as_ref() - .and_then(|provider| provider.name.as_deref()), - Some("KLIPY") - ); - let video = embed.video.as_ref().expect("video media resolved"); - assert!( - video - .url - .as_deref() - .is_some_and(|url| url.starts_with("https://")) - ); - assert!(video.width.is_some_and(|width| width > 0)); - assert!(video.height.is_some_and(|height| height > 0)); - assert!(video.content_type.as_deref().is_some_and(|content_type| { - content_type.starts_with("video/") || content_type == "image/gif" - })); - } - #[test] fn resolve_relative_url_rejects_non_http() { let base = Url::parse("https://klipy.com/gifs/test").unwrap(); diff --git a/knip.json b/knip.json index 74fad3bfb..071039e45 100644 --- a/knip.json +++ b/knip.json @@ -35,9 +35,6 @@ ".": { "project": ["scripts/**/*.mjs"] }, - "fluxer_api": { - "entry": ["src/api/test/fake-ncmec/Bin.ts"] - }, "fluxer_app": { "entry": ["lingui.config.js", "scripts/build-sw.mjs", "scripts/Generate*.ts"], "ignoreDependencies": ["@lingui/swc-plugin"] @@ -52,7 +49,6 @@ "native/*/loader-diagnostics.cjs", "native/*/*.test.mjs", "native/*/test/**/*.mjs", - "native/*/vm_tests/*.mjs", "native/*/scripts/**/*.mjs" ], "project": ["src/**/*.{ts,tsx,mjs,js}", "scripts/**/*.mjs", "native/**/*.{js,mjs,cjs,d.ts}"], diff --git a/package.json b/package.json index fef85f8a2..9cb03f7f2 100644 --- a/package.json +++ b/package.json @@ -13,24 +13,18 @@ "dev:desktop:canary": "cargo run -p fluxer-dev -- desktop canary", "dev:desktop:package": "cargo run -p fluxer-dev -- desktop package", "dev:desktop:run": "cargo run -p fluxer-dev -- desktop run", - "dev:desktop:smoke": "cargo run -p fluxer-dev -- desktop smoke-build", "dev:infra:down": "docker compose -f .devcontainer/docker-compose.yml down", "dev:infra:restart": "pnpm dev:infra:down && pnpm dev:infra:up", "dev:infra:up": "pnpm dev:infra:up:lite", "dev:infra:up:full": "docker compose -f .devcontainer/docker-compose.yml up -d postgres cassandra valkey nats livekit elasticsearch meilisearch mailpit", "dev:infra:up:lite": "docker compose -f .devcontainer/docker-compose.yml up -d postgres valkey nats livekit meilisearch mailpit", - "dev:media-proxy:seaweedfs": "cargo run -p fluxer-dev -- media-proxy seaweedfs-integration", - "dev:native-voice-it": "cargo run -p fluxer-dev -- native-voice-it", "dev:services": "cargo run -p fluxer-dev -- rust-services", - "dev:smoke": "cargo run -p fluxer-dev -- smoke", - "dev:smoke:public": "cargo run -p fluxer-dev -- smoke --public", "dev:tunnel": "cargo run -p fluxer-dev -- dev --cloudflare-tunnel", "dev:tunnel:configure": "cargo run -p fluxer-dev -- tunnel configure", "dev:tunnel:env": "cargo run -p fluxer-dev -- tunnel print-env", "dev:tunnel:run": "cargo run -p fluxer-dev -- tunnel run", "docs:build": "bash fluxer_docs/serve.sh build --clean", "docs:dev": "bash fluxer_docs/serve.sh", - "fake-ncmec": "pnpm tsx fluxer_api/src/api/test/fake-ncmec/Bin.ts", "i18n:compile": "pnpm --filter fluxer_app i18n:compile && pnpm --filter @fluxer/i18n static:compile && pnpm --filter @fluxer/i18n generate:types", "i18n:refresh": "pnpm --filter fluxer_app i18n:refresh", "i18n:generate": "pnpm --filter @fluxer/i18n generate:types", @@ -44,7 +38,6 @@ "openapi:validate": "pnpm --filter @fluxer/openapi validate", "remote": "node scripts/remote.mjs", "test": "cargo run -p fluxer-dev -- test", - "test:integration": "pnpm --filter @fluxer/integration test", "typecheck": "cargo run -p fluxer-dev -- typecheck" }, "prettier": { @@ -57,8 +50,7 @@ "devDependencies": { "@biomejs/biome": "catalog:", "@vitest/coverage-v8": "catalog:", - "knip": "catalog:", - "tsx": "catalog:" + "knip": "catalog:" }, "packageManager": "pnpm@10.29.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 186cc533d..166db4f94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -395,9 +395,6 @@ importers: knip: specifier: 'catalog:' version: 5.85.0(@types/node@25.3.0)(typescript@5.9.3) - tsx: - specifier: 'catalog:' - version: 4.21.0 fluxer_admin: devDependencies: diff --git a/scripts/local-k8s/Dockerfile.handoff-rollout-it b/scripts/local-k8s/Dockerfile.handoff-rollout-it deleted file mode 100644 index fa2419629..000000000 --- a/scripts/local-k8s/Dockerfile.handoff-rollout-it +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -FROM fluxer-dev-workspace:latest - -ARG HANDOFF_IMAGE_VERSION=v1 -ENV HANDOFF_IMAGE_VERSION=${HANDOFF_IMAGE_VERSION} - -USER vscode -WORKDIR /workspaces/fluxer/fluxer_gateway - -COPY --chown=vscode:vscode _build/test/lib/fluxer_gateway/ebin /workspaces/fluxer/fluxer_gateway/_build/test/lib/fluxer_gateway/ebin diff --git a/scripts/local-k8s/Dockerfile.hotpatch-it b/scripts/local-k8s/Dockerfile.hotpatch-it deleted file mode 100644 index 8ff258a0d..000000000 --- a/scripts/local-k8s/Dockerfile.hotpatch-it +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -FROM fluxer-dev-workspace:latest - -USER vscode -WORKDIR /workspaces/fluxer - -COPY --chown=vscode:vscode fluxer_gateway/rebar.config fluxer_gateway/rebar.lock /workspaces/fluxer/fluxer_gateway/ - -ENV GIT_CONFIG_GLOBAL=/dev/null - -RUN cd fluxer_gateway && rebar3 as test compile --deps_only - -COPY --chown=vscode:vscode . /workspaces/fluxer - -RUN cd fluxer_gateway && \ - rebar3 as test compile && \ - erlc \ - -I include \ - -o _build/test/lib/fluxer_gateway/ebin \ - test/gateway_hotpatch_k8s_smoke.erl - -WORKDIR /workspaces/fluxer/fluxer_gateway diff --git a/scripts/remote.mjs b/scripts/remote.mjs index ac7993c6d..2b3d9629c 100755 --- a/scripts/remote.mjs +++ b/scripts/remote.mjs @@ -19,7 +19,6 @@ function usage(exitCode = 0) { pnpm remote pull [--branch ] pnpm remote run -- pnpm remote apply-diff [-- ...] - pnpm remote test-webrtc pnpm remote tunnel start|status|stop pnpm remote macos-setup [--pubkey ~/.ssh/id_ed25519.pub] @@ -262,18 +261,6 @@ function pullCommand(host, branch) { ].join(' && '); } -function testWebrtcCommand(host) { - const commands = [ - 'corepack enable', - 'pnpm install', - 'pnpm --dir fluxer_desktop/native/webrtc-sender test', - 'pnpm --dir fluxer_desktop typecheck', - 'pnpm --dir fluxer_desktop build', - `node -e "const m=require('./fluxer_desktop/node_modules/@fluxer/webrtc-sender'); console.log(JSON.stringify({supported:m.isSupported?.(), hasVoiceEngine:typeof m.VoiceEngine==='function', loadError:m.loadError?String(m.loadError):null}))"`, - ]; - return host.platform === 'windows' ? commands.join('; ') : commands.join(' && '); -} - function macosSetup(args) { const pubkeyPath = path.resolve(expandHome(optionValue(args, '--pubkey', '~/.ssh/id_ed25519.pub'))); const pubkey = existsSync(pubkeyPath) ? readFileSync(pubkeyPath, 'utf8').trim() : ''; @@ -351,12 +338,6 @@ switch (command) { sshWithInput(name, host, 'git apply --whitespace=nowarn -', diff, options); break; } - case 'test-webrtc': { - const name = args[1] ?? fail('test-webrtc needs a host'); - const host = getHost(config, name); - ssh(name, host, testWebrtcCommand(host), options); - break; - } case 'tunnel': { const action = args[1] ?? fail('tunnel needs start|status|stop'); const name = args[2] ?? fail('tunnel needs a host'); diff --git a/tools/dev/Cargo.toml b/tools/dev/Cargo.toml index 2335093fd..0536c60a8 100644 --- a/tools/dev/Cargo.toml +++ b/tools/dev/Cargo.toml @@ -8,24 +8,20 @@ license.workspace = true anyhow = "1.0.102" axum = "0.8.9" base64 = "0.22.1" -chrono = "0.4.45" clap = { version = "4.6.1", features = ["derive", "env"] } -futures-util = "0.3.32" hmac = "0.13.0" hyper = { version = "1.10.1", features = ["http1", "server"] } hyper-util = { version = "0.1.20", features = ["tokio"] } image = { version = "0.25.10", default-features = false, features = ["gif", "jpeg", "png", "webp"] } libc = "0.2.186" regex = "1.12" -reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls", "stream"] } +reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "stream"] } scylla = { version = "1.6.0", features = ["chrono-04"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" sha2 = "0.11.0" -tempfile = "3.27.0" tokio = { version = "1.52.3", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "time"] } -tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] } url = "2.5.8" -urlencoding = "2.1.3" [dev-dependencies] +tempfile = "3.27.0" diff --git a/tools/dev/src/bootstrap.rs b/tools/dev/src/bootstrap.rs index 29a447ce9..41879683c 100644 --- a/tools/dev/src/bootstrap.rs +++ b/tools/dev/src/bootstrap.rs @@ -2,9 +2,9 @@ use crate::desktop::install_desktop; use crate::gateway::setup_gateway_config; +use crate::object_store::{bootstrap_schema_and_object_store, wait_s3_api}; use crate::paths::{ensure_state_dirs, ensure_writable_dev_paths}; use crate::proc::{PNPM_INSTALL_ENV, RunOptions, run_command, wait_http, wait_tcp}; -use crate::smoke::{bootstrap_schema_and_object_store, run_smoke, wait_s3_api}; use anyhow::Result; pub async fn bootstrap(skip_install: bool, skip_desktop_install: bool) -> Result<()> { @@ -30,7 +30,6 @@ pub async fn bootstrap(skip_install: bool, skip_desktop_install: bool) -> Result setup_gateway_config()?; wait_core_infra().await?; bootstrap_schema_and_object_store().await?; - run_smoke(false, false).await?; println!("Fluxer dev bootstrap complete."); Ok(()) } @@ -40,7 +39,7 @@ pub async fn post_start() -> Result<()> { ensure_writable_dev_paths()?; setup_gateway_config()?; crate::media_proxy::ensure_dev_object_store(true, 120).await?; - run_smoke(true, false).await + Ok(()) } pub async fn wait_core_infra() -> Result<()> { diff --git a/tools/dev/src/desktop.rs b/tools/dev/src/desktop.rs index 1ca85c2f4..2cd17113b 100644 --- a/tools/dev/src/desktop.rs +++ b/tools/dev/src/desktop.rs @@ -109,18 +109,9 @@ pub fn electron_args(args: &[String]) -> Vec { runtime_args } -pub fn electron_command(args: &[String], headless: bool) -> Vec { +pub fn electron_command(args: &[String]) -> Vec { let mut command = base_electron_command(); command.extend(electron_args(args)); - if headless - && cfg!(target_os = "linux") - && env::var_os("DISPLAY").is_none() - && crate::paths::which("xvfb-run").is_some() - { - let mut wrapped = vec!["xvfb-run".to_owned(), "-a".to_owned()]; - wrapped.extend(command); - return wrapped; - } command } @@ -151,28 +142,6 @@ fn disclaimed_electron_command(launcher: &Path, electron_binary: &Path) -> Vec Result<()> { - install_desktop()?; - build_desktop(true)?; - let command = electron_command( - &[ - "--fluxer-debug-info".to_owned(), - format!("--fluxer-app-url={LOCAL_APP_URL}"), - ], - true, - ); - let args: Vec<_> = command.iter().map(String::as_str).collect(); - run_command( - &args, - RunOptions { - cwd: DESKTOP_DIR.as_path(), - env: vec![("FLUXER_SKIP_NATIVE".to_owned(), Some("true".to_owned()))], - ..RunOptions::default() - }, - ) - .map(drop) -} - fn packages_for_windows(args: &[String]) -> bool { if args.iter().any(|arg| arg == "--win") { return true; @@ -261,7 +230,7 @@ fn run_desktop_process(app_url: &str, extra_args: &[String]) -> Result<()> { "--fluxer-log-renderer-console".to_owned(), ]; args.extend(extra_args.iter().cloned()); - let command = electron_command(&args, false); + let command = electron_command(&args); let refs: Vec<_> = command.iter().map(String::as_str).collect(); run_command( &refs, diff --git a/tools/dev/src/dev.rs b/tools/dev/src/dev.rs index d3b2eea40..fdc4b74e2 100644 --- a/tools/dev/src/dev.rs +++ b/tools/dev/src/dev.rs @@ -5,12 +5,12 @@ use crate::manifest::{ ADMIN_PORT, ANY_HOST, API_PORT, APP_PORT, APP_PROXY_PORT, DEV_PROXY_PORT, LOOPBACK_HOST, MEDIA_PROXY_PORT, rust_services, }; +use crate::object_store::s3_endpoint; use crate::paths::{DESKTOP_DIR, ROOT}; use crate::proc::{ PNPM_INSTALL_ENV, RESTART_LIMIT, RESTART_WINDOW, RunOptions, ShutdownSignal, format_command, merged_env, restart_budget_exceeded, run_command, wait_http, }; -use crate::smoke::s3_endpoint; use anyhow::{Result, bail}; use std::collections::{BTreeMap, VecDeque}; use std::path::{Path, PathBuf}; diff --git a/tools/dev/src/lib.rs b/tools/dev/src/lib.rs index f51cd8b3a..6457dad32 100644 --- a/tools/dev/src/lib.rs +++ b/tools/dev/src/lib.rs @@ -8,16 +8,14 @@ pub mod disclaim; pub mod env; pub mod gateway; pub mod gateway_reload; -pub mod local_k8s; pub mod manifest; pub mod marketing; +pub mod media_external; pub mod media_proxy; -pub mod media_stress; -pub mod native_voice_it; +pub mod object_store; pub mod paths; pub mod proc; pub mod proxy; pub mod rust_services; -pub mod smoke; pub mod tasks; pub mod tunnel; diff --git a/tools/dev/src/local_k8s.rs b/tools/dev/src/local_k8s.rs deleted file mode 100644 index a1fb4c69f..000000000 --- a/tools/dev/src/local_k8s.rs +++ /dev/null @@ -1,2153 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::paths::{ROOT, which}; -use crate::proc::{format_command, merged_env}; -use anyhow::{Context, Result, anyhow, bail}; -use chrono::Utc; -use serde_json::{Value, json}; -use std::env; -use std::ffi::OsString; -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::{Command, ExitStatus, Output, Stdio}; -use std::time::{Duration, Instant}; -use tempfile::TempDir; -use tokio::time::sleep; - -const CLUSTER_NAME: &str = "fluxer-hotpatch-it"; -const CONTEXT: &str = "kind-fluxer-hotpatch-it"; -const NAMESPACE: &str = "fluxer-it"; -const API_PORT: u16 = 36443; -const DOCKER_HOST: &str = "unix:///var/run/docker.sock"; -const REAL_DOCKER: &str = "/usr/bin/docker"; - -pub async fn create_cluster() -> Result<()> { - require_devcontainer()?; - require_tool("docker")?; - require_tool("kind")?; - require_tool("kubectl")?; - prepare_state_dir()?; - refuse_foreign_kubeconfig()?; - - let docker_wrapper = make_docker_wrapper()?; - let command_env = k8s_env(Some(docker_wrapper.path())); - run_inherit( - strings(&["docker", "version"]), - &command_env, - ROOT.as_path(), - )?; - - let kind_config = local_k8s_dir().join("kind-config.yaml"); - fs::write(&kind_config, render_kind_config(API_PORT)) - .with_context(|| format!("failed to write {}", kind_config.display()))?; - - let clusters = run_quiet( - strings(&["kind", "get", "clusters"]), - &command_env, - ROOT.as_path(), - false, - )?; - if has_exact_line(&clusters.stdout_text(), CLUSTER_NAME) { - run_inherit( - vec![ - "kind".to_owned(), - "export".to_owned(), - "kubeconfig".to_owned(), - "--name".to_owned(), - CLUSTER_NAME.to_owned(), - "--kubeconfig".to_owned(), - kubeconfig().display().to_string(), - ], - &command_env, - ROOT.as_path(), - )?; - } else { - run_inherit( - vec![ - "kind".to_owned(), - "create".to_owned(), - "cluster".to_owned(), - "--name".to_owned(), - CLUSTER_NAME.to_owned(), - "--config".to_owned(), - kind_config.display().to_string(), - "--kubeconfig".to_owned(), - kubeconfig().display().to_string(), - ], - &command_env, - ROOT.as_path(), - )?; - } - - run_inherit( - kubectl_config_args(&[ - "config", - "set-cluster", - CONTEXT, - "--server", - &format!("https://host.docker.internal:{API_PORT}"), - ]), - &command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_config_args(&["config", "set-context", CONTEXT, "--namespace", NAMESPACE]), - &command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_config_args(&["config", "use-context", CONTEXT]), - &command_env, - ROOT.as_path(), - )?; - chmod(&kubeconfig(), 0o600)?; - assert_safe_context()?; - - let mut ready = false; - for _ in 0..90 { - let result = run_quiet( - kubectl_args(&["get", "--raw=/readyz?verbose"]), - &command_env, - ROOT.as_path(), - false, - )?; - if result.status.success() { - ready = true; - break; - } - sleep(Duration::from_secs(2)).await; - } - if !ready { - bail!("local-k8s: kind API server did not become ready"); - } - - let namespace = run_quiet( - kubectl_args(&["get", "namespace", NAMESPACE]), - &command_env, - ROOT.as_path(), - false, - )?; - if !namespace.status.success() { - run_inherit( - kubectl_args(&["create", "namespace", NAMESPACE]), - &command_env, - ROOT.as_path(), - )?; - } - - println!( - "local kind cluster ready: context={CONTEXT} kubeconfig={} namespace={NAMESPACE}", - kubeconfig().display() - ); - Ok(()) -} - -pub fn run_kubectl_cli(args: &[String]) -> Result<()> { - require_devcontainer()?; - require_tool("kubectl")?; - assert_safe_context()?; - run_inherit(kubectl_args_owned(args), &k8s_env(None), ROOT.as_path()) -} - -pub fn run_helm_cli(args: &[String]) -> Result<()> { - require_devcontainer()?; - require_tool("helm")?; - assert_safe_context()?; - let mut command = strings(&[ - "helm", - "--kubeconfig", - &kubeconfig().display().to_string(), - "--kube-context", - CONTEXT, - ]); - command.extend(args.iter().cloned()); - run_inherit(command, &k8s_env(None), ROOT.as_path()) -} - -pub async fn run_hotpatch_smoke() -> Result<()> { - require_devcontainer()?; - require_tool("docker")?; - require_tool("kind")?; - require_tool("kubectl")?; - require_tool("erl")?; - assert_safe_context()?; - - let cfg = HotpatchConfig::from_env(); - let docker_wrapper = make_docker_wrapper()?; - let command_env = k8s_env(Some(docker_wrapper.path())); - - build_hotpatch_runner_image(&cfg, &command_env)?; - deploy_cassandra(&command_env)?; - let cassandra = wait_for_cql(&command_env).await?; - apply_schema(&cassandra, &command_env)?; - let (public_key, private_key) = generate_signing_key(&command_env)?; - create_signing_secret(&public_key, &private_key, &command_env)?; - - let producer_total = cfg.producer_event_count + cfg.producer_post_start_event_count; - run_hotpatch_job( - "hotpatch-producer", - cfg.producer_event_count, - cfg.producer_post_start_event_count, - cfg.producer_event_count, - 1, - 1, - &cfg, - &command_env, - )?; - assert_job_audits( - &cassandra, - "hotpatch-producer", - producer_total, - &cfg, - &command_env, - )?; - - run_hotpatch_job( - "hotpatch-resume", - 0, - 0, - producer_total, - 1, - 1, - &cfg, - &command_env, - )?; - assert_job_audits( - &cassandra, - "hotpatch-resume", - producer_total, - &cfg, - &command_env, - )?; - - run_hotpatch_job( - "hotpatch-parallel", - cfg.parallel_event_count, - cfg.parallel_post_start_event_count, - producer_total, - cfg.parallel_completions, - cfg.parallel_completions, - &cfg, - &command_env, - )?; - let parallel_min = producer_total + cfg.parallel_post_start_event_count; - assert_job_audits( - &cassandra, - "hotpatch-parallel", - parallel_min, - &cfg, - &command_env, - )?; - - let expected_total = cfg.expected_event_total(); - run_hotpatch_job( - "hotpatch-final-resume", - 0, - 0, - expected_total, - 1, - 1, - &cfg, - &command_env, - )?; - assert_job_audits( - &cassandra, - "hotpatch-final-resume", - expected_total, - &cfg, - &command_env, - )?; - run_hotpatch_job( - "hotpatch-cluster-verify", - 0, - 0, - expected_total, - cfg.verify_completions, - cfg.verify_completions, - &cfg, - &command_env, - )?; - assert_job_audits( - &cassandra, - "hotpatch-cluster-verify", - expected_total, - &cfg, - &command_env, - )?; - assert_event_count(&cassandra, expected_total, &cfg, &command_env)?; - - if cfg.cleanup_after_run { - cleanup_hotpatch_run(&command_env)?; - } - - println!( - "hotpatch local Kubernetes smoke passed: build_sha={} expected_events={} verify_completions={}", - cfg.build_sha, expected_total, cfg.verify_completions - ); - Ok(()) -} - -pub async fn run_handoff_rollout_smoke() -> Result<()> { - require_devcontainer()?; - require_tool("docker")?; - require_tool("kind")?; - require_tool("kubectl")?; - require_tool("erl")?; - require_tool("erlc")?; - require_tool("rebar3")?; - assert_safe_context()?; - - let cfg = HandoffConfig::from_env(); - let docker_wrapper = make_docker_wrapper()?; - let command_env = k8s_env(Some(docker_wrapper.path())); - let build_context = tempfile::Builder::new() - .prefix("fluxer-handoff-rollout-context.") - .tempdir()?; - - build_handoff_images(&cfg, build_context.path(), &command_env)?; - apply_values(handoff_controller_rbac(), &command_env)?; - - for cluster in cfg.clusters.split_whitespace() { - validate_cluster_name(cluster)?; - println!( - "handoff rollout smoke starting: cluster={cluster} replicas={} entities={}", - cfg.replicas, cfg.entity_count - ); - delete_previous_handoff_cluster_run(cluster, &command_env)?; - apply_values(handoff_statefulset(cluster, &cfg), &command_env)?; - run_inherit( - kubectl_args(&[ - "rollout", - "status", - &format!("statefulset/{}", statefulset_name(cluster)), - "--timeout=240s", - ]), - &command_env, - ROOT.as_path(), - )?; - run_handoff_controller_job(cluster, &cfg, &command_env).await?; - if cfg.cleanup_after_cluster { - delete_previous_handoff_cluster_run(cluster, &command_env)?; - } - } - - println!( - "handoff rollout local Kubernetes smoke passed: clusters=\"{}\" image_v1={} image_v2={}", - cfg.clusters, cfg.image_v1, cfg.image_v2 - ); - Ok(()) -} - -pub fn run_docker_wrapper(args: I) -> i32 -where - I: IntoIterator, -{ - let args: Vec = args.into_iter().collect(); - let output_file = docker_save_output_file(&args); - let direct_ok = Command::new(REAL_DOCKER) - .arg("version") - .env("DOCKER_HOST", DOCKER_HOST) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|status| status.success()) - .unwrap_or(false); - - let status = if direct_ok { - run_real_docker(&args) - } else { - run_sudo_docker(&args) - }; - let code = status_to_code(status); - - if code == 0 - && let Some(path) = output_file - && path.exists() - { - let _ = chown_current_user(&path); - } - code -} - -fn local_k8s_dir() -> PathBuf { - ROOT.join(".fluxer/k8s") -} - -fn kubeconfig() -> PathBuf { - local_k8s_dir().join("local-kubeconfig") -} - -fn require_devcontainer() -> Result<()> { - if !ROOT.starts_with("/workspaces") || !Path::new("/.dockerenv").is_file() { - bail!("local-k8s: run this from inside the Fluxer devcontainer"); - } - Ok(()) -} - -fn require_tool(name: &str) -> Result<()> { - if which(name).is_none() { - bail!("local-k8s: missing required tool: {name}"); - } - Ok(()) -} - -fn prepare_state_dir() -> Result<()> { - fs::create_dir_all(local_k8s_dir())?; - chmod(&local_k8s_dir(), 0o700) -} - -fn refuse_foreign_kubeconfig() -> Result<()> { - if !is_non_empty_file(&kubeconfig()) { - return Ok(()); - } - let command_env = k8s_env(None); - let contexts = run_quiet( - kubectl_config_args(&["config", "get-contexts", "-o", "name"]), - &command_env, - ROOT.as_path(), - false, - )? - .stdout_text(); - let current_context = run_quiet( - kubectl_config_args(&["config", "current-context"]), - &command_env, - ROOT.as_path(), - false, - )? - .stdout_text() - .trim() - .to_owned(); - - let foreign_contexts: Vec<_> = contexts - .lines() - .filter(|line| !line.trim().is_empty()) - .filter(|line| *line != CONTEXT) - .collect(); - if !foreign_contexts.is_empty() { - bail!( - "local-k8s: {} contains non-local Kubernetes contexts", - kubeconfig().display() - ); - } - if !current_context.is_empty() && current_context != CONTEXT { - bail!( - "local-k8s: {} current context is {current_context}, expected {CONTEXT}", - kubeconfig().display() - ); - } - Ok(()) -} - -fn assert_safe_context() -> Result<()> { - if !is_non_empty_file(&kubeconfig()) { - bail!( - "local-k8s: missing {}; run `fluxer-dev local-k8s create-cluster` first", - kubeconfig().display() - ); - } - let command_env = k8s_env(None); - let current_context = run_quiet( - kubectl_config_args(&["config", "current-context"]), - &command_env, - ROOT.as_path(), - false, - )? - .stdout_text() - .trim() - .to_owned(); - if current_context != CONTEXT { - bail!( - "local-k8s: refusing Kubernetes access with context {}; expected {CONTEXT}", - if current_context.is_empty() { - "" - } else { - ¤t_context - } - ); - } - - let server = run_quiet( - kubectl_args(&[ - "config", - "view", - "--raw", - "--minify", - "-o", - "jsonpath={.clusters[0].cluster.server}", - ]), - &command_env, - ROOT.as_path(), - false, - )? - .stdout_text() - .trim() - .to_owned(); - if !is_safe_api_server(&server) { - bail!( - "local-k8s: refusing Kubernetes API server {}; expected a local kind endpoint", - if server.is_empty() { "" } else { &server } - ); - } - Ok(()) -} - -fn is_non_empty_file(path: &Path) -> bool { - fs::metadata(path) - .map(|metadata| metadata.is_file() && metadata.len() > 0) - .unwrap_or(false) -} - -fn is_safe_api_server(server: &str) -> bool { - [ - "https://host.docker.internal:", - "https://127.0.0.1:", - "https://localhost:", - "https://0.0.0.0:", - ] - .iter() - .any(|prefix| server.starts_with(prefix)) -} - -fn render_kind_config(api_port: u16) -> String { - format!( - r#"kind: Cluster -apiVersion: kind.x-k8s.io/v1alpha4 -networking: - apiServerAddress: "0.0.0.0" - apiServerPort: {api_port} -kubeadmConfigPatches: -- | - kind: ClusterConfiguration - apiServer: - certSANs: - - host.docker.internal - - localhost - - 127.0.0.1 - - 0.0.0.0 -nodes: -- role: control-plane -"# - ) -} - -struct DockerWrapper { - _dir: TempDir, - path: PathBuf, -} - -impl DockerWrapper { - fn path(&self) -> &Path { - &self.path - } -} - -fn make_docker_wrapper() -> Result { - let dir = tempfile::Builder::new() - .prefix("fluxer-local-k8s-docker.") - .tempdir()?; - let target = dir.path().join("docker"); - let current_exe = env::current_exe()?; - link_or_copy_exe(¤t_exe, &target)?; - Ok(DockerWrapper { - path: dir.path().to_path_buf(), - _dir: dir, - }) -} - -#[cfg(unix)] -fn link_or_copy_exe(current_exe: &Path, target: &Path) -> Result<()> { - std::os::unix::fs::symlink(current_exe, target) - .with_context(|| format!("failed to create {}", target.display())) -} - -#[cfg(not(unix))] -fn link_or_copy_exe(current_exe: &Path, target: &Path) -> Result<()> { - fs::copy(current_exe, target) - .with_context(|| format!("failed to copy {}", current_exe.display()))?; - Ok(()) -} - -fn k8s_env(wrapper_path: Option<&Path>) -> Vec<(String, Option)> { - let mut command_env = vec![ - ("DOCKER_HOST".to_owned(), Some(DOCKER_HOST.to_owned())), - ( - "KUBECONFIG".to_owned(), - Some(kubeconfig().display().to_string()), - ), - ]; - if let Some(wrapper_path) = wrapper_path { - command_env.push(("PATH".to_owned(), Some(prepend_path(wrapper_path)))); - } - command_env -} - -fn prepend_path(path: &Path) -> String { - let mut paths = vec![path.to_path_buf()]; - if let Some(current) = env::var_os("PATH") { - paths.extend(env::split_paths(¤t)); - } - env::join_paths(paths) - .unwrap_or_else(|_| path.as_os_str().to_os_string()) - .to_string_lossy() - .into_owned() -} - -fn strings(args: &[&str]) -> Vec { - args.iter().map(|arg| (*arg).to_owned()).collect() -} - -fn kubectl_config_args(args: &[&str]) -> Vec { - let mut command = vec![ - "kubectl".to_owned(), - "--kubeconfig".to_owned(), - kubeconfig().display().to_string(), - ]; - command.extend(args.iter().map(|arg| (*arg).to_owned())); - command -} - -fn kubectl_args(args: &[&str]) -> Vec { - let mut command = vec![ - "kubectl".to_owned(), - "--kubeconfig".to_owned(), - kubeconfig().display().to_string(), - "--context".to_owned(), - CONTEXT.to_owned(), - ]; - command.extend(args.iter().map(|arg| (*arg).to_owned())); - command -} - -fn kubectl_args_owned(args: &[String]) -> Vec { - let mut command = vec![ - "kubectl".to_owned(), - "--kubeconfig".to_owned(), - kubeconfig().display().to_string(), - "--context".to_owned(), - CONTEXT.to_owned(), - ]; - command.extend(args.iter().cloned()); - command -} - -fn run_inherit( - args: Vec, - command_env: &[(String, Option)], - cwd: &Path, -) -> Result<()> { - let status = run_status(args, None, command_env, cwd)?; - if !status.success() { - bail!("Command failed with exit code {}.", status_code(status)); - } - Ok(()) -} - -fn run_with_stdin( - args: Vec, - stdin: &str, - command_env: &[(String, Option)], - cwd: &Path, -) -> Result<()> { - let status = run_status(args, Some(stdin), command_env, cwd)?; - if !status.success() { - bail!("Command failed with exit code {}.", status_code(status)); - } - Ok(()) -} - -fn run_status( - args: Vec, - stdin: Option<&str>, - extra_env: &[(String, Option)], - cwd: &Path, -) -> Result { - println!("$ {}", format_command(&args)); - let env = merged_env(Some(extra_env), true)?; - let mut command = Command::new(&args[0]); - command - .args(&args[1..]) - .current_dir(cwd) - .env_clear() - .envs(env); - if let Some(stdin_text) = stdin { - let mut child = command - .stdin(Stdio::piped()) - .spawn() - .with_context(|| format!("failed to run {}", format_command(&args)))?; - child - .stdin - .as_mut() - .ok_or_else(|| anyhow!("failed to open stdin for {}", format_command(&args)))? - .write_all(stdin_text.as_bytes())?; - child - .wait() - .with_context(|| format!("failed to wait for {}", format_command(&args))) - } else { - command - .status() - .with_context(|| format!("failed to run {}", format_command(&args))) - } -} - -fn run_capture_print( - args: Vec, - command_env: &[(String, Option)], - cwd: &Path, -) -> Result { - let output = run_output(args, command_env, cwd)?; - let text = output.combined_text(); - if !text.trim_end().is_empty() { - println!("{}", text.trim_end()); - } - if !output.status.success() { - bail!( - "Command failed with exit code {}.", - status_code(output.status) - ); - } - Ok(output) -} - -fn run_quiet( - args: Vec, - command_env: &[(String, Option)], - cwd: &Path, - check: bool, -) -> Result { - let output = run_output(args, command_env, cwd)?; - if check && !output.status.success() { - bail!( - "Command failed with exit code {}: {}", - status_code(output.status), - output.combined_text().trim_end() - ); - } - Ok(output) -} - -fn run_output( - args: Vec, - extra_env: &[(String, Option)], - cwd: &Path, -) -> Result { - println!("$ {}", format_command(&args)); - let env = merged_env(Some(extra_env), true)?; - let output = Command::new(&args[0]) - .args(&args[1..]) - .current_dir(cwd) - .env_clear() - .envs(env) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .with_context(|| format!("failed to run {}", format_command(&args)))?; - Ok(CommandOutput::from(output)) -} - -struct CommandOutput { - status: ExitStatus, - stdout: Vec, - stderr: Vec, -} - -impl CommandOutput { - fn from(output: Output) -> Self { - Self { - status: output.status, - stdout: output.stdout, - stderr: output.stderr, - } - } - - fn stdout_text(&self) -> String { - String::from_utf8_lossy(&self.stdout).into_owned() - } - - fn combined_text(&self) -> String { - let mut bytes = self.stdout.clone(); - bytes.extend_from_slice(&self.stderr); - String::from_utf8_lossy(&bytes).into_owned() - } -} - -fn status_code(status: ExitStatus) -> i32 { - status.code().unwrap_or(1) -} - -fn has_exact_line(text: &str, expected: &str) -> bool { - text.lines().any(|line| line == expected) -} - -fn apply_values(values: Vec, command_env: &[(String, Option)]) -> Result<()> { - let payload = if values.len() == 1 { - values.into_iter().next().expect("checked len") - } else { - json!({ - "apiVersion": "v1", - "kind": "List", - "items": values, - }) - }; - let text = serde_json::to_string_pretty(&payload)?; - run_with_stdin( - kubectl_args(&["apply", "-f", "-"]), - &text, - command_env, - ROOT.as_path(), - ) -} - -#[cfg(unix)] -fn chmod(path: &Path, mode: u32) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - let mut permissions = fs::metadata(path)?.permissions(); - permissions.set_mode(mode); - fs::set_permissions(path, permissions)?; - Ok(()) -} - -#[cfg(not(unix))] -fn chmod(_path: &Path, _mode: u32) -> Result<()> { - Ok(()) -} - -fn env_string(name: &str, default: impl FnOnce() -> String) -> String { - env::var(name).unwrap_or_else(|_| default()) -} - -fn env_u32(name: &str, default: u32) -> u32 { - env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(default) -} - -fn env_bool(name: &str, default: bool) -> bool { - env::var(name) - .map(|value| value == "true") - .unwrap_or(default) -} - -fn utc_run_id() -> String { - Utc::now().format("%Y%m%d%H%M%S").to_string() -} - -#[derive(Debug, Clone)] -struct HotpatchConfig { - build_sha: String, - runner_image: String, - producer_event_count: u32, - producer_post_start_event_count: u32, - parallel_completions: u32, - parallel_event_count: u32, - parallel_post_start_event_count: u32, - verify_completions: u32, - max_startup_apply_ms: u32, - max_live_apply_ms: u32, - cleanup_after_run: bool, - job_timeout: String, -} - -impl HotpatchConfig { - fn from_env() -> Self { - let parallel_completions = env_u32("PARALLEL_COMPLETIONS", 3); - Self { - build_sha: env_string("BUILD_SHA", || format!("hotpatch-it-{}", utc_run_id())), - runner_image: env_string("RUNNER_IMAGE", || { - "fluxer-hotpatch-it-runner:local".to_owned() - }), - producer_event_count: env_u32("PRODUCER_EVENT_COUNT", 25), - producer_post_start_event_count: env_u32("PRODUCER_POST_START_EVENT_COUNT", 10), - parallel_completions, - parallel_event_count: env_u32("PARALLEL_EVENT_COUNT", 5), - parallel_post_start_event_count: env_u32("PARALLEL_POST_START_EVENT_COUNT", 2), - verify_completions: env_u32("VERIFY_COMPLETIONS", parallel_completions), - max_startup_apply_ms: env_u32("HOTPATCH_MAX_STARTUP_APPLY_MS", 30_000), - max_live_apply_ms: env_u32("HOTPATCH_MAX_LIVE_APPLY_MS", 30_000), - cleanup_after_run: env_bool("HOTPATCH_CLEANUP_AFTER_RUN", true), - job_timeout: env_string("JOB_TIMEOUT", || "600s".to_owned()), - } - } - - fn expected_event_total(&self) -> u32 { - let producer_total = self.producer_event_count + self.producer_post_start_event_count; - let parallel_total = self.parallel_completions - * (self.parallel_event_count + self.parallel_post_start_event_count); - producer_total + parallel_total - } -} - -fn cleanup_hotpatch_run(command_env: &[(String, Option)]) -> Result<()> { - run_inherit( - kubectl_args(&[ - "delete", - "job", - "hotpatch-producer", - "hotpatch-resume", - "hotpatch-parallel", - "hotpatch-final-resume", - "hotpatch-cluster-verify", - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&[ - "delete", - "statefulset", - "hotpatch-cassandra", - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&[ - "delete", - "pod", - "-l", - "app.kubernetes.io/name=hotpatch-cassandra", - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&[ - "delete", - "service", - "hotpatch-cassandra", - "--ignore-not-found=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&[ - "delete", - "secret", - "hotpatch-signing", - "--ignore-not-found=true", - ]), - command_env, - ROOT.as_path(), - ) -} - -fn build_hotpatch_runner_image( - cfg: &HotpatchConfig, - command_env: &[(String, Option)], -) -> Result<()> { - run_inherit( - vec![ - "docker".to_owned(), - "build".to_owned(), - "-f".to_owned(), - ROOT.join("scripts/local-k8s/Dockerfile.hotpatch-it") - .display() - .to_string(), - "-t".to_owned(), - cfg.runner_image.clone(), - ROOT.display().to_string(), - ], - command_env, - ROOT.as_path(), - )?; - run_inherit( - vec![ - "kind".to_owned(), - "load".to_owned(), - "docker-image".to_owned(), - "--name".to_owned(), - CLUSTER_NAME.to_owned(), - cfg.runner_image.clone(), - ], - command_env, - ROOT.as_path(), - ) -} - -fn deploy_cassandra(command_env: &[(String, Option)]) -> Result<()> { - apply_values(hotpatch_cassandra_manifest(), command_env)?; - run_inherit( - kubectl_args(&[ - "rollout", - "status", - "statefulset/hotpatch-cassandra", - "--timeout=300s", - ]), - command_env, - ROOT.as_path(), - ) -} - -fn hotpatch_cassandra_manifest() -> Vec { - vec![ - json!({ - "apiVersion": "v1", - "kind": "Service", - "metadata": { - "name": "hotpatch-cassandra", - "labels": {"app.kubernetes.io/name": "hotpatch-cassandra"}, - }, - "spec": { - "selector": {"app.kubernetes.io/name": "hotpatch-cassandra"}, - "ports": [{"name": "cql", "port": 9042, "targetPort": 9042}], - }, - }), - json!({ - "apiVersion": "apps/v1", - "kind": "StatefulSet", - "metadata": { - "name": "hotpatch-cassandra", - "labels": {"app.kubernetes.io/name": "hotpatch-cassandra"}, - }, - "spec": { - "serviceName": "hotpatch-cassandra", - "replicas": 1, - "selector": {"matchLabels": {"app.kubernetes.io/name": "hotpatch-cassandra"}}, - "template": { - "metadata": {"labels": {"app.kubernetes.io/name": "hotpatch-cassandra"}}, - "spec": { - "containers": [{ - "name": "cassandra", - "image": "cassandra:5.0.8", - "imagePullPolicy": "IfNotPresent", - "ports": [{"name": "cql", "containerPort": 9042}], - "env": [ - {"name": "CASSANDRA_CLUSTER_NAME", "value": "fluxer-hotpatch-it"}, - {"name": "CASSANDRA_DC", "value": "datacenter1"}, - {"name": "CASSANDRA_ENDPOINT_SNITCH", "value": "GossipingPropertyFileSnitch"}, - {"name": "HEAP_NEWSIZE", "value": "128M"}, - {"name": "MAX_HEAP_SIZE", "value": "768M"}, - ], - "readinessProbe": { - "tcpSocket": {"port": 9042}, - "initialDelaySeconds": 20, - "periodSeconds": 5, - "failureThreshold": 24, - }, - "volumeMounts": [{"name": "data", "mountPath": "/var/lib/cassandra"}], - }], - "volumes": [{"name": "data", "emptyDir": {}}], - }, - }, - }, - }), - ] -} - -async fn wait_for_cql(command_env: &[(String, Option)]) -> Result { - let pod = cassandra_pod(command_env)?; - for _ in 0..90 { - let result = run_quiet( - kubectl_args(&[ - "exec", - &pod, - "--", - "cqlsh", - "-e", - "SELECT release_version FROM system.local;", - ]), - command_env, - ROOT.as_path(), - false, - )?; - if result.status.success() { - return Ok(pod); - } - sleep(Duration::from_secs(5)).await; - } - bail!("local-k8s: Cassandra CQL did not become ready"); -} - -fn cassandra_pod(command_env: &[(String, Option)]) -> Result { - let output = run_quiet( - kubectl_args(&[ - "get", - "pod", - "-l", - "app.kubernetes.io/name=hotpatch-cassandra", - "-o", - "jsonpath={.items[0].metadata.name}", - ]), - command_env, - ROOT.as_path(), - true, - )?; - Ok(output.stdout_text().trim().to_owned()) -} - -fn apply_schema(pod: &str, command_env: &[(String, Option)]) -> Result<()> { - run_with_stdin( - kubectl_args(&["exec", "-i", pod, "--", "cqlsh"]), - HOTPATCH_SCHEMA, - command_env, - ROOT.as_path(), - ) -} - -const HOTPATCH_SCHEMA: &str = r#"CREATE KEYSPACE IF NOT EXISTS fluxer -WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}; - -CREATE TABLE IF NOT EXISTS fluxer.gateway_hotpatch_events_by_build ( - build_sha text, - event_id timeuuid, - schema_version int, - kind text, - created_at timestamp, - created_by text, - signer_key_id text, - bundle_sha256 blob, - signature blob, - bundle blob, - PRIMARY KEY ((build_sha), event_id) -) WITH CLUSTERING ORDER BY (event_id ASC); - -CREATE TABLE IF NOT EXISTS fluxer.gateway_hotpatch_applied_by_node ( - build_sha text, - node_name text, - event_id timeuuid, - applied_at timestamp, - module_count int, - bundle_sha256 blob, - status text, - error text, - PRIMARY KEY ((build_sha, node_name), event_id) -) WITH CLUSTERING ORDER BY (event_id ASC); -"#; - -fn generate_signing_key(command_env: &[(String, Option)]) -> Result<(String, String)> { - let output = run_quiet( - strings(&[ - "erl", - "-noshell", - "-eval", - r#"{PublicKey, PrivateKey} = crypto:generate_key(eddsa, ed25519), io:format("~ts ~ts~n", [base64:encode(PublicKey), base64:encode(PrivateKey)]), halt(0)."#, - ]), - command_env, - ROOT.as_path(), - true, - )?; - let text = output.stdout_text(); - let mut parts = text.split_whitespace(); - let public_key = parts - .next() - .ok_or_else(|| anyhow!("erl did not print a public signing key"))? - .to_owned(); - let private_key = parts - .next() - .ok_or_else(|| anyhow!("erl did not print a private signing key"))? - .to_owned(); - Ok((public_key, private_key)) -} - -fn create_signing_secret( - public_key: &str, - private_key: &str, - command_env: &[(String, Option)], -) -> Result<()> { - let secret = run_quiet( - kubectl_args(&[ - "create", - "secret", - "generic", - "hotpatch-signing", - &format!("--from-literal=public_keys=ops:{public_key}"), - &format!("--from-literal=private_key_base64={private_key}"), - "--dry-run=client", - "-o", - "json", - ]), - command_env, - ROOT.as_path(), - true, - )?; - run_with_stdin( - kubectl_args(&["apply", "-f", "-"]), - &secret.stdout_text(), - command_env, - ROOT.as_path(), - ) -} - -fn delete_job_if_present(name: &str, command_env: &[(String, Option)]) -> Result<()> { - run_inherit( - kubectl_args(&["delete", "job", name, "--ignore-not-found=true"]), - command_env, - ROOT.as_path(), - ) -} - -#[allow(clippy::too_many_arguments)] -fn run_hotpatch_job( - name: &str, - event_count: u32, - post_start_event_count: u32, - expect_event_count: u32, - completions: u32, - parallelism: u32, - cfg: &HotpatchConfig, - command_env: &[(String, Option)], -) -> Result<()> { - delete_job_if_present(name, command_env)?; - apply_values( - vec![hotpatch_job_manifest( - name, - event_count, - post_start_event_count, - expect_event_count, - completions, - parallelism, - cfg, - )], - command_env, - )?; - let wait = run_quiet( - kubectl_args(&[ - "wait", - "--for=condition=complete", - &format!("job/{name}"), - &format!("--timeout={}", cfg.job_timeout), - ]), - command_env, - ROOT.as_path(), - false, - )?; - if !wait.status.success() { - let _ = run_capture_print( - kubectl_args(&["describe", &format!("job/{name}")]), - command_env, - ROOT.as_path(), - ); - let _ = run_capture_print( - kubectl_args(&[ - "logs", - &format!("job/{name}"), - "--all-containers=true", - "--tail=-1", - ]), - command_env, - ROOT.as_path(), - ); - bail!("local-k8s: hotpatch job {name} failed"); - } - run_capture_print( - kubectl_args(&[ - "logs", - &format!("job/{name}"), - "--all-containers=true", - "--tail=-1", - ]), - command_env, - ROOT.as_path(), - )?; - Ok(()) -} - -fn hotpatch_job_manifest( - name: &str, - event_count: u32, - post_start_event_count: u32, - expect_event_count: u32, - completions: u32, - parallelism: u32, - cfg: &HotpatchConfig, -) -> Value { - json!({ - "apiVersion": "batch/v1", - "kind": "Job", - "metadata": {"name": name}, - "spec": { - "completions": completions, - "parallelism": parallelism, - "backoffLimit": 0, - "activeDeadlineSeconds": 540, - "template": { - "spec": { - "restartPolicy": "Never", - "containers": [{ - "name": "hotpatch-smoke", - "image": cfg.runner_image.clone(), - "imagePullPolicy": "IfNotPresent", - "workingDir": "/workspaces/fluxer/fluxer_gateway", - "command": [ - "bash", - "-lc", - "erl -sname hotpatch -pa _build/test/lib/*/ebin -noshell -s gateway_hotpatch_k8s_smoke main" - ], - "env": [ - {"name": "BUILD_SHA", "value": cfg.build_sha.clone()}, - {"name": "FLUXER_GATEWAY_HOTPATCH_ENABLED", "value": "true"}, - {"name": "FLUXER_GATEWAY_LOGGER_LEVEL", "value": "warning"}, - {"name": "FLUXER_GATEWAY_HOTPATCH_CASSANDRA_HOSTS", "value": format!("hotpatch-cassandra.{NAMESPACE}.svc.cluster.local")}, - {"name": "FLUXER_GATEWAY_HOTPATCH_CASSANDRA_PORT", "value": "9042"}, - {"name": "FLUXER_GATEWAY_HOTPATCH_CASSANDRA_KEYSPACE", "value": "fluxer"}, - {"name": "FLUXER_GATEWAY_HOTPATCH_POLL_INTERVAL_MS", "value": "1000"}, - {"name": "FLUXER_GATEWAY_HOTPATCH_STARTUP_SYNC_TIMEOUT_MS", "value": "120000"}, - {"name": "HOTPATCH_EVENT_COUNT", "value": event_count.to_string()}, - {"name": "HOTPATCH_POST_START_EVENT_COUNT", "value": post_start_event_count.to_string()}, - {"name": "HOTPATCH_EXPECT_EVENT_COUNT", "value": expect_event_count.to_string()}, - {"name": "HOTPATCH_TIMEOUT_MS", "value": "120000"}, - {"name": "HOTPATCH_MAX_STARTUP_APPLY_MS", "value": cfg.max_startup_apply_ms.to_string()}, - {"name": "HOTPATCH_MAX_LIVE_APPLY_MS", "value": cfg.max_live_apply_ms.to_string()}, - { - "name": "FLUXER_GATEWAY_HOTPATCH_PUBLIC_KEYS", - "valueFrom": {"secretKeyRef": {"name": "hotpatch-signing", "key": "public_keys"}} - }, - { - "name": "FLUXER_GATEWAY_HOTPATCH_PRIVATE_KEY_BASE64", - "valueFrom": {"secretKeyRef": {"name": "hotpatch-signing", "key": "private_key_base64"}} - }, - ], - }], - }, - }, - }, - }) -} - -fn cql_count( - pod: &str, - cql: &str, - command_env: &[(String, Option)], -) -> Result> { - let output = run_quiet( - kubectl_args(&["exec", pod, "--", "cqlsh", "-k", "fluxer", "-e", cql]), - command_env, - ROOT.as_path(), - true, - )?; - Ok(parse_first_integer_line(&output.stdout_text())) -} - -fn parse_first_integer_line(text: &str) -> Option { - text.lines() - .map(str::trim) - .find(|line| !line.is_empty() && line.bytes().all(|byte| byte.is_ascii_digit())) - .and_then(|line| line.parse().ok()) -} - -fn job_pods(name: &str, command_env: &[(String, Option)]) -> Result> { - let output = run_quiet( - kubectl_args(&[ - "get", - "pods", - "-l", - &format!("job-name={name}"), - "-o", - "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}", - ]), - command_env, - ROOT.as_path(), - true, - )?; - Ok(output - .stdout_text() - .lines() - .filter(|line| !line.trim().is_empty()) - .map(|line| line.trim().to_owned()) - .collect()) -} - -fn assert_job_audits( - pod: &str, - job: &str, - expected: u32, - cfg: &HotpatchConfig, - command_env: &[(String, Option)], -) -> Result<()> { - let pod_names = job_pods(job, command_env)?; - if pod_names.is_empty() { - bail!("local-k8s: expected at least one pod for hotpatch job {job}"); - } - for pod_name in &pod_names { - let node_name = format!("hotpatch@{pod_name}"); - let actual = cql_count( - pod, - &format!( - "SELECT COUNT(*) FROM gateway_hotpatch_applied_by_node WHERE build_sha = '{}' AND node_name = '{}';", - cfg.build_sha, node_name - ), - command_env, - )?; - if actual.unwrap_or(0) < expected { - bail!( - "local-k8s: expected node {node_name} from job {job} to audit at least {expected} hotpatch events, got {}", - actual - .map(|count| count.to_string()) - .unwrap_or_else(|| "".to_owned()) - ); - } - } - println!( - "verified hotpatch audits for job {job}: pods={} expected_events_per_pod={expected}", - pod_names.len() - ); - Ok(()) -} - -fn assert_event_count( - pod: &str, - expected: u32, - cfg: &HotpatchConfig, - command_env: &[(String, Option)], -) -> Result<()> { - let actual = cql_count( - pod, - &format!( - "SELECT COUNT(*) FROM gateway_hotpatch_events_by_build WHERE build_sha = '{}';", - cfg.build_sha - ), - command_env, - )?; - if actual.unwrap_or(0) < expected { - bail!( - "local-k8s: expected at least {expected} hotpatch events for {}, got {}", - cfg.build_sha, - actual - .map(|count| count.to_string()) - .unwrap_or_else(|| "".to_owned()) - ); - } - println!( - "verified {} hotpatch events for build {}", - actual.unwrap_or(0), - cfg.build_sha - ); - Ok(()) -} - -#[derive(Debug, Clone)] -struct HandoffConfig { - image_v1: String, - image_v2: String, - clusters: String, - replicas: u32, - entity_count: u32, - workload_concurrency: u32, - max_increment_latency_ms: u32, - min_workload_ops: u32, - erl_flags: String, - cleanup_after_cluster: bool, - job_timeout_seconds: u64, -} - -impl HandoffConfig { - fn from_env() -> Self { - let run_id = env_string("RUN_ID", utc_run_id); - let image_repo = env_string("HANDOFF_IMAGE_REPO", || { - format!("fluxer-handoff-rollout-it-{run_id}") - }); - let entity_count = env_u32("HANDOFF_ENTITY_COUNT", 600); - Self { - image_v1: env_string("HANDOFF_IMAGE_V1", || format!("{image_repo}:v1")), - image_v2: env_string("HANDOFF_IMAGE_V2", || format!("{image_repo}:v2")), - clusters: env_string("HANDOFF_CLUSTERS", || { - "sessions presence calls guilds".to_owned() - }), - replicas: env_u32("HANDOFF_REPLICAS", 3), - entity_count, - workload_concurrency: env_u32("HANDOFF_WORKLOAD_CONCURRENCY", 8), - max_increment_latency_ms: env_u32("HANDOFF_MAX_INCREMENT_LATENCY_MS", 5000), - min_workload_ops: env_u32("HANDOFF_MIN_WORKLOAD_OPS", entity_count), - erl_flags: env_string("HANDOFF_ERL_FLAGS", || "+S 2:2 +A 4".to_owned()), - cleanup_after_cluster: env_bool("HANDOFF_CLEANUP_AFTER_CLUSTER", true), - job_timeout_seconds: env_u32("JOB_TIMEOUT_SECONDS", 900) as u64, - } - } -} - -fn validate_cluster_name(cluster: &str) -> Result<()> { - if cluster.is_empty() - || !cluster - .bytes() - .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') - { - bail!( - "local-k8s: invalid handoff cluster name: {}", - if cluster.is_empty() { - "" - } else { - cluster - } - ); - } - Ok(()) -} - -fn statefulset_name(cluster: &str) -> String { - format!("handoff-rollout-{cluster}") -} - -fn service_name(cluster: &str) -> String { - format!("handoff-rollout-{cluster}-headless") -} - -fn controller_job_name(cluster: &str) -> String { - format!("handoff-rollout-{cluster}-controller") -} - -fn build_handoff_images( - cfg: &HandoffConfig, - build_context: &Path, - command_env: &[(String, Option)], -) -> Result<()> { - prepare_handoff_build_context(build_context, command_env)?; - for (version, image) in [("v1", &cfg.image_v1), ("v2", &cfg.image_v2)] { - run_inherit( - vec![ - "docker".to_owned(), - "build".to_owned(), - "--build-arg".to_owned(), - format!("HANDOFF_IMAGE_VERSION={version}"), - "-f".to_owned(), - ROOT.join("scripts/local-k8s/Dockerfile.handoff-rollout-it") - .display() - .to_string(), - "-t".to_owned(), - image.clone(), - build_context.display().to_string(), - ], - command_env, - ROOT.as_path(), - )?; - } - for image in [&cfg.image_v1, &cfg.image_v2] { - run_inherit( - vec![ - "kind".to_owned(), - "load".to_owned(), - "docker-image".to_owned(), - "--name".to_owned(), - CLUSTER_NAME.to_owned(), - image.clone(), - ], - command_env, - ROOT.as_path(), - )?; - } - Ok(()) -} - -fn prepare_handoff_build_context( - build_context: &Path, - command_env: &[(String, Option)], -) -> Result<()> { - let gateway_dir = ROOT.join("fluxer_gateway"); - run_inherit( - strings(&["rebar3", "as", "test", "compile"]), - &with_extra(command_env, "GIT_CONFIG_GLOBAL", Some("/dev/null")), - &gateway_dir, - )?; - run_inherit( - vec![ - "erlc".to_owned(), - "-I".to_owned(), - "include".to_owned(), - "-o".to_owned(), - "_build/test/lib/fluxer_gateway/ebin".to_owned(), - "test/gateway_handoff_rollout_k8s_smoke.erl".to_owned(), - ], - command_env, - &gateway_dir, - )?; - - let source = gateway_dir.join("_build/test/lib/fluxer_gateway/ebin"); - let destination = build_context.join("_build/test/lib/fluxer_gateway/ebin"); - copy_dir_recursive(&source, &destination) -} - -fn with_extra( - command_env: &[(String, Option)], - key: &str, - value: Option<&str>, -) -> Vec<(String, Option)> { - let mut next = command_env.to_vec(); - next.push((key.to_owned(), value.map(str::to_owned))); - next -} - -fn copy_dir_recursive(source: &Path, destination: &Path) -> Result<()> { - fs::create_dir_all(destination)?; - for entry in - fs::read_dir(source).with_context(|| format!("failed to read {}", source.display()))? - { - let entry = entry?; - let source_path = entry.path(); - let destination_path = destination.join(entry.file_name()); - let file_type = entry.file_type()?; - if file_type.is_dir() { - copy_dir_recursive(&source_path, &destination_path)?; - } else if file_type.is_file() { - fs::copy(&source_path, &destination_path).with_context(|| { - format!( - "failed to copy {} to {}", - source_path.display(), - destination_path.display() - ) - })?; - } - } - Ok(()) -} - -fn handoff_controller_rbac() -> Vec { - vec![ - json!({ - "apiVersion": "v1", - "kind": "ServiceAccount", - "metadata": { - "name": "handoff-rollout-controller", - "labels": {"app.kubernetes.io/name": "handoff-rollout"}, - }, - }), - json!({ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", - "metadata": { - "name": "handoff-rollout-controller", - "labels": {"app.kubernetes.io/name": "handoff-rollout"}, - }, - "rules": [ - {"apiGroups": [""], "resources": ["pods", "pods/log"], "verbs": ["get", "list", "watch"]}, - {"apiGroups": ["apps"], "resources": ["statefulsets"], "verbs": ["get", "list", "watch", "patch", "update"]}, - ], - }), - json!({ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "RoleBinding", - "metadata": { - "name": "handoff-rollout-controller", - "labels": {"app.kubernetes.io/name": "handoff-rollout"}, - }, - "subjects": [{"kind": "ServiceAccount", "name": "handoff-rollout-controller"}], - "roleRef": { - "apiGroup": "rbac.authorization.k8s.io", - "kind": "Role", - "name": "handoff-rollout-controller", - }, - }), - ] -} - -fn delete_previous_handoff_cluster_run( - cluster: &str, - command_env: &[(String, Option)], -) -> Result<()> { - let statefulset = statefulset_name(cluster); - let service = service_name(cluster); - let job = controller_job_name(cluster); - run_inherit( - kubectl_args(&[ - "delete", - "job", - &job, - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&[ - "delete", - "statefulset", - &statefulset, - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&[ - "delete", - "pod", - "-l", - &format!("app.kubernetes.io/name=handoff-rollout,app.kubernetes.io/instance={cluster}"), - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - run_inherit( - kubectl_args(&["delete", "service", &service, "--ignore-not-found=true"]), - command_env, - ROOT.as_path(), - ) -} - -fn handoff_statefulset(cluster: &str, cfg: &HandoffConfig) -> Vec { - let service = service_name(cluster); - let statefulset = statefulset_name(cluster); - vec![ - json!({ - "apiVersion": "v1", - "kind": "Service", - "metadata": { - "name": service, - "labels": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - }, - "spec": { - "clusterIP": "None", - "selector": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - "ports": [ - {"name": "epmd", "port": 4369, "targetPort": 4369}, - {"name": "erl-dist", "port": 9100, "targetPort": 9100}, - ], - }, - }), - json!({ - "apiVersion": "apps/v1", - "kind": "StatefulSet", - "metadata": { - "name": statefulset, - "labels": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - }, - "spec": { - "serviceName": service_name(cluster), - "replicas": cfg.replicas, - "updateStrategy": { - "type": "RollingUpdate", - "rollingUpdate": {"partition": cfg.replicas}, - }, - "selector": { - "matchLabels": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - }, - "template": { - "metadata": { - "labels": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - }, - "spec": { - "terminationGracePeriodSeconds": 20, - "containers": [{ - "name": "node", - "image": cfg.image_v1.clone(), - "imagePullPolicy": "IfNotPresent", - "ports": [ - {"name": "epmd", "containerPort": 4369}, - {"name": "erl-dist", "containerPort": 9100}, - ], - "env": [ - {"name": "HANDOFF_CLUSTER_NAME", "value": cluster}, - {"name": "HANDOFF_IMAGE_VERSION", "value": "v1"}, - {"name": "POD_IP", "valueFrom": {"fieldRef": {"fieldPath": "status.podIP"}}}, - {"name": "POD_NAMESPACE", "valueFrom": {"fieldRef": {"fieldPath": "metadata.namespace"}}}, - ], - "command": [ - "bash", - "-lc", - format!("exec erl {} -name \"handoff@${{POD_IP}}\" -setcookie fluxer_handoff_rollout_it -kernel inet_dist_listen_min 9100 inet_dist_listen_max 9100 -pa _build/test/lib/*/ebin -noshell -s gateway_handoff_rollout_k8s_smoke node_main", cfg.erl_flags), - ], - "readinessProbe": { - "exec": { - "command": [ - "bash", - "-lc", - "epmd -names | grep -q \"name handoff at port 9100\" && timeout 1 bash -lc \")], -) -> Result<()> { - let job = controller_job_name(cluster); - run_inherit( - kubectl_args(&[ - "delete", - "job", - &job, - "--ignore-not-found=true", - "--wait=true", - ]), - command_env, - ROOT.as_path(), - )?; - apply_values(vec![handoff_controller_job(cluster, cfg)], command_env)?; - if !wait_for_handoff_job(&job, cfg.job_timeout_seconds, command_env).await? { - let _ = run_capture_print( - kubectl_args(&["describe", &format!("job/{job}")]), - command_env, - ROOT.as_path(), - ); - let _ = run_capture_print( - kubectl_args(&[ - "logs", - &format!("job/{job}"), - "--all-containers=true", - "--tail=-1", - ]), - command_env, - ROOT.as_path(), - ); - bail!("local-k8s: handoff controller job {job} failed"); - } - run_capture_print( - kubectl_args(&[ - "logs", - &format!("job/{job}"), - "--all-containers=true", - "--tail=-1", - ]), - command_env, - ROOT.as_path(), - )?; - Ok(()) -} - -fn handoff_controller_job(cluster: &str, cfg: &HandoffConfig) -> Value { - let job = controller_job_name(cluster); - json!({ - "apiVersion": "batch/v1", - "kind": "Job", - "metadata": { - "name": job, - "labels": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - }, - "spec": { - "backoffLimit": 0, - "activeDeadlineSeconds": 840, - "template": { - "metadata": { - "labels": { - "app.kubernetes.io/name": "handoff-rollout", - "app.kubernetes.io/instance": cluster, - }, - }, - "spec": { - "serviceAccountName": "handoff-rollout-controller", - "restartPolicy": "Never", - "containers": [{ - "name": "controller", - "image": cfg.image_v1.clone(), - "imagePullPolicy": "IfNotPresent", - "workingDir": "/workspaces/fluxer/fluxer_gateway", - "command": [ - "bash", - "-lc", - format!("exec erl {} -name \"controller-{cluster}@${{POD_IP}}\" -setcookie fluxer_handoff_rollout_it -kernel inet_dist_listen_min 9100 inet_dist_listen_max 9100 -pa _build/test/lib/*/ebin -noshell -s gateway_handoff_rollout_k8s_smoke controller_main", cfg.erl_flags), - ], - "env": [ - {"name": "KUBECONFIG", "value": ""}, - {"name": "HANDOFF_CLUSTER_NAME", "value": cluster}, - {"name": "HANDOFF_IMAGE_VERSION", "value": "controller"}, - {"name": "HANDOFF_IMAGE_V2", "value": cfg.image_v2.clone()}, - {"name": "HANDOFF_REPLICAS", "value": cfg.replicas.to_string()}, - {"name": "HANDOFF_ENTITY_COUNT", "value": cfg.entity_count.to_string()}, - {"name": "HANDOFF_WORKLOAD_CONCURRENCY", "value": cfg.workload_concurrency.to_string()}, - {"name": "HANDOFF_MAX_INCREMENT_LATENCY_MS", "value": cfg.max_increment_latency_ms.to_string()}, - {"name": "HANDOFF_MIN_WORKLOAD_OPS", "value": cfg.min_workload_ops.to_string()}, - {"name": "POD_IP", "valueFrom": {"fieldRef": {"fieldPath": "status.podIP"}}}, - {"name": "POD_NAMESPACE", "valueFrom": {"fieldRef": {"fieldPath": "metadata.namespace"}}}, - ], - "resources": {"requests": {"cpu": "50m", "memory": "96Mi"}}, - }], - }, - }, - }, - }) -} - -async fn wait_for_handoff_job( - job: &str, - timeout_seconds: u64, - command_env: &[(String, Option)], -) -> Result { - let deadline = Instant::now() + Duration::from_secs(timeout_seconds); - while Instant::now() < deadline { - let status = run_quiet( - kubectl_args(&[ - "get", - &format!("job/{job}"), - "-o", - "jsonpath={.status.succeeded},{.status.failed}", - ]), - command_env, - ROOT.as_path(), - false, - )?; - let text = status.stdout_text(); - let (succeeded, failed) = parse_job_status(&text); - if succeeded == Some(1) { - return Ok(true); - } - if failed.unwrap_or(0) != 0 { - return Ok(false); - } - sleep(Duration::from_secs(2)).await; - } - Ok(false) -} - -fn parse_job_status(text: &str) -> (Option, Option) { - let mut parts = text.trim().splitn(2, ','); - let succeeded = parts.next().and_then(|part| { - let part = part.trim(); - if part.is_empty() { - None - } else { - part.parse().ok() - } - }); - let failed = parts.next().and_then(|part| { - let part = part.trim(); - if part.is_empty() { - None - } else { - part.parse().ok() - } - }); - (succeeded, failed) -} - -fn docker_save_output_file(args: &[OsString]) -> Option { - if args.first().and_then(|arg| arg.to_str()) != Some("save") { - return None; - } - let mut index = 1; - while index < args.len() { - let arg = args[index].to_string_lossy(); - if arg == "-o" || arg == "--output" { - return args.get(index + 1).map(PathBuf::from); - } - if let Some(value) = arg.strip_prefix("--output=") { - return Some(PathBuf::from(value)); - } - index += 1; - } - None -} - -fn run_real_docker(args: &[OsString]) -> std::io::Result { - Command::new(REAL_DOCKER) - .args(args) - .env("DOCKER_HOST", DOCKER_HOST) - .status() -} - -fn run_sudo_docker(args: &[OsString]) -> std::io::Result { - Command::new("sudo") - .arg("-n") - .arg("env") - .arg(format!("DOCKER_HOST={DOCKER_HOST}")) - .arg(REAL_DOCKER) - .args(args) - .status() -} - -fn status_to_code(status: std::io::Result) -> i32 { - match status { - Ok(status) => status.code().unwrap_or(1), - Err(error) => { - eprintln!("docker wrapper: {error}"); - 1 - } - } -} - -fn chown_current_user(path: &Path) -> Result<()> { - #[cfg(unix)] - { - let owner = format!("{}:{}", unsafe { libc::geteuid() }, unsafe { - libc::getegid() - }); - let status = Command::new("sudo") - .arg("-n") - .arg("chown") - .arg(owner) - .arg(path) - .status()?; - if !status.success() { - bail!("sudo chown failed for {}", path.display()); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn renders_kind_config_with_local_api_port() { - let config = render_kind_config(36443); - assert!(config.contains("apiServerAddress: \"0.0.0.0\"")); - assert!(config.contains("apiServerPort: 36443")); - assert!(config.contains("- host.docker.internal")); - } - - #[test] - fn only_accepts_local_kubernetes_api_servers() { - assert!(is_safe_api_server("https://host.docker.internal:36443")); - assert!(is_safe_api_server("https://127.0.0.1:36443")); - assert!(is_safe_api_server("https://localhost:36443")); - assert!(!is_safe_api_server("https://example.com:6443")); - assert!(!is_safe_api_server("")); - } - - #[test] - fn validates_handoff_cluster_names() { - assert!(validate_cluster_name("sessions").is_ok()); - assert!(validate_cluster_name("call-shard-1").is_ok()); - assert!(validate_cluster_name("").is_err()); - assert!(validate_cluster_name("Sessions").is_err()); - assert!(validate_cluster_name("calls_1").is_err()); - } - - #[test] - fn computes_hotpatch_expected_total() { - let cfg = HotpatchConfig { - build_sha: "build".to_owned(), - runner_image: "runner".to_owned(), - producer_event_count: 25, - producer_post_start_event_count: 10, - parallel_completions: 3, - parallel_event_count: 5, - parallel_post_start_event_count: 2, - verify_completions: 3, - max_startup_apply_ms: 30_000, - max_live_apply_ms: 30_000, - cleanup_after_run: true, - job_timeout: "600s".to_owned(), - }; - assert_eq!(cfg.expected_event_total(), 56); - } - - #[test] - fn parses_cql_count_output() { - let output = "\n count\n-------\n 42\n\n(1 rows)\n"; - assert_eq!(parse_first_integer_line(output), Some(42)); - assert_eq!(parse_first_integer_line("no rows"), None); - } - - #[test] - fn parses_job_status_output() { - assert_eq!(parse_job_status("1,"), (Some(1), None)); - assert_eq!(parse_job_status(",2"), (None, Some(2))); - assert_eq!(parse_job_status("0,0"), (Some(0), Some(0))); - } - - #[test] - fn parses_docker_save_output_flags() { - assert_eq!( - docker_save_output_file(&[ - OsString::from("save"), - OsString::from("-o"), - OsString::from("/tmp/image.tar"), - OsString::from("image:tag"), - ]), - Some(PathBuf::from("/tmp/image.tar")) - ); - assert_eq!( - docker_save_output_file(&[ - OsString::from("save"), - OsString::from("--output=/tmp/image.tar"), - ]), - Some(PathBuf::from("/tmp/image.tar")) - ); - assert_eq!(docker_save_output_file(&[OsString::from("pull")]), None); - } - - #[test] - fn builds_hotpatch_job_manifest() { - let cfg = HotpatchConfig { - build_sha: "build-sha".to_owned(), - runner_image: "runner:local".to_owned(), - producer_event_count: 25, - producer_post_start_event_count: 10, - parallel_completions: 3, - parallel_event_count: 5, - parallel_post_start_event_count: 2, - verify_completions: 3, - max_startup_apply_ms: 30_000, - max_live_apply_ms: 30_000, - cleanup_after_run: true, - job_timeout: "600s".to_owned(), - }; - let manifest = hotpatch_job_manifest("hotpatch-producer", 25, 10, 25, 1, 1, &cfg); - assert_eq!(manifest["kind"], "Job"); - assert_eq!(manifest["metadata"]["name"], "hotpatch-producer"); - assert_eq!( - manifest["spec"]["template"]["spec"]["containers"][0]["image"], - "runner:local" - ); - } - - #[test] - fn builds_handoff_resource_names() { - assert_eq!(statefulset_name("sessions"), "handoff-rollout-sessions"); - assert_eq!( - service_name("sessions"), - "handoff-rollout-sessions-headless" - ); - assert_eq!( - controller_job_name("sessions"), - "handoff-rollout-sessions-controller" - ); - } - - #[test] - fn builds_handoff_statefulset_manifest() { - let cfg = HandoffConfig { - image_v1: "handoff:v1".to_owned(), - image_v2: "handoff:v2".to_owned(), - clusters: "sessions".to_owned(), - replicas: 3, - entity_count: 600, - workload_concurrency: 8, - max_increment_latency_ms: 5000, - min_workload_ops: 600, - erl_flags: "+S 2:2 +A 4".to_owned(), - cleanup_after_cluster: true, - job_timeout_seconds: 900, - }; - let manifest = handoff_statefulset("sessions", &cfg); - assert_eq!(manifest.len(), 2); - assert_eq!(manifest[0]["kind"], "Service"); - assert_eq!(manifest[1]["kind"], "StatefulSet"); - assert_eq!(manifest[1]["spec"]["replicas"], 3); - assert_eq!( - manifest[1]["spec"]["template"]["spec"]["containers"][0]["image"], - "handoff:v1" - ); - } -} diff --git a/tools/dev/src/main.rs b/tools/dev/src/main.rs index de3ce3640..257adc4b3 100644 --- a/tools/dev/src/main.rs +++ b/tools/dev/src/main.rs @@ -7,7 +7,7 @@ use fluxer_dev::cassandra::{ }; use fluxer_dev::desktop::{ build_desktop, install_desktop, package_desktop, run_desktop, run_desktop_canary, - smoke_build_desktop, typecheck_desktop, + typecheck_desktop, }; use fluxer_dev::env::merge_default_env_with_current; use fluxer_dev::manifest::{DEV_PROXY_PORT, LOCAL_APP_URL}; @@ -34,14 +34,11 @@ enum Command { Proxy(ProxyArgs), Dev(DevArgs), RustServices(RustServicesArgs), - Smoke(SmokeArgs), Cassandra(CassandraArgs), Desktop(DesktopArgs), - LocalK8s(LocalK8sArgs), Marketing(MarketingArgs), MediaProxy(MediaProxyArgs), Tunnel(TunnelArgs), - NativeVoiceIt(fluxer_dev::native_voice_it::NativeVoiceItArgs), } #[derive(Debug, Args)] @@ -80,14 +77,6 @@ struct RustServicesArgs { services: Vec, } -#[derive(Debug, Args)] -struct SmokeArgs { - #[arg(long)] - quick: bool, - #[arg(long)] - public: bool, -} - #[derive(Debug, Args)] struct CassandraArgs { #[command(subcommand)] @@ -119,7 +108,6 @@ enum DesktopCommand { skip_native: bool, }, Typecheck, - SmokeBuild, Package { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] builder_args: Vec, @@ -148,27 +136,6 @@ enum DesktopCommand { }, } -#[derive(Debug, Args)] -struct LocalK8sArgs { - #[command(subcommand)] - command: LocalK8sCommand, -} - -#[derive(Debug, Subcommand)] -enum LocalK8sCommand { - CreateCluster, - Kubectl { - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - args: Vec, - }, - Helm { - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - args: Vec, - }, - HotpatchSmoke, - HandoffRolloutSmoke, -} - #[derive(Debug, Args)] struct MarketingArgs { #[command(subcommand)] @@ -188,9 +155,7 @@ struct MediaProxyArgs { } #[derive(Debug, Subcommand)] -#[allow(clippy::large_enum_variant)] enum MediaProxyCommand { - BeeGifBench, Doctor { #[arg(long)] repair: bool, @@ -199,13 +164,8 @@ enum MediaProxyCommand { #[arg(long)] path: Option, }, - SeaweedfsIntegration { - #[arg(long)] - isolated_store: bool, - }, RustStressSmoke, - StressCompare(fluxer_dev::media_stress::StressCompareArgs), - SignExternalUrl(fluxer_dev::media_stress::SignExternalUrlArgs), + SignExternalUrl(fluxer_dev::media_external::SignExternalUrlArgs), } #[derive(Debug, Args)] @@ -236,21 +196,6 @@ enum TunnelCommand { #[tokio::main] async fn main() -> Result<()> { - if std::env::args_os() - .next() - .and_then(|path| { - std::path::PathBuf::from(path) - .file_name() - .map(|name| name.to_owned()) - }) - .as_deref() - == Some(std::ffi::OsStr::new("docker")) - { - std::process::exit(fluxer_dev::local_k8s::run_docker_wrapper( - std::env::args_os().skip(1), - )); - } - let cli = Cli::parse(); if !matches!( cli.command, @@ -284,7 +229,6 @@ async fn main() -> Result<()> { Command::RustServices(args) => { std::process::exit(fluxer_dev::rust_services::run_rust_services(&args.services).await?) } - Command::Smoke(args) => fluxer_dev::smoke::run_smoke(args.quick, args.public).await?, Command::Cassandra(args) => match args.command { CassandraCommand::Diff { output } => { let diff = compute_diff(None).await?; @@ -307,7 +251,6 @@ async fn main() -> Result<()> { DesktopCommand::Install => install_desktop()?, DesktopCommand::Build { skip_native } => build_desktop(skip_native)?, DesktopCommand::Typecheck => typecheck_desktop()?, - DesktopCommand::SmokeBuild => smoke_build_desktop()?, DesktopCommand::Package { builder_args } => package_desktop(&builder_args)?, DesktopCommand::Run { app_url, @@ -329,15 +272,6 @@ async fn main() -> Result<()> { fluxer_dev::disclaim::exec_disclaimed(&program, &args)? } }, - Command::LocalK8s(args) => match args.command { - LocalK8sCommand::CreateCluster => fluxer_dev::local_k8s::create_cluster().await?, - LocalK8sCommand::Kubectl { args } => fluxer_dev::local_k8s::run_kubectl_cli(&args)?, - LocalK8sCommand::Helm { args } => fluxer_dev::local_k8s::run_helm_cli(&args)?, - LocalK8sCommand::HotpatchSmoke => fluxer_dev::local_k8s::run_hotpatch_smoke().await?, - LocalK8sCommand::HandoffRolloutSmoke => { - fluxer_dev::local_k8s::run_handoff_rollout_smoke().await? - } - }, Command::Marketing(args) => match args.command { MarketingCommand::PreprocessBlogImage(args) => { fluxer_dev::marketing::preprocess_blog_image(args)? @@ -347,7 +281,6 @@ async fn main() -> Result<()> { } }, Command::MediaProxy(args) => match args.command { - MediaProxyCommand::BeeGifBench => fluxer_dev::media_proxy::run_bee_gif_bench().await?, MediaProxyCommand::Doctor { repair, base_url, @@ -356,20 +289,13 @@ async fn main() -> Result<()> { fluxer_dev::media_proxy::run_dev_media_doctor(repair, &base_url, path.as_deref()) .await?; } - MediaProxyCommand::SeaweedfsIntegration { isolated_store } => { - fluxer_dev::media_proxy::run_seaweedfs_media_proxy_integration(isolated_store) - .await?; - } MediaProxyCommand::RustStressSmoke => { fluxer_dev::media_proxy::run_rust_stress_smoke()?; } - MediaProxyCommand::StressCompare(args) => { - std::process::exit(fluxer_dev::media_stress::run_stress_compare(args).await?) - } MediaProxyCommand::SignExternalUrl(args) => { println!( "{}", - fluxer_dev::media_stress::sign_external_url( + fluxer_dev::media_external::sign_external_url( &args.secret_key, &args.server_url, &args.upstream @@ -391,7 +317,6 @@ async fn main() -> Result<()> { fluxer_dev::tunnel::run_cloudflare_tunnel(token, token_file).await?, ), }, - Command::NativeVoiceIt(args) => fluxer_dev::native_voice_it::run(args).await?, } Ok(()) } diff --git a/tools/dev/src/media_external.rs b/tools/dev/src/media_external.rs new file mode 100644 index 000000000..37b91daea --- /dev/null +++ b/tools/dev/src/media_external.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use anyhow::{Context, Result}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use clap::Args; +use hmac::{Hmac, KeyInit, Mac}; +use sha2::Sha256; + +#[derive(Debug, Clone, Args)] +pub struct SignExternalUrlArgs { + #[arg(long)] + pub secret_key: String, + #[arg(long)] + pub server_url: String, + pub upstream: String, +} + +pub fn sign_external_url(secret_key: &str, server_url: &str, upstream: &str) -> Result { + let path = format!("v2/{}", URL_SAFE_NO_PAD.encode(upstream.as_bytes())); + let mut mac = Hmac::::new_from_slice(secret_key.as_bytes()) + .context("failed to create HMAC signer")?; + mac.update(path.as_bytes()); + let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); + Ok(format!( + "{}/external/{}/{}", + server_url.trim_end_matches('/'), + signature, + path + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signs_external_urls_with_urlsafe_components() { + let signed = sign_external_url( + "benchmark-secret", + "http://127.0.0.1:19110/", + "https://example.test/a b.jpg", + ) + .unwrap(); + let parts = signed.split('/').collect::>(); + assert!(signed.starts_with("http://127.0.0.1:19110/external/")); + assert_eq!(parts[5], "v2"); + assert_eq!( + URL_SAFE_NO_PAD.decode(parts[6]).unwrap(), + b"https://example.test/a b.jpg" + ); + assert_eq!(URL_SAFE_NO_PAD.decode(parts[4]).unwrap().len(), 32); + } +} diff --git a/tools/dev/src/media_proxy.rs b/tools/dev/src/media_proxy.rs index b6aa685bb..414303011 100644 --- a/tools/dev/src/media_proxy.rs +++ b/tools/dev/src/media_proxy.rs @@ -1,23 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +use crate::object_store::{ensure_s3_buckets, wait_s3_api}; use crate::paths::{ - DEV_LOG_DIR, DEV_SEAWEEDFS_DIR, DEV_SEAWEEDFS_PID_FILE, ROOT, ensure_writable_dev_paths, which, + DEV_LOG_DIR, DEV_SEAWEEDFS_DIR, DEV_SEAWEEDFS_PID_FILE, ensure_writable_dev_paths, which, }; -use crate::proc::{RunOptions, merged_env, run_command}; -use crate::smoke::{ensure_s3_buckets, wait_s3_api}; use anyhow::{Context, Result, bail}; -use reqwest::Method; -use sha2::{Digest, Sha256}; -use std::collections::BTreeMap; -use std::env; -use std::fs::{self, File, OpenOptions}; -use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs}; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::fs::{self, OpenOptions}; +use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; +use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use tokio::time::sleep; -const S3_BUCKETS: &str = "cdn,uploads,static"; const DEV_S3_BUCKETS: &str = "fluxer,fluxer-uploads,fluxer-downloads,fluxer-reports,fluxer-harvests,fluxer-static"; const DEV_S3_ACCESS_KEY_ID: &str = "fluxer"; @@ -25,22 +18,6 @@ const DEV_S3_SECRET_ACCESS_KEY: &str = "fluxer-secret"; const DEV_S3_HOST: &str = "127.0.0.1"; const DEV_S3_PORT: u16 = 8333; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ObjectStore { - pub endpoint: String, - pub access_key_id: String, - pub secret_access_key: String, - pub region: String, - pub container_name: Option, -} - -#[derive(Debug, Clone)] -pub struct HttpResponse { - pub status: u16, - pub headers: BTreeMap, - pub body: Vec, -} - pub async fn run_dev_media_doctor( repair: bool, base_url: &str, @@ -75,6 +52,63 @@ pub async fn ensure_dev_object_store(repair: bool, repair_timeout_secs: u64) -> Ok(()) } +pub fn run_rust_stress_smoke() -> Result<()> { + crate::proc::run(&["cargo", "test", "-p", "fluxer-media-proxy"])?; + crate::proc::run(&[ + "cargo", + "bench", + "-p", + "fluxer-media-proxy", + "--bench", + "core", + "--", + "--sample-size", + "10", + "--warm-up-time", + "1", + "--measurement-time", + "1", + ])?; + + if which("cargo-fuzz").is_none() { + eprintln!("cargo-fuzz not installed; skipping fuzz smoke"); + return Ok(()); + } + let use_nightly = rustup_has_nightly(); + for target in ["parsers", "signing_external_path", "thumbhash"] { + run_fuzz_smoke(target, use_nightly)?; + } + Ok(()) +} + +fn rustup_has_nightly() -> bool { + let Ok(output) = Command::new("rustup").args(["toolchain", "list"]).output() else { + return false; + }; + if !output.status.success() { + return false; + } + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|line| line.starts_with("nightly")) +} + +fn run_fuzz_smoke(target: &str, use_nightly: bool) -> Result<()> { + if use_nightly { + crate::proc::run(&[ + "cargo", + "+nightly", + "fuzz", + "run", + target, + "--", + "-runs=1000", + ]) + } else { + crate::proc::run(&["cargo", "fuzz", "run", target, "--", "-runs=1000"]) + } +} + fn start_dev_seaweedfs() -> Result<()> { ensure_writable_dev_paths()?; if tcp_reachable(DEV_S3_HOST, DEV_S3_PORT) { @@ -183,1005 +217,30 @@ async fn check_dev_media_path(base_url: &str, path: &str) -> Result<()> { format!("/{path}") }; let url = format!("{}{}", base_url.trim_end_matches('/'), path); - let response = request("GET", &url, None, None).await?; - if response.status == 502 { + let status = request_status(&url).await?; + if status == 502 { bail!("Media proxy returned 502 for {url}; storage is still unreachable from the proxy."); } - if response.status >= 500 { - bail!("Media proxy returned {} for {url}", response.status); + if status >= 500 { + bail!("Media proxy returned {status} for {url}"); } - println!("Media path check: {} ({url})", response.status); + println!("Media path check: {status} ({url})"); Ok(()) } - -pub async fn run_seaweedfs_media_proxy_integration(isolated_store: bool) -> Result<()> { - let store = if isolated_store { - start_isolated_store().await? - } else { - discover_store().await? - }; - let temp_dir = tempfile::Builder::new() - .prefix("fluxer-media-proxy-seaweedfs.") - .tempdir()?; - let mut proxy_process = None; - let mut log_file = None; - let result = async { - generate_fixtures(temp_dir.path())?; - build_media_proxy()?; - seed_fixtures(&store, temp_dir.path())?; - let log = File::create(temp_dir.path().join("proxy.log"))?; - let proxy_port = find_free_port("127.0.0.1")?; - proxy_process = Some(start_media_proxy(&store, proxy_port, &log)?); - log_file = Some(log); - wait_for_success(&format!("http://127.0.0.1:{proxy_port}/_health"), 120).await?; - check_media_proxy_routes(proxy_port).await - } - .await; - if let Some(mut process) = proxy_process { - let _ = process.kill(); - let _ = process.wait(); - } - drop(log_file); - if let Some(container_name) = &store.container_name { - let _ = run_command( - &["docker", "rm", "-f", container_name], - RunOptions { - check: false, - capture: true, - ..RunOptions::default() - }, - ); - } - result?; - println!("SeaweedFS media proxy integration test passed."); - Ok(()) -} - -pub fn run_rust_stress_smoke() -> Result<()> { - crate::proc::run(&["cargo", "test", "-p", "fluxer-media-proxy"])?; - crate::proc::run(&[ - "cargo", - "bench", - "-p", - "fluxer-media-proxy", - "--bench", - "core", - "--", - "--sample-size", - "10", - "--warm-up-time", - "1", - "--measurement-time", - "1", - ])?; - - if which("cargo-fuzz").is_none() { - eprintln!("cargo-fuzz not installed; skipping fuzz smoke"); - return Ok(()); - } - let use_nightly = rustup_has_nightly(); - for target in ["parsers", "signing_external_path", "thumbhash"] { - run_fuzz_smoke(target, use_nightly)?; - } - Ok(()) -} - -pub async fn run_bee_gif_bench() -> Result<()> { - require_command("ffprobe")?; - let cfg = BeeGifBenchConfig::from_env(); - let temp_dir = tempfile::Builder::new() - .prefix("fluxer-bee-gif-bench.") - .tempdir()?; - let storage_root = temp_dir.path().join("storage"); - let server_log = temp_dir.path().join("server.log"); - let server_bin = ROOT.join("target/release/fluxer-media-proxy"); - - if cfg.build || !server_bin.is_file() { - eprintln!("[bee-gif-bench] building media proxy"); - run_command( - &["cargo", "build", "--release", "-p", "fluxer-media-proxy"], - RunOptions::default(), - )?; - } - - fs::create_dir_all(&cfg.cache_dir)?; - let fixture = cfg.cache_dir.join("animated-policy-fixture.gif"); - ensure_bee_gif_fixture(&fixture, &cfg.fixture_url).await?; - - let object_path = storage_root.join("cdn").join(BEE_GIF_OBJECT_KEY); - if let Some(parent) = object_path.parent() { - fs::create_dir_all(parent)?; - } - fs::copy(&fixture, &object_path)?; - - let fixture_bytes = fs::read(&fixture)?; - let fixture_sha = sha256_hex(&fixture_bytes); - let (fixture_width, fixture_height) = image::image_dimensions(&fixture) - .map_err(|error| anyhow::anyhow!("failed to read GIF dimensions: {error}"))?; - let target_height = scaled_height(fixture_width, fixture_height, cfg.target_width); - let expected_screen = format!("{}x{target_height}", cfg.target_width); - let fixture_frames = gifsicle_info(&fixture).and_then(|info| parse_gifsicle_frame_count(&info)); - eprintln!( - "[bee-gif-bench] fixture bytes={} size={}x{} sha256={fixture_sha}", - fixture_bytes.len(), - fixture_width, - fixture_height - ); - - let log = File::create(&server_log)?; - let stderr = log.try_clone()?; - let server = Command::new(&server_bin) - .env("FLUXER_MEDIA_PROXY_SECRET_KEY", "bench-secret") - .env("FLUXER_MEDIA_PROXY_STORAGE_ROOT", &storage_root) - .env("FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_BYTES", "0") - .env("FLUXER_MEDIA_PROXY_PORT", cfg.port.to_string()) - .env("FLUXER_MEDIA_PROXY_HOST", "127.0.0.1") - .stdout(Stdio::from(log)) - .stderr(Stdio::from(stderr)) - .spawn()?; - let mut server = ChildGuard::new(server); - let base_url = format!("http://127.0.0.1:{}", cfg.port); - if let Err(error) = wait_for_success(&format!("{base_url}/_health"), 5).await { - let log_text = fs::read_to_string(&server_log).unwrap_or_default(); - bail!("server did not become healthy: {error}\n{log_text}"); - } - - let context = BeeGifBenchContext { - base_url, - fixture_sha, - fixture_bytes: fixture_bytes.len(), - fixture_frames, - max_seconds: cfg.max_seconds, - work_dir: temp_dir.path().to_path_buf(), - }; - measure_original(&context, "animated_query_passthrough", "?animated=true").await?; - measure_original( - &context, - "explicit_gif_passthrough", - "?animated=true&format=gif", - ) - .await?; - measure_animated_webp( - &context, - "animated_webp_transcode", - "?format=webp&animated=true", - ) - .await?; - measure_original( - &context, - "native_size_passthrough", - &format!("?width={fixture_width}&height={fixture_height}&animated=true"), - ) - .await?; - measure_original( - &context, - "no_upscale_passthrough", - &format!("?width={}&animated=true", fixture_width + 1), - ) - .await?; - measure_gif_transform( - &context, - "sized_gif_transform", - &format!("?format=gif&width={}&animated=true", cfg.target_width), - &expected_screen, - ) - .await?; - measure_animated_webp( - &context, - "sized_webp_transcode", - &format!("?format=webp&width={}&animated=true", cfg.target_width), - ) - .await?; - measure_static_webp( - &context, - "static_webp_freeze_frame", - &format!("?format=webp&width={}&animated=false", cfg.target_width), - ) - .await?; - - if cfg.full_transcode { - require_command("gif2webp")?; - eprintln!( - "[bee-gif-bench] running stock gif2webp comparison; this is expected to be much slower" - ); - let stock = temp_dir.path().join("stock.webp"); - let start = Instant::now(); - let status = Command::new("gif2webp") - .args(["-quiet", "-q", "85", "-m", "0", "-mt"]) - .arg(&fixture) - .arg("-o") - .arg(&stock) - .status()?; - if !status.success() { - bail!( - "gif2webp failed with exit code {}", - status.code().unwrap_or(1) - ); - } - let elapsed = start.elapsed(); - let stock_bytes = fs::metadata(&stock)?.len(); - println!( - "stock_gif2webp_m0_lossless seconds={:.3} bytes={stock_bytes}", - elapsed.as_secs_f64() - ); - } - - server.stop(); - Ok(()) -} - -fn rustup_has_nightly() -> bool { - let Ok(output) = Command::new("rustup").args(["toolchain", "list"]).output() else { - return false; - }; - if !output.status.success() { - return false; - } - String::from_utf8_lossy(&output.stdout) - .lines() - .any(|line| line.starts_with("nightly")) -} - -fn run_fuzz_smoke(target: &str, use_nightly: bool) -> Result<()> { - if use_nightly { - crate::proc::run(&[ - "cargo", - "+nightly", - "fuzz", - "run", - target, - "--", - "-runs=1000", - ]) - } else { - crate::proc::run(&["cargo", "fuzz", "run", target, "--", "-runs=1000"]) - } -} - -pub async fn discover_store() -> Result { - if let Ok(endpoint) = env::var("FLUXER_S3_ENDPOINT") { - return Ok(ObjectStore { - endpoint, - access_key_id: env::var("FLUXER_S3_ACCESS_KEY_ID") - .unwrap_or_else(|_| "fluxer".to_owned()), - secret_access_key: env::var("FLUXER_S3_SECRET_ACCESS_KEY") - .unwrap_or_else(|_| "fluxer-secret".to_owned()), - region: env::var("FLUXER_S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned()), - container_name: None, - }); - } - if tcp_reachable("seaweedfs", 8333) { - return Ok(ObjectStore { - endpoint: "http://seaweedfs:8333".to_owned(), - access_key_id: "fluxer".to_owned(), - secret_access_key: "fluxer-secret".to_owned(), - region: "us-east-1".to_owned(), - container_name: None, - }); - } - if tcp_reachable("127.0.0.1", 3900) { - return Ok(ObjectStore { - endpoint: "http://127.0.0.1:3900".to_owned(), - access_key_id: "fluxer".to_owned(), - secret_access_key: "fluxer-secret".to_owned(), - region: "us-east-1".to_owned(), - container_name: None, - }); - } - start_isolated_store().await -} - -pub async fn start_isolated_store() -> Result { - require_command("docker")?; - let s3_port = find_free_port("127.0.0.1")?; - let master_port = find_free_port("127.0.0.1")?; - let container_name = format!("fluxer-media-proxy-seaweedfs-{}", std::process::id()); - let access_key_id = "fluxeraccess"; - let secret_access_key = "fluxersecretkey1234567890"; - let image = - env::var("SEAWEEDFS_IMAGE").unwrap_or_else(|_| "chrislusf/seaweedfs:latest".to_owned()); - run_command( - &[ - "docker", - "run", - "-d", - "--rm", - "--name", - &container_name, - "-p", - &format!("127.0.0.1:{s3_port}:8333"), - "-p", - &format!("127.0.0.1:{master_port}:9333"), - "-e", - &format!("AWS_ACCESS_KEY_ID={access_key_id}"), - "-e", - &format!("AWS_SECRET_ACCESS_KEY={secret_access_key}"), - "-e", - &format!("S3_BUCKET={S3_BUCKETS}"), - &image, - ], - RunOptions { - capture: true, - ..RunOptions::default() - }, - )?; - wait_for_success( - &format!("http://127.0.0.1:{master_port}/cluster/status"), - 120, - ) - .await?; - wait_for_http_endpoint(&format!("http://127.0.0.1:{s3_port}/"), 120).await?; - Ok(ObjectStore { - endpoint: format!("http://127.0.0.1:{s3_port}"), - access_key_id: access_key_id.to_owned(), - secret_access_key: secret_access_key.to_owned(), - region: "us-east-1".to_owned(), - container_name: Some(container_name), - }) -} - -fn generate_fixtures(temp_dir: &Path) -> Result<()> { - require_command("ffmpeg")?; - let image = temp_dir.join("fixture.png"); - let video = temp_dir.join("fixture.mp4"); - run_command( - &[ - "ffmpeg", - "-hide_banner", - "-loglevel", - "error", - "-y", - "-f", - "lavfi", - "-i", - "testsrc2=size=320x240:rate=30:duration=1", - "-frames:v", - "1", - &image.display().to_string(), - ], - RunOptions::default(), - )?; - run_command( - &[ - "ffmpeg", - "-hide_banner", - "-loglevel", - "error", - "-y", - "-f", - "lavfi", - "-i", - "testsrc2=size=640x360:rate=30:duration=2", - "-f", - "lavfi", - "-i", - "sine=frequency=880:duration=2", - "-c:v", - "libx264", - "-pix_fmt", - "yuv420p", - "-c:a", - "aac", - "-movflags", - "+faststart", - &video.display().to_string(), - ], - RunOptions::default(), - ) - .map(drop) -} - -fn build_media_proxy() -> Result<()> { - ensure_writable_dev_paths()?; - crate::proc::run(&["cargo", "build", "--release", "-p", "fluxer-media-proxy"]) -} - -fn seed_fixtures(store: &ObjectStore, temp_dir: &Path) -> Result<()> { - let image = temp_dir.join("fixture.png").display().to_string(); - let video = temp_dir.join("fixture.mp4").display().to_string(); - let mut env = media_proxy_env(store); - env.push(("S3_FIXTURE_IMAGE".to_owned(), Some(image))); - env.push(("S3_FIXTURE_VIDEO".to_owned(), Some(video))); - run_command( - &[ - "cargo", - "run", - "--release", - "-p", - "fluxer-media-proxy", - "--bin", - "fluxer-media-proxy-s3-fixture", - ], - RunOptions { - env, - ..RunOptions::default() - }, - ) - .map(drop) -} - -fn start_media_proxy(store: &ObjectStore, port: u16, log_file: &File) -> Result { - let binary = ROOT.join("target/release/fluxer-media-proxy"); - let mut env = media_proxy_env(store); - env.extend([ - ( - "FLUXER_MEDIA_PROXY_HOST".to_owned(), - Some("127.0.0.1".to_owned()), - ), - ("FLUXER_MEDIA_PROXY_PORT".to_owned(), Some(port.to_string())), - ]); - let process_env = merged_env(Some(&env), true)?; - Ok(Command::new(binary) - .current_dir(ROOT.as_path()) - .env_clear() - .envs(process_env) - .stdout(Stdio::from(log_file.try_clone()?)) - .stderr(Stdio::from(log_file.try_clone()?)) - .spawn()?) -} - -pub fn media_proxy_env(store: &ObjectStore) -> Vec<(String, Option)> { - vec![ - ( - "FLUXER_MEDIA_PROXY_SECRET_KEY".to_owned(), - Some("integration-secret".to_owned()), - ), - ( - "FLUXER_MEDIA_PROXY_STORAGE_BACKEND".to_owned(), - Some("s3".to_owned()), - ), - ( - "FLUXER_S3_ENDPOINT".to_owned(), - Some(store.endpoint.clone()), - ), - ("FLUXER_S3_REGION".to_owned(), Some(store.region.clone())), - ( - "FLUXER_S3_ACCESS_KEY_ID".to_owned(), - Some(store.access_key_id.clone()), - ), - ( - "FLUXER_S3_SECRET_ACCESS_KEY".to_owned(), - Some(store.secret_access_key.clone()), - ), - ( - "FLUXER_S3_FORCE_PATH_STYLE".to_owned(), - Some("true".to_owned()), - ), - ("FLUXER_S3_BUCKET_CDN".to_owned(), Some("cdn".to_owned())), - ( - "FLUXER_S3_BUCKET_UPLOADS".to_owned(), - Some("uploads".to_owned()), - ), - ( - "FLUXER_S3_BUCKET_STATIC".to_owned(), - Some("static".to_owned()), - ), - ] -} - -async fn check_media_proxy_routes(port: u16) -> Result<()> { - let proxy = format!("http://127.0.0.1:{port}"); - let theme = request("GET", &format!("{proxy}/themes/test.css"), None, None).await?; - assert_status(&theme, 200)?; - assert_header_prefix(&theme, "content-type", "text/css; charset=utf-8")?; - - let range = request( - "GET", - &format!("{proxy}/attachments/1/2/file.txt"), - Some(vec![("Range", "bytes=2-7")]), - None, - ) - .await?; - assert_status(&range, 206)?; - if range.body != b"234567" { - bail!("unexpected range response body"); - } - - let head = request( - "HEAD", - &format!("{proxy}/attachments/1/2/file.txt"), - None, - None, - ) - .await?; - assert_status(&head, 200)?; - if head.headers.get("content-length").map(String::as_str) != Some("16") { - bail!("unexpected content-length for fixture object"); - } - - let image = request( - "GET", - &format!("{proxy}/attachments/1/3/image.png?width=96&height=96&format=webp&quality=high"), - None, - None, - ) - .await?; - assert_status(&image, 200)?; - assert_header_prefix(&image, "content-type", "image/webp")?; - assert_webp(&image.body)?; - - let avatar = request( - "GET", - &format!("{proxy}/avatars/42/abc.png?size=128&format=webp"), - None, - None, - ) - .await?; - assert_status(&avatar, 200)?; - assert_header_prefix(&avatar, "content-type", "image/webp")?; - assert_webp(&avatar.body)?; - - let video = request( - "GET", - &format!("{proxy}/attachments/1/4/video.mp4?format=jpeg"), - None, - None, - ) - .await?; - assert_status(&video, 200)?; - assert_header_prefix(&video, "content-type", "image/jpeg")?; - assert_jpeg(&video.body)?; - - let metadata = post_json( - &format!("{proxy}/_metadata"), - serde_json::json!({"version": 2, "type": "upload", "nsfw": "allow", "upload_filename": "upload-image.png", "filename": "upload-image.png"}), - ) - .await?; - if metadata - .get("content_type") - .and_then(|value| value.as_str()) - != Some("image/png") - || metadata.get("width").and_then(|value| value.as_i64()) != Some(320) - || metadata.get("height").and_then(|value| value.as_i64()) != Some(240) - { - bail!("unexpected upload metadata response: {metadata}"); - } - - let s3_metadata = post_json( - &format!("{proxy}/_metadata"), - serde_json::json!({"version": 2, "type": "s3", "nsfw": "allow", "bucket": "cdn", "key": "metadata/image.png"}), - ) - .await?; - if s3_metadata - .get("content_type") - .and_then(|value| value.as_str()) - != Some("image/png") - { - bail!("unexpected S3 metadata response: {s3_metadata}"); - } - - let thumbnail = request( - "POST", - &format!("{proxy}/_thumbnail"), - Some(vec![ - ("Authorization", "Bearer integration-secret"), - ("Content-Type", "application/json"), - ]), - Some( - serde_json::json!({"upload_filename": "upload-video.mp4"}) - .to_string() - .into_bytes(), - ), - ) - .await?; - assert_status(&thumbnail, 200)?; - assert_header_prefix(&thumbnail, "content-type", "image/webp")?; - assert_webp(&thumbnail.body)?; - - let frames = post_json( - &format!("{proxy}/_frames"), - serde_json::json!({"version": 2, "type": "upload", "nsfw": "allow", "upload_filename": "upload-video.mp4"}), - ) - .await?; - let has_jpeg = frames - .get("frames") - .and_then(|value| value.as_array()) - .map(|frames| { - frames.iter().any(|frame| { - frame.get("mime_type").and_then(|value| value.as_str()) == Some("image/jpeg") - }) - }) - .unwrap_or(false); - if !has_jpeg { - bail!("unexpected frames response: {frames}"); - } - Ok(()) -} - -async fn post_json(url: &str, payload: serde_json::Value) -> Result { - let response = request( - "POST", - url, - Some(vec![ - ("Authorization", "Bearer integration-secret"), - ("Content-Type", "application/json"), - ]), - Some(payload.to_string().into_bytes()), - ) - .await?; - assert_status(&response, 200)?; - Ok(serde_json::from_slice(&response.body)?) -} - -async fn request( - method: &str, - url: &str, - headers: Option>, - data: Option>, -) -> Result { +async fn request_status(url: &str) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build()?; - let mut builder = client.request(Method::from_bytes(method.as_bytes())?, url); - for (name, value) in headers.unwrap_or_default() { - builder = builder.header(name, value); - } - if let Some(data) = data { - builder = builder.body(data); - } - let response = builder.send().await?; - let status = response.status().as_u16(); - let headers = response - .headers() - .iter() - .map(|(name, value)| { - ( - name.as_str().to_ascii_lowercase(), - value.to_str().unwrap_or_default().to_owned(), - ) - }) - .collect(); - let body = response.bytes().await?.to_vec(); - Ok(HttpResponse { - status, - headers, - body, - }) -} - -fn assert_status(response: &HttpResponse, expected: u16) -> Result<()> { - if response.status != expected { - bail!( - "expected HTTP {expected}, got {}: {:?}", - response.status, - &response.body[..response.body.len().min(500)] - ); - } - Ok(()) -} - -fn assert_header_prefix(response: &HttpResponse, name: &str, prefix: &str) -> Result<()> { - let value = response.headers.get(name).map(String::as_str).unwrap_or(""); - if !value - .to_ascii_lowercase() - .starts_with(&prefix.to_ascii_lowercase()) - { - bail!("expected {name} to start with {prefix:?}, got {value:?}"); - } - Ok(()) -} - -fn assert_webp(body: &[u8]) -> Result<()> { - if body.len() < 12 || !body.starts_with(b"RIFF") || &body[8..12] != b"WEBP" { - bail!("response is not a WebP image"); - } - Ok(()) -} - -fn assert_jpeg(body: &[u8]) -> Result<()> { - if !body.starts_with(&[0xff, 0xd8]) { - bail!("response is not a JPEG image"); - } - Ok(()) -} - -const BEE_GIF_OBJECT_KEY: &str = "attachments/bench/gifs/animated-policy-fixture.gif"; - -struct BeeGifBenchConfig { - cache_dir: PathBuf, - fixture_url: String, - port: u16, - max_seconds: f64, - target_width: u32, - build: bool, - full_transcode: bool, -} - -impl BeeGifBenchConfig { - fn from_env() -> Self { - let media_root = ROOT.join("fluxer_media_proxy"); - Self { - cache_dir: env_path("BENCH_CACHE_DIR", media_root.join(".benchmark-cache/media")), - fixture_url: env::var("BEE_GIF_URL") - .unwrap_or_else(|_| "https://www.gstatic.com/webp/animated/1.gif".to_owned()), - port: env_parse("PORT", 18182), - max_seconds: env_parse("MAX_SECONDS", 2.0), - target_width: env_parse("TARGET_WIDTH", 60), - build: env_bool("BENCH_BUILD", true), - full_transcode: env_bool("BENCH_FULL_TRANSCODE", false), - } - } -} - -struct BeeGifBenchContext { - base_url: String, - fixture_sha: String, - fixture_bytes: usize, - fixture_frames: Option, - max_seconds: f64, - work_dir: PathBuf, -} - -async fn ensure_bee_gif_fixture(path: &Path, url: &str) -> Result<()> { - if path.is_file() && fs::metadata(path)?.len() > 0 { - return Ok(()); - } - eprintln!("[bee-gif-bench] downloading fixture"); - let tmp = path.with_extension("gif.tmp"); - let response = reqwest::get(url).await?.error_for_status()?; - let bytes = response.bytes().await?; - fs::write(&tmp, &bytes)?; - fs::rename(tmp, path)?; - Ok(()) -} - -async fn measure_original(ctx: &BeeGifBenchContext, label: &str, query: &str) -> Result<()> { - let result = fetch_bench_case(ctx, label, "gif", query).await?; - println!("{}", result.summary(label)); - if result.status != 200 - || !content_type_matches(&result.content_type, "image/gif") - || result.bytes != ctx.fixture_bytes - || result.sha256 != ctx.fixture_sha - { - bail!("{label}: response changed the GIF bytes"); - } - assert_elapsed(label, result.seconds, ctx.max_seconds) -} - -async fn measure_gif_transform( - ctx: &BeeGifBenchContext, - label: &str, - query: &str, - expected_screen: &str, -) -> Result<()> { - let result = fetch_bench_case(ctx, label, "gif", query).await?; - println!("{}", result.summary(label)); - if result.status != 200 - || !content_type_matches(&result.content_type, "image/gif") - || result.bytes == 0 - || result.sha256 == ctx.fixture_sha - { - bail!("{label}: expected a transformed GIF response"); - } - if let Some(expected_frames) = ctx.fixture_frames { - let info = gifsicle_info(&result.output_file) - .ok_or_else(|| anyhow::anyhow!("{label}: gifsicle could not inspect output"))?; - let frames = parse_gifsicle_frame_count(&info); - let screen = parse_gifsicle_logical_screen(&info); - if frames != Some(expected_frames) || screen.as_deref() != Some(expected_screen) { - bail!( - "{label}: expected {expected_frames} frames at {expected_screen}, got {} at {}", - frames - .map(|value| value.to_string()) - .unwrap_or_else(|| "unknown".to_owned()), - screen.unwrap_or_else(|| "unknown".to_owned()) - ); - } - } - assert_elapsed(label, result.seconds, ctx.max_seconds) -} - -async fn measure_static_webp(ctx: &BeeGifBenchContext, label: &str, query: &str) -> Result<()> { - let result = fetch_bench_case(ctx, label, "webp", query).await?; - println!("{}", result.summary(label)); - if result.status != 200 - || !content_type_matches(&result.content_type, "image/webp") - || result.bytes == 0 - { - bail!("{label}: expected a static WebP response"); - } - assert_elapsed(label, result.seconds, ctx.max_seconds) -} - -async fn measure_animated_webp(ctx: &BeeGifBenchContext, label: &str, query: &str) -> Result<()> { - let result = fetch_bench_case(ctx, label, "webp", query).await?; - println!("{}", result.summary(label)); - if result.status != 200 - || !content_type_matches(&result.content_type, "image/webp") - || result.bytes == 0 - { - bail!("{label}: expected an animated WebP response"); - } - if !is_animated_webp(&result.body) { - bail!("{label}: expected a RIFF animated WebP payload"); - } - assert_elapsed(label, result.seconds, ctx.max_seconds) -} - -struct BenchFetchResult { - status: u16, - seconds: f64, - bytes: usize, - content_type: String, - sha256: String, - output_file: PathBuf, - body: Vec, -} - -impl BenchFetchResult { - fn summary(&self, label: &str) -> String { - format!( - "{label} status={} seconds={:.6} bytes={} content_type={} sha256={}", - self.status, self.seconds, self.bytes, self.content_type, self.sha256 - ) - } -} - -async fn fetch_bench_case( - ctx: &BeeGifBenchContext, - label: &str, - extension: &str, - query: &str, -) -> Result { - let url = format!("{}/{BEE_GIF_OBJECT_KEY}{query}", ctx.base_url); - let output_file = ctx.work_dir.join(format!("{label}.{extension}")); - let start = Instant::now(); - let response = request("GET", &url, None, None).await?; - let seconds = start.elapsed().as_secs_f64(); - fs::write(&output_file, &response.body)?; - let content_type = response - .headers - .get("content-type") - .cloned() - .unwrap_or_default(); - let sha256 = sha256_hex(&response.body); - Ok(BenchFetchResult { - status: response.status, - seconds, - bytes: response.body.len(), - content_type, - sha256, - output_file, - body: response.body, - }) -} - -fn assert_elapsed(label: &str, seconds: f64, max_seconds: f64) -> Result<()> { - if seconds > max_seconds { - bail!("{label}: {seconds:.6}s exceeded {max_seconds:.6}s budget"); - } - Ok(()) -} - -fn content_type_matches(actual: &str, expected: &str) -> bool { - actual - .to_ascii_lowercase() - .starts_with(&expected.to_ascii_lowercase()) -} - -fn sha256_hex(bytes: &[u8]) -> String { - bytes_to_lower_hex(&Sha256::digest(bytes)) -} - -fn bytes_to_lower_hex(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len() * 2); - for byte in bytes { - out.push(HEX[(byte >> 4) as usize] as char); - out.push(HEX[(byte & 0x0f) as usize] as char); - } - out -} - -fn scaled_height(width: u32, height: u32, target_width: u32) -> u32 { - ((height as f64 * target_width as f64 / width as f64) + 0.5).floor() as u32 -} - -fn is_animated_webp(body: &[u8]) -> bool { - body.starts_with(b"RIFF") && body.windows(4).any(|window| window == b"ANMF") -} - -fn gifsicle_info(path: &Path) -> Option { - which("gifsicle")?; - let output = Command::new("gifsicle") - .arg("--info") - .arg(path) - .output() - .ok()?; - if !output.status.success() { - return None; - } - Some(String::from_utf8_lossy(&output.stdout).into_owned()) -} - -fn parse_gifsicle_frame_count(info: &str) -> Option { - let first = info.lines().next()?; - let words: Vec<_> = first.split_whitespace().collect(); - words - .windows(2) - .find(|window| window[1] == "images") - .and_then(|window| window[0].parse().ok()) -} - -fn parse_gifsicle_logical_screen(info: &str) -> Option { - info.lines() - .find(|line| line.contains("logical screen")) - .and_then(|line| { - let words: Vec<_> = line.split_whitespace().collect(); - words - .windows(3) - .find(|window| window[0] == "logical" && window[1] == "screen") - .map(|window| window[2].to_owned()) - }) -} - -fn env_path(name: &str, default: PathBuf) -> PathBuf { - env::var_os(name).map(PathBuf::from).unwrap_or(default) -} - -fn env_parse(name: &str, default: T) -> T -where - T: std::str::FromStr, -{ - env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(default) -} - -fn env_bool(name: &str, default: bool) -> bool { - env::var(name) - .map(|value| value == "1" || value.eq_ignore_ascii_case("true")) - .unwrap_or(default) -} - -struct ChildGuard { - child: Option, -} - -impl ChildGuard { - fn new(child: Child) -> Self { - Self { child: Some(child) } - } - - fn stop(&mut self) { - if let Some(mut child) = self.child.take() { - let _ = child.kill(); - let _ = child.wait(); - } - } -} - -impl Drop for ChildGuard { - fn drop(&mut self) { - self.stop(); - } + Ok(client.get(url).send().await?.status().as_u16()) } async fn wait_for_success(url: &str, timeout_secs: u64) -> Result<()> { let deadline = Instant::now() + Duration::from_secs(timeout_secs); let mut last_error = None; while Instant::now() < deadline { - match request("GET", url, None, None).await { - Ok(response) if (200..300).contains(&response.status) => return Ok(()), - Ok(response) => last_error = Some(format!("HTTP {}", response.status)), - Err(error) => last_error = Some(error.to_string()), - } - sleep(Duration::from_millis(250)).await; - } - bail!( - "Timed out waiting for {url}: {}", - last_error.unwrap_or_else(|| "unknown error".to_owned()) - ); -} - -async fn wait_for_http_endpoint(url: &str, timeout_secs: u64) -> Result<()> { - let deadline = Instant::now() + Duration::from_secs(timeout_secs); - let mut last_error = None; - while Instant::now() < deadline { - match request("GET", url, None, None).await { - Ok(response) if response.status < 500 => return Ok(()), - Ok(response) => last_error = Some(format!("HTTP {}", response.status)), + match request_status(url).await { + Ok(status) if (200..300).contains(&status) => return Ok(()), + Ok(status) => last_error = Some(format!("HTTP {status}")), Err(error) => last_error = Some(error.to_string()), } sleep(Duration::from_millis(250)).await; @@ -1204,88 +263,3 @@ fn tcp_reachable(host: &str, port: u16) -> bool { ) .is_ok() } - -pub fn find_free_port(host: &str) -> Result { - let listener = TcpListener::bind((host, 0))?; - Ok(listener.local_addr()?.port()) -} - -fn require_command(name: &str) -> Result<()> { - if which(name).is_none() { - bail!("missing required command: {name}"); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn media_proxy_env_maps_store_to_fluxer_env() { - let store = ObjectStore { - endpoint: "http://s3".to_owned(), - access_key_id: "ak".to_owned(), - secret_access_key: "sk".to_owned(), - region: "r".to_owned(), - container_name: None, - }; - let env = media_proxy_env(&store); - assert!(env.iter().any( - |(key, value)| key == "FLUXER_S3_ENDPOINT" && value.as_deref() == Some("http://s3") - )); - assert!( - env.iter().any( - |(key, value)| key == "FLUXER_S3_BUCKET_CDN" && value.as_deref() == Some("cdn") - ) - ); - } - - #[test] - fn validates_image_magic_bytes() { - assert!(assert_webp(b"RIFFxxxxWEBP").is_ok()); - assert!(assert_jpeg(&[0xff, 0xd8, 0x00]).is_ok()); - assert!(assert_webp(b"nope").is_err()); - assert!(assert_jpeg(b"nope").is_err()); - } - - #[test] - fn detects_animated_webp_markers() { - assert!(is_animated_webp(b"RIFFxxxxWEBPVP8X....ANMF")); - assert!(!is_animated_webp(b"RIFFxxxxWEBPVP8 ")); - assert!(!is_animated_webp(b"not-webp-ANMF")); - } - - #[test] - fn parses_gifsicle_summary() { - let info = "* test.gif 12 images\n logical screen 60x45\n"; - assert_eq!(parse_gifsicle_frame_count(info), Some(12)); - assert_eq!( - parse_gifsicle_logical_screen(info).as_deref(), - Some("60x45") - ); - assert_eq!(parse_gifsicle_frame_count("no frames"), None); - } - - #[test] - fn computes_scaled_height_like_benchmark_script() { - assert_eq!(scaled_height(320, 240, 60), 45); - assert_eq!(scaled_height(100, 33, 10), 3); - } - - #[test] - fn matches_content_type_prefixes() { - assert!(content_type_matches("image/gif", "image/gif")); - assert!(content_type_matches( - "image/webp; charset=binary", - "image/webp" - )); - assert!(!content_type_matches("image/png", "image/webp")); - } - - #[test] - fn finds_free_port() { - let port = find_free_port("127.0.0.1").unwrap(); - assert!(port > 0); - } -} diff --git a/tools/dev/src/media_stress.rs b/tools/dev/src/media_stress.rs deleted file mode 100644 index eb7541d7a..000000000 --- a/tools/dev/src/media_stress.rs +++ /dev/null @@ -1,801 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use anyhow::{Context, Result, bail}; -use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; -use clap::Args; -use hmac::{Hmac, KeyInit, Mac}; -use image::{AnimationDecoder, RgbaImage, codecs::gif::GifDecoder, imageops::FilterType}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use std::collections::BTreeMap; -use std::io::{BufReader, Cursor}; -use std::process::Command; -use std::time::{Duration, Instant}; -use tokio::task::JoinSet; - -const DEFAULT_PREFIXES: &[&str] = &[ - "attachments/", - "emojis/", - "stickers/", - "avatars/", - "icons/", - "banners/", -]; -const KNOWN_EXTS: &[&str] = &[ - "png", "jpg", "jpeg", "webp", "gif", "apng", "avif", "heic", "heif", "jxl", -]; - -#[derive(Debug, Clone, Args)] -pub struct StressCompareArgs { - #[arg(long, help = "v1 base URL, for example http://media-proxy:8080")] - pub v1: String, - #[arg(long, help = "v2 base URL, for example http://10.244.147.209:8080")] - pub v2: String, - #[arg(long, default_value = "fluxer")] - pub bucket: String, - #[arg( - long, - env = "FLUXER_S3_ENDPOINT", - default_value = "https://ewr1.vultrobjects.com" - )] - pub endpoint: String, - #[arg(long, env = "FLUXER_S3_REGION", default_value = "ewr1")] - pub region: String, - #[arg(long, env = "FLUXER_S3_ACCESS_KEY_ID")] - pub access_key: Option, - #[arg(long, env = "FLUXER_S3_SECRET_ACCESS_KEY")] - pub secret_key: Option, - #[arg(long, action = clap::ArgAction::Append)] - pub prefix: Vec, - #[arg(long, default_value_t = 20)] - pub per_prefix: usize, - #[arg(long, default_value = "plain,resize,format,resize+format")] - pub matrix: String, - #[arg(long, default_value_t = 256)] - pub size: u32, - #[arg(long, default_value = "webp")] - pub format: String, - #[arg(long, default_value_t = 8)] - pub concurrency: usize, - #[arg(long, default_value_t = 30.0)] - pub timeout: f64, - #[arg(long, default_value_t = 42)] - pub seed: u64, - #[arg(long, default_value = "-", help = "JSON report path, or - for stdout")] - pub report: String, - #[arg(long, default_value_t = 40)] - pub max_issues_print: usize, -} - -#[derive(Debug, Clone, Args)] -pub struct SignExternalUrlArgs { - #[arg(long)] - pub secret_key: String, - #[arg(long)] - pub server_url: String, - pub upstream: String, -} - -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -pub struct StressCase { - pub label: String, - pub url_path: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub query: String, - pub expect_image: bool, -} - -#[derive(Debug, Clone, Serialize, PartialEq)] -pub struct StressResult { - pub case: StressCase, - pub v1_status: i32, - pub v2_status: i32, - pub v1_ct: String, - pub v2_ct: String, - pub v1_size: usize, - pub v2_size: usize, - pub issues: Vec, - pub elapsed_v1_ms: f64, - pub elapsed_v2_ms: f64, -} - -#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] -pub struct PrefixSummary { - pub ok: u64, - pub fail: u64, -} - -#[derive(Debug, Clone, Serialize, PartialEq)] -pub struct StressSummary { - pub total: usize, - pub issues: usize, - pub by_prefix: BTreeMap, - pub issues_sample: Vec, -} - -#[derive(Debug)] -struct FetchResult { - status: i32, - content_type: String, - body: Vec, - elapsed_ms: f64, -} - -#[derive(Debug, Deserialize)] -struct AwsListObjects { - #[serde(rename = "Contents", default)] - contents: Vec, - #[serde(rename = "IsTruncated", default)] - is_truncated: bool, - #[serde(rename = "NextContinuationToken")] - next_continuation_token: Option, -} - -#[derive(Debug, Deserialize)] -struct AwsObject { - #[serde(rename = "Key")] - key: String, -} - -#[derive(Debug, Clone)] -struct StableRng(u64); - -impl StableRng { - fn new(seed: u64) -> Self { - Self(seed) - } - - fn next_below(&mut self, upper: usize) -> usize { - self.0 = self - .0 - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - ((self.0 >> 32) as usize) % upper - } -} - -pub async fn run_stress_compare(args: StressCompareArgs) -> Result { - let access_key = args - .access_key - .as_deref() - .filter(|value| !value.is_empty()) - .context("missing S3 creds: pass --access-key or set FLUXER_S3_ACCESS_KEY_ID")?; - let secret_key = args - .secret_key - .as_deref() - .filter(|value| !value.is_empty()) - .context("missing S3 creds: pass --secret-key or set FLUXER_S3_SECRET_ACCESS_KEY")?; - - let prefixes = if args.prefix.is_empty() { - DEFAULT_PREFIXES - .iter() - .map(|value| (*value).to_owned()) - .collect() - } else { - args.prefix.clone() - }; - let mut rng = StableRng::new(args.seed); - let mut cases = Vec::new(); - for prefix in &prefixes { - let keys = list_random_keys( - &RandomKeyListRequest { - endpoint: &args.endpoint, - access_key, - secret_key, - region: &args.region, - bucket: &args.bucket, - prefix, - want: args.per_prefix, - }, - &mut rng, - )?; - for key in keys { - cases.extend(cases_for_key(&key, &args.matrix, args.size, &args.format)); - } - } - eprintln!("# {} cases across {} prefixes", cases.len(), prefixes.len()); - - let results = compare_cases( - cases, - &args.v1, - &args.v2, - args.timeout, - args.concurrency.max(1), - ) - .await?; - let summary = summarize_results(&results, args.max_issues_print); - let output = serde_json::to_string_pretty(&summary)?; - if args.report == "-" { - println!("{output}"); - } else { - std::fs::write(&args.report, output) - .with_context(|| format!("failed to write {}", args.report))?; - } - Ok(summary.issues.min(255) as i32) -} - -pub fn sign_external_url(secret_key: &str, server_url: &str, upstream: &str) -> Result { - let path = format!("v2/{}", URL_SAFE_NO_PAD.encode(upstream.as_bytes())); - let mut mac = Hmac::::new_from_slice(secret_key.as_bytes()) - .context("failed to create HMAC signer")?; - mac.update(path.as_bytes()); - let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); - Ok(format!( - "{}/external/{}/{}", - server_url.trim_end_matches('/'), - signature, - path - )) -} - -struct RandomKeyListRequest<'a> { - endpoint: &'a str, - access_key: &'a str, - secret_key: &'a str, - region: &'a str, - bucket: &'a str, - prefix: &'a str, - want: usize, -} - -fn list_random_keys( - request: &RandomKeyListRequest<'_>, - rng: &mut StableRng, -) -> Result> { - if request.want == 0 { - return Ok(Vec::new()); - } - let mut keys = Vec::new(); - let mut seen = 0usize; - let mut continuation_token = None; - let scan_limit = request.want.saturating_mul(50).max(5000); - - loop { - let page = list_objects_page( - request.endpoint, - request.access_key, - request.secret_key, - request.region, - request.bucket, - request.prefix, - continuation_token.as_deref(), - )?; - for object in page.contents { - seen += 1; - if keys.len() < request.want { - keys.push(object.key); - } else { - let index = rng.next_below(seen); - if index < request.want { - keys[index] = object.key; - } - } - } - if seen >= scan_limit || !page.is_truncated { - break; - } - continuation_token = page.next_continuation_token; - if continuation_token.is_none() { - break; - } - } - - Ok(keys) -} - -fn list_objects_page( - endpoint: &str, - access_key: &str, - secret_key: &str, - region: &str, - bucket: &str, - prefix: &str, - continuation_token: Option<&str>, -) -> Result { - let mut command = Command::new("aws"); - command - .arg("--no-cli-pager") - .arg("--endpoint-url") - .arg(endpoint) - .arg("--region") - .arg(region) - .arg("s3api") - .arg("list-objects-v2") - .arg("--bucket") - .arg(bucket) - .arg("--prefix") - .arg(prefix) - .arg("--max-keys") - .arg("1000") - .arg("--output") - .arg("json") - .env("AWS_ACCESS_KEY_ID", access_key) - .env("AWS_SECRET_ACCESS_KEY", secret_key) - .env("AWS_DEFAULT_REGION", region) - .env("AWS_EC2_METADATA_DISABLED", "true"); - if let Some(token) = continuation_token { - command.arg("--continuation-token").arg(token); - } - let output = command.output().context("failed to run aws s3api")?; - if !output.status.success() { - bail!( - "aws s3api list-objects-v2 failed: {}", - String::from_utf8_lossy(&output.stderr).trim() - ); - } - serde_json::from_slice(&output.stdout).context("failed to parse aws s3api JSON") -} - -pub fn cases_for_key(key: &str, matrix: &str, size: u32, format: &str) -> Vec { - let Some(url_path) = url_for_key(key) else { - return Vec::new(); - }; - let is_attachment = key.starts_with("attachments/"); - let mut cases = Vec::new(); - for variant in matrix.split(',').map(str::trim).filter(|v| !v.is_empty()) { - match variant { - "plain" => cases.push(StressCase { - label: format!("{key}|plain"), - url_path: url_path.clone(), - query: String::new(), - expect_image: true, - }), - "resize" => cases.push(StressCase { - label: format!("{key}|resize{size}"), - url_path: url_path.clone(), - query: resize_query(is_attachment, size), - expect_image: true, - }), - "format" => cases.push(StressCase { - label: format!("{key}|fmt={format}"), - url_path: url_path.clone(), - query: format!("format={format}"), - expect_image: true, - }), - "resize+format" if is_attachment => cases.push(StressCase { - label: format!("{key}|w{size}+{format}"), - url_path: url_path.clone(), - query: format!("format={format}&width={size}&height={size}"), - expect_image: true, - }), - "resize+format" => cases.push(StressCase { - label: format!("{key}|s{size}+{format}"), - url_path: url_path.clone(), - query: format!("size={size}&format={format}"), - expect_image: true, - }), - _ => {} - } - } - cases -} - -pub fn url_for_key(key: &str) -> Option { - let parts = key.split('/').collect::>(); - let prefix = parts.first().copied()?; - match prefix { - "emojis" | "stickers" if parts.len() == 2 => Some(format!( - "/{prefix}/{}", - quote_component(&ensure_ext(parts[1], "webp")) - )), - "attachments" if parts.len() == 4 => Some(format!( - "/attachments/{}/{}/{}", - quote_component(parts[1]), - quote_component(parts[2]), - quote_component(parts[3]) - )), - "avatars" | "icons" | "banners" | "splashes" | "embed-splashes" if parts.len() == 3 => { - Some(format!( - "/{prefix}/{}/{}", - quote_component(parts[1]), - quote_component(&ensure_ext(parts[2], "webp")) - )) - } - _ => None, - } -} - -fn ensure_ext(name: &str, default: &str) -> String { - let lower = name.to_ascii_lowercase(); - if KNOWN_EXTS - .iter() - .any(|extension| lower.ends_with(&format!(".{extension}"))) - { - name.to_owned() - } else { - format!("{name}.{default}") - } -} - -fn quote_component(value: &str) -> String { - urlencoding::encode(value).into_owned() -} - -fn resize_query(is_attachment: bool, size: u32) -> String { - if is_attachment { - format!("width={size}&height={size}") - } else { - format!("size={size}") - } -} - -async fn compare_cases( - cases: Vec, - v1_base: &str, - v2_base: &str, - timeout: f64, - concurrency: usize, -) -> Result> { - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .timeout(Duration::from_secs_f64(timeout.max(0.1))) - .build()?; - let mut pending = cases.into_iter(); - let mut join_set = JoinSet::new(); - let mut results = Vec::new(); - let mut submitted = 0usize; - - loop { - while join_set.len() < concurrency { - let Some(case) = pending.next() else { - break; - }; - let client = client.clone(); - let v1_base = v1_base.to_owned(); - let v2_base = v2_base.to_owned(); - join_set.spawn(async move { compare_one(client, case, v1_base, v2_base).await }); - submitted += 1; - } - let Some(joined) = join_set.join_next().await else { - break; - }; - let result = joined.context("stress compare worker panicked")?; - results.push(result); - if results.len() % 25 == 0 { - let issue_count = results - .iter() - .filter(|result| !result.issues.is_empty()) - .count(); - eprintln!( - "# {}/{} done, {} issues so far", - results.len(), - submitted + pending.len(), - issue_count - ); - } - } - - Ok(results) -} - -async fn compare_one( - client: reqwest::Client, - case: StressCase, - v1_base: String, - v2_base: String, -) -> StressResult { - let v1 = fetch(&client, &v1_base, &case.url_path, &case.query).await; - let v2 = fetch(&client, &v2_base, &case.url_path, &case.query).await; - let mut result = StressResult { - case, - v1_status: v1.status, - v2_status: v2.status, - v1_ct: v1.content_type, - v2_ct: v2.content_type, - v1_size: v1.body.len(), - v2_size: v2.body.len(), - issues: Vec::new(), - elapsed_v1_ms: v1.elapsed_ms, - elapsed_v2_ms: v2.elapsed_ms, - }; - - if result.v1_status != result.v2_status { - result.issues.push(format!( - "status mismatch v1={} v2={}", - result.v1_status, result.v2_status - )); - return result; - } - if result.v1_status >= 400 { - return result; - } - if !result.case.expect_image { - if v1.body != v2.body { - result.issues.push(format!( - "body diverges v1={}B v2={}B", - result.v1_size, result.v2_size - )); - } - return result; - } - - let (a_image, a_frames, a_error) = decode_image(&v1.body); - let (b_image, b_frames, b_error) = decode_image(&v2.body); - match (a_error.as_deref(), b_error.as_deref()) { - (Some(a_error), None) => result - .issues - .push(format!("v1 decode failed but v2 ok: {a_error}")), - (None, Some(b_error)) => result.issues.push(format!( - "v2 decode failed: {b_error} (v1 ok, {a_frames} frame(s))" - )), - (Some(_), Some(_)) => { - if result.v1_size.abs_diff(result.v2_size) > result.v1_size.saturating_div(4).max(256) { - result.issues.push(format!( - "both decode-failed, size differs ({} vs {})", - result.v1_size, result.v2_size - )); - } - return result; - } - (None, None) => {} - } - - if let (Some(a_image), Some(b_image)) = (a_image, b_image) { - if a_frames != b_frames { - result - .issues - .push(format!("frame count v1={a_frames} v2={b_frames}")); - } - let similarity = pixel_similarity(&a_image, &b_image); - if similarity > 0.08 { - result.issues.push(format!( - "pixel diff {similarity:.3} (v1 size {}x{} v2 size {}x{})", - a_image.width(), - a_image.height(), - b_image.width(), - b_image.height() - )); - } - } - result -} - -async fn fetch(client: &reqwest::Client, base: &str, path: &str, query: &str) -> FetchResult { - let url = format!( - "{}{}{}", - base.trim_end_matches('/'), - path, - if query.is_empty() { - String::new() - } else { - format!("?{query}") - } - ); - let started = Instant::now(); - match client.get(url).send().await { - Ok(response) => { - let status = response.status().as_u16() as i32; - let content_type = response - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .unwrap_or_default() - .to_owned(); - match response.bytes().await { - Ok(body) => FetchResult { - status, - content_type, - body: body.to_vec(), - elapsed_ms: started.elapsed().as_secs_f64() * 1000.0, - }, - Err(error) => FetchResult { - status: -1, - content_type: format!("", error_kind(&error)), - body: Vec::new(), - elapsed_ms: started.elapsed().as_secs_f64() * 1000.0, - }, - } - } - Err(error) => FetchResult { - status: -1, - content_type: format!("", error_kind(&error)), - body: Vec::new(), - elapsed_ms: started.elapsed().as_secs_f64() * 1000.0, - }, - } -} - -fn error_kind(error: &reqwest::Error) -> &'static str { - if error.is_timeout() { - "Timeout" - } else if error.is_connect() { - "Connect" - } else if error.is_decode() { - "Decode" - } else { - "Request" - } -} - -fn decode_image(data: &[u8]) -> (Option, usize, Option) { - if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") { - return decode_gif(data); - } - match image::load_from_memory(data) { - Ok(image) => (Some(image.to_rgba8()), 1, None), - Err(error) => (None, 0, Some(error.to_string())), - } -} - -fn decode_gif(data: &[u8]) -> (Option, usize, Option) { - let reader = BufReader::new(Cursor::new(data)); - let decoder = match GifDecoder::new(reader) { - Ok(decoder) => decoder, - Err(error) => return (None, 0, Some(error.to_string())), - }; - match decoder.into_frames().collect_frames() { - Ok(frames) => { - let first = frames.first().map(|frame| frame.buffer().clone()); - (first, frames.len(), None) - } - Err(error) => (None, 0, Some(error.to_string())), - } -} - -fn pixel_similarity(a: &RgbaImage, b: &RgbaImage) -> f64 { - let b_resized; - let b = if a.dimensions() == b.dimensions() { - b - } else { - let width_diff = a.width().abs_diff(b.width()); - let height_diff = a.height().abs_diff(b.height()); - if width_diff > 8 || height_diff > 8 { - return 1.0; - } - b_resized = image::imageops::resize(b, a.width(), a.height(), FilterType::Lanczos3); - &b_resized - }; - let sum = a - .as_raw() - .iter() - .zip(b.as_raw()) - .map(|(a, b)| (*a as i32 - *b as i32).unsigned_abs() as u64) - .sum::(); - sum as f64 / (a.as_raw().len() as f64 * 255.0) -} - -pub fn summarize_results(results: &[StressResult], max_issues_print: usize) -> StressSummary { - let mut by_prefix = BTreeMap::new(); - for result in results { - let prefix = result - .case - .url_path - .split('/') - .nth(1) - .unwrap_or("") - .to_owned(); - let entry = by_prefix - .entry(prefix) - .or_insert(PrefixSummary { ok: 0, fail: 0 }); - if result.issues.is_empty() { - entry.ok += 1; - } else { - entry.fail += 1; - } - } - let issues_sample = results - .iter() - .filter(|result| !result.issues.is_empty()) - .take(max_issues_print) - .cloned() - .collect::>(); - StressSummary { - total: results.len(), - issues: results - .iter() - .filter(|result| !result.issues.is_empty()) - .count(), - by_prefix, - issues_sample, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use base64::engine::general_purpose::URL_SAFE_NO_PAD; - - #[test] - fn maps_s3_keys_to_public_routes() { - assert_eq!( - url_for_key("avatars/42/hash"), - Some("/avatars/42/hash.webp".to_owned()) - ); - assert_eq!( - url_for_key("avatars/42/hash.avif"), - Some("/avatars/42/hash.avif".to_owned()) - ); - assert_eq!( - url_for_key("emojis/123"), - Some("/emojis/123.webp".to_owned()) - ); - assert_eq!( - url_for_key("attachments/a b/c/name#.png"), - Some("/attachments/a%20b/c/name%23.png".to_owned()) - ); - assert_eq!(url_for_key("unknown/1/2"), None); - } - - #[test] - fn builds_transform_matrix_for_attachment_and_avatar() { - let attachment = cases_for_key( - "attachments/a/b/photo.png", - "plain,resize,format,resize+format", - 128, - "webp", - ); - assert_eq!(attachment.len(), 4); - assert_eq!(attachment[1].query, "width=128&height=128"); - assert_eq!(attachment[3].query, "format=webp&width=128&height=128"); - - let avatar = cases_for_key("avatars/42/hash", "resize,resize+format", 64, "webp"); - assert_eq!(avatar[0].query, "size=64"); - assert_eq!(avatar[1].query, "size=64&format=webp"); - } - - #[test] - fn signs_external_urls_with_urlsafe_components() { - let signed = sign_external_url( - "benchmark-secret", - "http://127.0.0.1:19110/", - "https://example.test/a b.jpg", - ) - .unwrap(); - let parts = signed.split('/').collect::>(); - assert!(signed.starts_with("http://127.0.0.1:19110/external/")); - assert_eq!(parts[5], "v2"); - assert_eq!( - URL_SAFE_NO_PAD.decode(parts[6]).unwrap(), - b"https://example.test/a b.jpg" - ); - assert_eq!(URL_SAFE_NO_PAD.decode(parts[4]).unwrap().len(), 32); - } - - #[test] - fn summarizes_results_by_prefix() { - let results = vec![ - result_for("/attachments/a/b/c.png", vec![]), - result_for("/attachments/a/b/d.png", vec!["bad".to_owned()]), - result_for("/avatars/1/a.webp", vec![]), - ]; - let summary = summarize_results(&results, 1); - - assert_eq!(summary.total, 3); - assert_eq!(summary.issues, 1); - assert_eq!( - summary.by_prefix.get("attachments"), - Some(&PrefixSummary { ok: 1, fail: 1 }) - ); - assert_eq!(summary.issues_sample.len(), 1); - } - - #[test] - fn pixel_similarity_detects_equal_and_different_images() { - let image_a = RgbaImage::from_pixel(2, 2, image::Rgba([0, 0, 0, 255])); - let image_b = RgbaImage::from_pixel(2, 2, image::Rgba([0, 0, 0, 255])); - let image_c = RgbaImage::from_pixel(2, 2, image::Rgba([255, 255, 255, 255])); - - assert_eq!(pixel_similarity(&image_a, &image_b), 0.0); - assert!(pixel_similarity(&image_a, &image_c) > 0.70); - } - - fn result_for(path: &str, issues: Vec) -> StressResult { - StressResult { - case: StressCase { - label: path.to_owned(), - url_path: path.to_owned(), - query: String::new(), - expect_image: true, - }, - v1_status: 200, - v2_status: 200, - v1_ct: "image/png".to_owned(), - v2_ct: "image/png".to_owned(), - v1_size: 10, - v2_size: 10, - issues, - elapsed_v1_ms: 1.0, - elapsed_v2_ms: 1.0, - } - } -} diff --git a/tools/dev/src/native_voice_it.rs b/tools/dev/src/native_voice_it.rs deleted file mode 100644 index ddd9f4bb6..000000000 --- a/tools/dev/src/native_voice_it.rs +++ /dev/null @@ -1,1711 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::paths::ROOT; -use crate::proc::{RunOptions, run_command, wait_http}; -use anyhow::{Context, Result, anyhow, bail}; -use clap::Args; -use futures_util::{SinkExt, StreamExt}; -use reqwest::{Client, Method, StatusCode}; -use serde_json::{Value, json}; -use std::collections::VecDeque; -use std::path::PathBuf; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::net::TcpStream; -use tokio::time::{sleep, timeout as tokio_timeout}; -use tokio_tungstenite::tungstenite::Message; -use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async}; - -const DEFAULT_API_URL: &str = "http://127.0.0.1:8088/api"; -const DEFAULT_GATEWAY_URL: &str = "ws://127.0.0.1:8088/gateway?v=1&encoding=json"; -const HAS_SESSION_STARTED: &str = "549755813888"; -const DEFAULT_PASSWORD: &str = "T3st!VoiceIntegration12345"; -const DEFAULT_NATIVE_SCREEN_CODECS: &str = "vp8,h264"; - -#[derive(Debug, Args)] -pub struct NativeVoiceItArgs { - #[arg(long, env = "FLUXER_NATIVE_VOICE_IT_API_URL", default_value = DEFAULT_API_URL)] - api_url: String, - #[arg( - long, - env = "FLUXER_NATIVE_VOICE_IT_GATEWAY_URL", - default_value = DEFAULT_GATEWAY_URL - )] - gateway_url: String, - #[arg(long, env = "FLUXER_NATIVE_VOICE_IT_LIVEKIT_URL")] - livekit_url: Option, - #[arg(long, default_value_t = 60)] - wait_secs: u64, - #[arg(long)] - reset: bool, - #[arg(long)] - skip_native_media: bool, - #[arg(long, env = "FLUXER_NATIVE_VOICE_IT_SCREEN_CODECS", default_value = DEFAULT_NATIVE_SCREEN_CODECS)] - native_screen_codecs: String, - #[arg(long)] - native_strict: bool, - #[arg(long)] - native_duration_ms: Option, - #[arg(long, default_value_t = 16)] - stress_iterations: u32, -} - -#[derive(Debug, Clone)] -struct Config { - api_url: String, - gateway_url: String, - livekit_url: Option, - wait: Duration, - reset: bool, - skip_native_media: bool, - native_screen_codecs: String, - native_strict: bool, - native_duration_ms: Option, - stress_iterations: u32, - test_harness_token: Option, -} - -#[derive(Debug, Clone)] -struct TestAccount { - user_id: String, - token: String, - username: String, -} - -#[derive(Debug, Clone)] -struct VoiceConnection { - connection_id: String, - identity: String, - token: String, - endpoint: String, - version: u64, -} - -#[derive(Debug)] -struct TestWorld { - accounts: Vec, - guild_id: String, - voice_channel_id: String, - dm_channel_id: String, - group_dm_channel_id: String, -} - -#[derive(Debug)] -struct Api { - client: Client, - base_url: String, - test_harness_token: Option, - client_ip: String, -} - -type GatewayWs = WebSocketStream>; - -#[derive(Debug)] -struct GatewayClient { - name: String, - ws: GatewayWs, - queued_dispatches: VecDeque, -} - -#[derive(Debug)] -struct GatewayRolloutGuard { - admin_token: String, - previous_rollout: Value, -} - -pub async fn run(args: NativeVoiceItArgs) -> Result<()> { - let config = Config::from_args(args); - let api = Api::new(&config)?; - wait_http("Fluxer API", &api.url("/_health"), config.wait.as_secs()).await?; - - if config.reset { - api.request(Method::POST, "/test/reset", None, Some(json!({}))) - .await - .context("failed to reset test harness state")?; - } - - let world = create_world(&api).await?; - println!( - "Native voice integration world: guild={} voice_channel={} dm={} gdm={} users={}", - world.guild_id, - world.voice_channel_id, - world.dm_channel_id, - world.group_dm_channel_id, - world.accounts.len() - ); - - let rollout_guard = disable_gateway_voice_reconciliation_for_it(&api, &world.accounts[0]) - .await - .context("failed to disable gateway voice reconciliation for native voice integration")?; - let result = run_with_reconciliation_guard(&api, &config, &world).await; - let restore_result = restore_gateway_rollout(&api, rollout_guard).await; - match (result, restore_result) { - (Ok(()), Ok(())) => Ok(()), - (Err(error), Ok(())) => Err(error), - (Ok(()), Err(restore_error)) => Err(restore_error.context( - "native voice integration passed but failed to restore gateway rollout config", - )), - (Err(error), Err(restore_error)) => { - eprintln!( - "Failed to restore gateway rollout config after native voice integration failure: {restore_error:#}" - ); - Err(error) - } - } -} - -async fn run_with_reconciliation_guard( - api: &Api, - config: &Config, - world: &TestWorld, -) -> Result<()> { - let mut gateways = connect_gateways(config, world).await?; - let mut voice_connections = join_and_confirm_all(api, config, world, &mut gateways).await?; - - run_voice_mutation_scenarios(world, &mut gateways, &mut voice_connections).await?; - run_private_call_scenarios(api, config, world, &mut gateways).await?; - - if !config.skip_native_media { - run_native_livekit_harness(config, world, &voice_connections)?; - } else { - println!("Native LiveKit media harness skipped by --skip-native-media."); - } - - run_disconnect_scenarios(world, &mut gateways, &voice_connections).await?; - println!("Native voice backend integration checks passed."); - Ok(()) -} - -async fn disable_gateway_voice_reconciliation_for_it( - api: &Api, - admin: &TestAccount, -) -> Result { - api.request( - Method::POST, - &format!("/test/users/{}/acls", admin.user_id), - None, - Some(json!({ - "acls": [ - "admin:authenticate", - "instance:config:update", - "instance:config:view", - ], - })), - ) - .await - .context("failed to grant native voice integration instance-config ACLs")?; - - let current = api - .request( - Method::POST, - "/admin/instance-config/get", - Some(&admin.token), - None, - ) - .await - .context("failed to read gateway rollout config")?; - let previous_rollout = current - .get("gateway_rollout") - .filter(|value| value.is_object()) - .cloned() - .ok_or_else(|| anyhow!("admin instance-config response did not include gateway_rollout"))?; - let mut disabled_rollout = previous_rollout.clone(); - let disabled_rollout_object = disabled_rollout - .as_object_mut() - .ok_or_else(|| anyhow!("gateway_rollout was not an object"))?; - disabled_rollout_object.insert( - "voice_reconciliation_v3_percentage".to_owned(), - Value::from(0), - ); - api.request( - Method::POST, - "/admin/instance-config/update", - Some(&admin.token), - Some(json!({"gateway_rollout": disabled_rollout})), - ) - .await - .context("failed to disable gateway voice reconciliation v3")?; - sleep(Duration::from_millis(2500)).await; - println!("Gateway voice reconciliation v3 disabled for native voice integration."); - Ok(GatewayRolloutGuard { - admin_token: admin.token.clone(), - previous_rollout, - }) -} - -async fn restore_gateway_rollout(api: &Api, guard: GatewayRolloutGuard) -> Result<()> { - api.request( - Method::POST, - "/admin/instance-config/update", - Some(&guard.admin_token), - Some(json!({"gateway_rollout": guard.previous_rollout})), - ) - .await - .context("failed to restore previous gateway rollout config")?; - println!("Gateway rollout config restored after native voice integration."); - Ok(()) -} - -impl Config { - fn from_args(args: NativeVoiceItArgs) -> Self { - Self { - api_url: args.api_url, - gateway_url: args.gateway_url, - livekit_url: args.livekit_url, - wait: Duration::from_secs(args.wait_secs), - reset: args.reset, - skip_native_media: args.skip_native_media, - native_screen_codecs: args.native_screen_codecs, - native_strict: args.native_strict, - native_duration_ms: args.native_duration_ms, - stress_iterations: args.stress_iterations, - test_harness_token: std::env::var("FLUXER_TEST_HARNESS_TOKEN") - .ok() - .filter(|value| !value.trim().is_empty()), - } - } -} - -impl Api { - fn new(config: &Config) -> Result { - Ok(Self { - client: Client::builder() - .timeout(Duration::from_secs(20)) - .build() - .context("failed to build HTTP client")?, - base_url: config.api_url.trim_end_matches('/').to_owned(), - test_harness_token: config.test_harness_token.clone(), - client_ip: unique_client_ip(), - }) - } - - fn url(&self, path: &str) -> String { - format!("{}/{}", self.base_url, path.trim_start_matches('/')) - } - - async fn request( - &self, - method: Method, - path: &str, - auth_token: Option<&str>, - body: Option, - ) -> Result { - let (status, payload) = self - .request_status(method.clone(), path, auth_token, body) - .await?; - if !status.is_success() { - bail!("HTTP {method} {path} returned {status}: {payload}"); - } - Ok(payload) - } - - async fn request_status( - &self, - method: Method, - path: &str, - auth_token: Option<&str>, - body: Option, - ) -> Result<(StatusCode, Value)> { - let mut attempt = 0; - loop { - let mut request = self.client.request(method.clone(), self.url(path)); - if let Some(token) = auth_token { - request = request.header("authorization", token); - } - if let Some(token) = &self.test_harness_token { - request = request.header("x-test-token", token); - } - request = request.header("x-forwarded-for", &self.client_ip); - if let Some(body) = body.clone() { - request = request.json(&body); - } - let response = request - .send() - .await - .with_context(|| format!("HTTP {method} {path} failed"))?; - let status = response.status(); - let text = response.text().await.unwrap_or_default(); - let payload = parse_json_or_null(&text) - .with_context(|| format!("failed to parse HTTP {method} {path}"))?; - if status != StatusCode::TOO_MANY_REQUESTS || attempt >= 12 { - return Ok((status, payload)); - } - attempt += 1; - let retry_after_ms = numeric_field(&payload, "retry_after") - .unwrap_or(1000) - .clamp(100, 5000); - sleep(Duration::from_millis(retry_after_ms)).await; - } - } -} - -impl GatewayClient { - async fn connect(name: impl Into, gateway_url: &str, token: &str) -> Result { - let name = name.into(); - let (ws, _) = connect_async(gateway_url) - .await - .with_context(|| format!("{name}: failed to connect to gateway {gateway_url}"))?; - let mut client = Self { - name, - ws, - queued_dispatches: VecDeque::new(), - }; - client.wait_for_opcode(10, Duration::from_secs(10)).await?; - client - .send_json(json!({ - "op": 2, - "d": { - "token": token, - "properties": { - "os": std::env::consts::OS, - "browser": "fluxer-dev-native-voice-it", - "device": client.name, - }, - "presence": null, - "ignored_events": [], - "flags": 0, - }, - })) - .await?; - client - .wait_for_dispatch("READY", Duration::from_secs(20), |_| true) - .await - .with_context(|| format!("{}: READY dispatch not received", client.name))?; - Ok(client) - } - - async fn send_json(&mut self, payload: Value) -> Result<()> { - self.ws - .send(Message::Text(payload.to_string().into())) - .await - .with_context(|| format!("{}: failed to send gateway frame", self.name)) - } - - async fn send_voice_update(&mut self, payload: Value) -> Result<()> { - self.send_json(json!({"op": 4, "d": payload})).await - } - - async fn wait_for_opcode(&mut self, opcode: i64, wait: Duration) -> Result { - let deadline = tokio::time::Instant::now() + wait; - loop { - let frame = self.recv_json(deadline).await?; - if frame.get("op").and_then(Value::as_i64) == Some(opcode) { - return Ok(frame); - } - self.maybe_queue_dispatch(frame); - } - } - - async fn wait_for_dispatch( - &mut self, - event: &str, - wait: Duration, - predicate: F, - ) -> Result - where - F: Fn(&Value) -> bool, - { - if let Some(index) = self - .queued_dispatches - .iter() - .position(|frame| dispatch_data(frame, event).map(&predicate).unwrap_or(false)) - { - let frame = self - .queued_dispatches - .remove(index) - .expect("queued dispatch index came from position"); - return Ok(frame.get("d").cloned().unwrap_or(Value::Null)); - } - - let deadline = tokio::time::Instant::now() + wait; - loop { - let frame = self.recv_json(deadline).await.with_context(|| { - format!( - "{}: while waiting for {event}; queued_dispatches={}", - self.name, - self.queued_dispatch_summary() - ) - })?; - if let Some(data) = dispatch_data(&frame, event) - && predicate(data) - { - return Ok(data.clone()); - } - self.maybe_queue_dispatch(frame); - } - } - - async fn recv_json(&mut self, deadline: tokio::time::Instant) -> Result { - loop { - let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); - if remaining.is_zero() { - bail!("{}: timed out waiting for gateway frame", self.name); - } - let message = tokio_timeout(remaining, self.ws.next()) - .await - .with_context(|| format!("{}: timed out waiting for gateway frame", self.name))? - .ok_or_else(|| anyhow!("{}: gateway stream ended", self.name))? - .with_context(|| format!("{}: gateway receive failed", self.name))?; - match message { - Message::Text(text) => { - return serde_json::from_str(text.as_ref()) - .with_context(|| format!("{}: invalid gateway JSON: {text}", self.name)); - } - Message::Binary(bytes) => { - return serde_json::from_slice(&bytes) - .with_context(|| format!("{}: invalid gateway binary JSON", self.name)); - } - Message::Ping(bytes) => { - self.ws - .send(Message::Pong(bytes)) - .await - .with_context(|| format!("{}: failed to send gateway pong", self.name))?; - } - Message::Pong(_) | Message::Frame(_) => {} - Message::Close(close) => bail!("{}: gateway closed: {close:?}", self.name), - } - } - } - - fn maybe_queue_dispatch(&mut self, frame: Value) { - if frame.get("op").and_then(Value::as_i64) == Some(0) { - self.queued_dispatches.push_back(frame); - } - } - - fn queued_dispatch_summary(&self) -> String { - if self.queued_dispatches.is_empty() { - return "[]".to_owned(); - } - let entries = self - .queued_dispatches - .iter() - .map(|frame| { - let event = frame - .get("t") - .and_then(Value::as_str) - .unwrap_or(""); - let data = frame.get("d").unwrap_or(&Value::Null); - let mutation = data - .get("mutation_id") - .and_then(Value::as_str) - .unwrap_or("-"); - let status = data.get("status").and_then(Value::as_str).unwrap_or("-"); - let error = data - .get("error_code") - .and_then(Value::as_str) - .unwrap_or("-"); - format!("{event}:mutation={mutation}:status={status}:error={error}") - }) - .collect::>(); - format!("[{}]", entries.join(", ")) - } -} - -async fn create_world(api: &Api) -> Result { - let mut accounts = Vec::new(); - for role in ["publisher", "subscriber", "publisher2"] { - accounts.push(create_account(api, role).await?); - } - - let guild = api - .request( - Method::POST, - "/guilds", - Some(&accounts[0].token), - Some(json!({"name": unique_name("Native Voice IT")})), - ) - .await - .context("failed to create integration guild")?; - let guild_id = string_field(&guild, "id")?; - - let channel = api - .request( - Method::POST, - &format!("/guilds/{guild_id}/channels"), - Some(&accounts[0].token), - Some(json!({"name": "native-voice-it", "type": 2})), - ) - .await - .context("failed to create integration voice channel")?; - let voice_channel_id = string_field(&channel, "id")?; - - let invite = api - .request( - Method::POST, - &format!("/channels/{voice_channel_id}/invites"), - Some(&accounts[0].token), - Some(json!({})), - ) - .await - .context("failed to create integration invite")?; - let invite_code = string_field(&invite, "code")?; - - for account in accounts.iter().skip(1) { - api.request( - Method::POST, - &format!("/invites/{invite_code}"), - Some(&account.token), - Some(Value::Null), - ) - .await - .with_context(|| format!("{} failed to accept guild invite", account.username))?; - } - - let private_channels = api - .request( - Method::POST, - &format!("/test/users/{}/private-channels", accounts[0].user_id), - None, - Some(json!({ - "dm_count": 1, - "group_dm_count": 1, - "recipients": [ - accounts[1].user_id, - accounts[2].user_id, - ], - })), - ) - .await - .context("failed to seed integration private voice channels")?; - let dm_channel_id = nested_string_field(&private_channels, "dms", 0, "channel_id")?; - let group_dm_channel_id = nested_string_field(&private_channels, "group_dms", 0, "channel_id")?; - - Ok(TestWorld { - accounts, - guild_id, - voice_channel_id, - dm_channel_id, - group_dm_channel_id, - }) -} - -async fn create_account(api: &Api, role: &str) -> Result { - let username_prefix = match role { - "publisher" => "vpub", - "subscriber" => "vsub", - "publisher2" => "vpub2", - _ => "voice", - }; - let username = unique_slug(username_prefix); - let email = format!("{username}@example.com"); - let registration = api - .request( - Method::POST, - "/auth/register", - None, - Some(json!({ - "email": email, - "username": username, - "global_name": role, - "password": DEFAULT_PASSWORD, - "date_of_birth": "1990-01-01", - "consent": true, - })), - ) - .await - .with_context(|| format!("{role}: failed to register account"))?; - let user_id = string_field(®istration, "user_id")?; - let token = string_field(®istration, "token")?; - api.request( - Method::PATCH, - &format!("/test/users/{user_id}/flags"), - None, - Some(json!({"flags": HAS_SESSION_STARTED})), - ) - .await - .with_context(|| format!("{role}: failed to mark session started"))?; - api.request( - Method::POST, - &format!("/test/users/{user_id}/security-flags"), - None, - Some(json!({ - "email_verified": true, - "suspicious_activity_flags": 0, - })), - ) - .await - .with_context(|| format!("{role}: failed to mark account verified"))?; - Ok(TestAccount { - user_id, - token, - username, - }) -} - -async fn connect_gateways(config: &Config, world: &TestWorld) -> Result> { - let mut clients = Vec::new(); - for account in &world.accounts { - let client = - GatewayClient::connect(&account.username, &config.gateway_url, &account.token).await?; - clients.push(client); - } - sleep(Duration::from_millis(500)).await; - Ok(clients) -} - -async fn join_and_confirm_all( - api: &Api, - config: &Config, - world: &TestWorld, - gateways: &mut [GatewayClient], -) -> Result> { - let mut connections = Vec::new(); - for index in 0..world.accounts.len() { - let self_stream = index == 0 || index == 2; - let self_video = index == 0; - let join_payload = json!({ - "guild_id": world.guild_id, - "channel_id": world.voice_channel_id, - "self_mute": false, - "self_deaf": false, - "self_video": self_video, - "self_stream": self_stream, - "is_mobile": false, - "latitude": 59.3293, - "longitude": 18.0686, - }); - gateways[index].send_voice_update(join_payload).await?; - let server = gateways[index] - .wait_for_dispatch("VOICE_SERVER_UPDATE", config.wait, |data| { - data.get("guild_id").and_then(Value::as_str) == Some(world.guild_id.as_str()) - && data.get("channel_id").and_then(Value::as_str) - == Some(world.voice_channel_id.as_str()) - && data.get("token").and_then(Value::as_str).is_some() - && data.get("endpoint").and_then(Value::as_str).is_some() - && data.get("connection_id").and_then(Value::as_str).is_some() - }) - .await - .with_context(|| { - format!( - "{} did not receive VOICE_SERVER_UPDATE", - gateways[index].name - ) - })?; - let connection_id = string_field(&server, "connection_id")?; - confirm_voice_connection( - api, - Some(&world.guild_id), - &world.voice_channel_id, - &connection_id, - ) - .await?; - let state = wait_for_voice_state_on_all( - gateways, - &connection_id, - Some(&world.voice_channel_id), - config.wait, - ) - .await?; - assert_bool_field(&state, "self_stream", self_stream)?; - assert_bool_field(&state, "self_video", self_video)?; - let user_id = world.accounts[index].user_id.clone(); - let identity = format!("user_{user_id}_{connection_id}"); - connections.push(VoiceConnection { - connection_id, - identity, - token: string_field(&server, "token")?, - endpoint: string_field(&server, "endpoint")?, - version: numeric_field(&state, "version").unwrap_or(0), - }); - } - Ok(connections) -} - -async fn confirm_voice_connection( - api: &Api, - guild_id: Option<&str>, - channel_id: &str, - connection_id: &str, -) -> Result<()> { - for attempt in 0..20 { - let mut body = json!({ - "channel_id": channel_id, - "connection_id": connection_id, - }); - if let Some(guild_id) = guild_id { - body["guild_id"] = Value::from(guild_id.to_owned()); - } - let (status, payload) = api - .request_status( - Method::POST, - "/test/voice/confirm-connection", - None, - Some(body), - ) - .await?; - if status.is_success() && payload.get("success").and_then(Value::as_bool) == Some(true) { - return Ok(()); - } - if status == StatusCode::NOT_FOUND - && payload.get("error").and_then(Value::as_str) == Some("pending_join_not_found") - && attempt < 19 - { - sleep(Duration::from_millis(250)).await; - continue; - } - bail!("voice confirm failed for {connection_id}: HTTP {status} {payload}"); - } - bail!("voice confirm failed for {connection_id}: pending join was not found after retries"); -} - -async fn run_voice_mutation_scenarios( - world: &TestWorld, - gateways: &mut [GatewayClient], - connections: &mut [VoiceConnection], -) -> Result<()> { - let publisher_stream_key = build_guild_stream_key( - &world.guild_id, - &world.voice_channel_id, - &connections[0].connection_id, - ); - let subscriber = 1; - let subscriber_conn = connections[subscriber].connection_id.clone(); - let runtime_epoch = unique_slug("runtime"); - - let watch_ack = send_mutation_and_wait( - &mut gateways[subscriber], - world, - &subscriber_conn, - "watch-publisher", - &runtime_epoch, - Some(connections[subscriber].version), - json!({ - "self_mute": false, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": [publisher_stream_key], - }), - ) - .await?; - assert_ack_status(&watch_ack, "applied")?; - assert_viewer_keys( - &watch_ack["canonical_state"], - &[publisher_stream_key.as_str()], - )?; - connections[subscriber].version = numeric_field(&watch_ack, "server_version").unwrap_or(1); - wait_for_voice_state_on_all( - gateways, - &subscriber_conn, - Some(&world.voice_channel_id), - Duration::from_secs(10), - ) - .await - .context("watch mutation did not broadcast subscriber voice state")?; - - let preserve_ack = send_mutation_and_wait( - &mut gateways[subscriber], - world, - &subscriber_conn, - "preserve-watch-while-muted", - &runtime_epoch, - Some(connections[subscriber].version), - json!({ - "self_mute": true, - "self_deaf": false, - "self_video": false, - "self_stream": false, - }), - ) - .await?; - assert_ack_status(&preserve_ack, "applied")?; - assert_viewer_keys( - &preserve_ack["canonical_state"], - &[publisher_stream_key.as_str()], - )?; - assert_bool_field(&preserve_ack["canonical_state"], "self_mute", true)?; - connections[subscriber].version = - numeric_field(&preserve_ack, "server_version").unwrap_or(connections[subscriber].version); - - let clear_ack = send_mutation_and_wait( - &mut gateways[subscriber], - world, - &subscriber_conn, - "clear-watch", - &runtime_epoch, - Some(connections[subscriber].version), - json!({ - "self_mute": true, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": Value::Null, - }), - ) - .await?; - assert_ack_status(&clear_ack, "applied")?; - assert_viewer_keys(&clear_ack["canonical_state"], &[])?; - connections[subscriber].version = - numeric_field(&clear_ack, "server_version").unwrap_or(connections[subscriber].version); - - sleep(Duration::from_millis(1200)).await; - let invalid_ack = send_mutation_and_wait( - &mut gateways[subscriber], - world, - &subscriber_conn, - "watch-missing-connection", - &runtime_epoch, - Some(connections[subscriber].version), - json!({ - "self_mute": true, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": [build_guild_stream_key(&world.guild_id, &world.voice_channel_id, "missing-connection")], - }), - ) - .await?; - assert_ack_status(&invalid_ack, "rejected")?; - assert_eq_field(&invalid_ack, "error_code", "VOICE_CONNECTION_NOT_FOUND")?; - - let stale_ack = send_mutation_and_wait( - &mut gateways[subscriber], - world, - &subscriber_conn, - "stale-base-version", - &runtime_epoch, - Some(0), - json!({ - "self_mute": false, - "self_deaf": false, - "self_video": false, - "self_stream": false, - }), - ) - .await?; - assert_ack_status(&stale_ack, "rejected")?; - assert_eq_field(&stale_ack, "error_code", "stale_base_version")?; - - sleep(Duration::from_millis(1200)).await; - for index in 0..5 { - let self_mute = index % 2 == 1; - let mutation_id = format!("queued-mute-{index}"); - gateways[subscriber] - .send_voice_update(voice_update_payload( - world, - &subscriber_conn, - &mutation_id, - &runtime_epoch, - None, - json!({ - "self_mute": self_mute, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": [], - }), - )) - .await?; - } - let queued_ack = gateways[subscriber] - .wait_for_dispatch("VOICE_STATE_ACK", Duration::from_secs(8), |data| { - data.get("mutation_id").and_then(Value::as_str) == Some("queued-mute-4") - }) - .await - .context("final queued voice mutation was not acknowledged")?; - assert_ack_status(&queued_ack, "applied")?; - assert_bool_field(&queued_ack["canonical_state"], "self_mute", false)?; - assert_viewer_keys(&queued_ack["canonical_state"], &[])?; - connections[subscriber].version = - numeric_field(&queued_ack, "server_version").unwrap_or(connections[subscriber].version); - - Ok(()) -} - -async fn run_private_call_scenarios( - api: &Api, - config: &Config, - world: &TestWorld, - gateways: &mut [GatewayClient], -) -> Result<()> { - let dm_participants = [0usize, 1usize]; - let mut dm_connections = join_and_confirm_private_call( - api, - config, - world, - "DM", - &world.dm_channel_id, - &dm_participants, - gateways, - ) - .await?; - run_private_watch_scenarios( - "DM", - &world.dm_channel_id, - &dm_participants, - gateways, - &mut dm_connections, - ) - .await?; - run_private_stress_scenarios( - config, - "DM", - &world.dm_channel_id, - &dm_participants, - gateways, - &mut dm_connections, - ) - .await?; - disconnect_private_call( - "DM", - &world.dm_channel_id, - &dm_participants, - gateways, - &dm_connections, - ) - .await?; - - let gdm_participants = [0usize, 1usize, 2usize]; - let mut gdm_connections = join_and_confirm_private_call( - api, - config, - world, - "GDM", - &world.group_dm_channel_id, - &gdm_participants, - gateways, - ) - .await?; - run_private_watch_scenarios( - "GDM", - &world.group_dm_channel_id, - &gdm_participants, - gateways, - &mut gdm_connections, - ) - .await?; - run_private_stress_scenarios( - config, - "GDM", - &world.group_dm_channel_id, - &gdm_participants, - gateways, - &mut gdm_connections, - ) - .await?; - disconnect_private_call( - "GDM", - &world.group_dm_channel_id, - &gdm_participants, - gateways, - &gdm_connections, - ) - .await?; - println!("Private DM/GDM voice backend integration checks passed."); - Ok(()) -} - -async fn join_and_confirm_private_call( - api: &Api, - config: &Config, - world: &TestWorld, - label: &str, - channel_id: &str, - participant_indices: &[usize], - gateways: &mut [GatewayClient], -) -> Result> { - let mut connections = Vec::new(); - for (position, gateway_index) in participant_indices.iter().copied().enumerate() { - let self_stream = position == 0; - let self_video = position == 0; - gateways[gateway_index] - .send_voice_update(json!({ - "guild_id": Value::Null, - "channel_id": channel_id, - "self_mute": false, - "self_deaf": false, - "self_video": self_video, - "self_stream": self_stream, - "is_mobile": position % 2 == 1, - "latitude": 59.3293, - "longitude": 18.0686, - })) - .await?; - let server = gateways[gateway_index] - .wait_for_dispatch("VOICE_SERVER_UPDATE", config.wait, |data| { - data.get("channel_id").and_then(Value::as_str) == Some(channel_id) - && data.get("guild_id").map(Value::is_null).unwrap_or(true) - && data.get("token").and_then(Value::as_str).is_some() - && data.get("endpoint").and_then(Value::as_str).is_some() - && data.get("connection_id").and_then(Value::as_str).is_some() - }) - .await - .with_context(|| { - format!( - "{} did not receive private {label} VOICE_SERVER_UPDATE", - gateways[gateway_index].name - ) - })?; - let connection_id = string_field(&server, "connection_id")?; - confirm_voice_connection(api, None, channel_id, &connection_id).await?; - let state = wait_for_voice_state_on_gateways_matching( - gateways, - participant_indices, - &connection_id, - Some(channel_id), - config.wait, - |_| true, - ) - .await - .with_context(|| { - format!("{label} join did not broadcast VOICE_STATE_UPDATE for {connection_id}") - })?; - assert_bool_field(&state, "self_stream", self_stream)?; - assert_bool_field(&state, "self_video", self_video)?; - let account = &world.accounts[gateway_index]; - let identity = format!("user_{}_{}", account.user_id, connection_id); - connections.push(VoiceConnection { - connection_id, - identity, - token: string_field(&server, "token")?, - endpoint: string_field(&server, "endpoint")?, - version: numeric_field(&state, "version").unwrap_or(0), - }); - } - Ok(connections) -} - -async fn run_private_watch_scenarios( - label: &str, - channel_id: &str, - participant_indices: &[usize], - gateways: &mut [GatewayClient], - connections: &mut [VoiceConnection], -) -> Result<()> { - if connections.len() < 2 || participant_indices.len() < 2 { - bail!("{label} private watch scenario requires at least two participants"); - } - let publisher_key = build_dm_stream_key(channel_id, &connections[0].connection_id); - let subscriber_position = 1usize; - let subscriber_gateway_index = participant_indices[subscriber_position]; - let subscriber_conn = connections[subscriber_position].connection_id.clone(); - - gateways[subscriber_gateway_index] - .send_voice_update(private_voice_update_payload( - channel_id, - &subscriber_conn, - json!({ - "self_mute": false, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": [publisher_key], - }), - )) - .await?; - let expected = vec![publisher_key.clone()]; - let state = wait_for_voice_state_on_gateways_matching( - gateways, - participant_indices, - &subscriber_conn, - Some(channel_id), - Duration::from_secs(10), - |data| viewer_keys_match(data, &expected), - ) - .await - .with_context(|| format!("{label} watch update did not broadcast watched stream key"))?; - assert_viewer_key_strings(&state, &expected)?; - - gateways[subscriber_gateway_index] - .send_voice_update(private_voice_update_payload( - channel_id, - &subscriber_conn, - json!({ - "self_mute": true, - "self_deaf": false, - "self_video": false, - "self_stream": false, - }), - )) - .await?; - let state = wait_for_voice_state_on_gateways_matching( - gateways, - participant_indices, - &subscriber_conn, - Some(channel_id), - Duration::from_secs(10), - |data| { - viewer_keys_match(data, &expected) - && data.get("self_mute").and_then(Value::as_bool) == Some(true) - }, - ) - .await - .with_context(|| format!("{label} mute update did not preserve private watch keys"))?; - assert_bool_field(&state, "self_mute", true)?; - assert_viewer_key_strings(&state, &expected)?; - - gateways[subscriber_gateway_index] - .send_voice_update(private_voice_update_payload( - channel_id, - &subscriber_conn, - json!({ - "self_mute": true, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": Value::Null, - }), - )) - .await?; - let empty: Vec = Vec::new(); - let state = wait_for_voice_state_on_gateways_matching( - gateways, - participant_indices, - &subscriber_conn, - Some(channel_id), - Duration::from_secs(10), - |data| viewer_keys_match(data, &empty), - ) - .await - .with_context(|| format!("{label} unwatch update did not clear private watch keys"))?; - assert_viewer_key_strings(&state, &empty)?; - Ok(()) -} - -async fn run_private_stress_scenarios( - config: &Config, - label: &str, - channel_id: &str, - participant_indices: &[usize], - gateways: &mut [GatewayClient], - connections: &mut [VoiceConnection], -) -> Result<()> { - if config.stress_iterations == 0 { - return Ok(()); - } - if connections.len() < 2 || participant_indices.len() < 2 { - bail!("{label} private stress scenario requires at least two participants"); - } - let publisher_key = build_dm_stream_key(channel_id, &connections[0].connection_id); - let subscriber_position = 1usize; - let subscriber_gateway_index = participant_indices[subscriber_position]; - let subscriber_conn = connections[subscriber_position].connection_id.clone(); - for iteration in 0..config.stress_iterations { - let expected_keys = if iteration % 3 == 0 { - Vec::new() - } else { - vec![publisher_key.clone()] - }; - let self_deaf = iteration % 4 == 0; - let self_stream = iteration % 5 == 0; - gateways[subscriber_gateway_index] - .send_voice_update(private_voice_update_payload( - channel_id, - &subscriber_conn, - json!({ - "self_mute": iteration % 2 == 0, - "self_deaf": self_deaf, - "self_video": false, - "self_stream": self_stream, - "viewer_stream_keys": expected_keys, - }), - )) - .await?; - let state = wait_for_voice_state_on_gateways_matching( - gateways, - participant_indices, - &subscriber_conn, - Some(channel_id), - Duration::from_secs(10), - |data| { - viewer_keys_match(data, &expected_keys) - && data.get("self_deaf").and_then(Value::as_bool) == Some(self_deaf) - && data.get("self_stream").and_then(Value::as_bool) == Some(self_stream) - }, - ) - .await - .with_context(|| { - format!("{label} private stress iteration {iteration} did not converge") - })?; - assert_viewer_key_strings(&state, &expected_keys)?; - assert_bool_field(&state, "self_deaf", self_deaf)?; - assert_bool_field(&state, "self_stream", self_stream)?; - } - Ok(()) -} - -async fn disconnect_private_call( - label: &str, - channel_id: &str, - participant_indices: &[usize], - gateways: &mut [GatewayClient], - connections: &[VoiceConnection], -) -> Result<()> { - for (position, gateway_index) in participant_indices.iter().copied().enumerate().rev() { - let connection = connections - .get(position) - .ok_or_else(|| anyhow!("{label} missing private connection at index {position}"))?; - gateways[gateway_index] - .send_voice_update(json!({ - "guild_id": Value::Null, - "channel_id": Value::Null, - "connection_id": connection.connection_id, - })) - .await?; - wait_for_voice_state_on_gateways_matching( - gateways, - participant_indices, - &connection.connection_id, - None, - Duration::from_secs(10), - |_| true, - ) - .await - .with_context(|| { - format!( - "{label} private disconnect did not broadcast for channel {channel_id} connection {}", - connection.connection_id - ) - })?; - } - Ok(()) -} - -fn private_voice_update_payload(channel_id: &str, connection_id: &str, changes: Value) -> Value { - let mut payload = json!({ - "guild_id": Value::Null, - "channel_id": channel_id, - "connection_id": connection_id, - "self_mute": false, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "is_mobile": false, - }); - if let (Some(map), Some(changes)) = (payload.as_object_mut(), changes.as_object()) { - for (key, value) in changes { - map.insert(key.clone(), value.clone()); - } - } - payload -} - -async fn run_disconnect_scenarios( - world: &TestWorld, - gateways: &mut [GatewayClient], - connections: &[VoiceConnection], -) -> Result<()> { - sleep(Duration::from_millis(1200)).await; - gateways[0] - .send_voice_update(json!({ - "guild_id": world.guild_id, - "channel_id": Value::Null, - "connection_id": connections[0].connection_id, - })) - .await?; - wait_for_voice_state_on_all( - gateways, - &connections[0].connection_id, - None, - Duration::from_secs(10), - ) - .await - .context("publisher disconnect did not broadcast VOICE_STATE_UPDATE")?; - - let runtime_epoch = unique_slug("runtime-after-disconnect"); - let disconnected_key = build_guild_stream_key( - &world.guild_id, - &world.voice_channel_id, - &connections[0].connection_id, - ); - let ack = send_mutation_and_wait( - &mut gateways[1], - world, - &connections[1].connection_id, - "watch-disconnected-publisher", - &runtime_epoch, - None, - json!({ - "self_mute": false, - "self_deaf": false, - "self_video": false, - "self_stream": false, - "viewer_stream_keys": [disconnected_key], - }), - ) - .await?; - assert_ack_status(&ack, "rejected")?; - assert_eq_field(&ack, "error_code", "VOICE_CONNECTION_NOT_FOUND")?; - Ok(()) -} - -async fn send_mutation_and_wait( - gateway: &mut GatewayClient, - world: &TestWorld, - connection_id: &str, - mutation_id: &str, - runtime_epoch: &str, - base_version: Option, - changes: Value, -) -> Result { - gateway - .send_voice_update(voice_update_payload( - world, - connection_id, - mutation_id, - runtime_epoch, - base_version, - changes, - )) - .await?; - gateway - .wait_for_dispatch("VOICE_STATE_ACK", Duration::from_secs(8), |data| { - data.get("mutation_id").and_then(Value::as_str) == Some(mutation_id) - }) - .await - .with_context(|| format!("{mutation_id}: VOICE_STATE_ACK not received")) -} - -fn voice_update_payload( - world: &TestWorld, - connection_id: &str, - mutation_id: &str, - runtime_epoch: &str, - base_version: Option, - changes: Value, -) -> Value { - let mut payload = json!({ - "guild_id": world.guild_id, - "channel_id": world.voice_channel_id, - "connection_id": connection_id, - "mutation_id": mutation_id, - "runtime_epoch": runtime_epoch, - }); - if let Some(base_version) = base_version { - payload["base_version"] = Value::from(base_version); - } - if let (Some(map), Some(changes)) = (payload.as_object_mut(), changes.as_object()) { - for (key, value) in changes { - map.insert(key.clone(), value.clone()); - } - } - payload -} - -async fn wait_for_voice_state_on_all( - gateways: &mut [GatewayClient], - connection_id: &str, - expected_channel_id: Option<&str>, - wait: Duration, -) -> Result { - let indices = (0..gateways.len()).collect::>(); - wait_for_voice_state_on_gateways_matching( - gateways, - &indices, - connection_id, - expected_channel_id, - wait, - |_| true, - ) - .await -} - -async fn wait_for_voice_state_on_gateways_matching( - gateways: &mut [GatewayClient], - gateway_indices: &[usize], - connection_id: &str, - expected_channel_id: Option<&str>, - wait: Duration, - predicate: F, -) -> Result -where - F: Fn(&Value) -> bool, -{ - let mut first = None; - for index in gateway_indices { - let gateway = gateways - .get_mut(*index) - .ok_or_else(|| anyhow!("gateway index {index} is out of range"))?; - let state = gateway - .wait_for_dispatch("VOICE_STATE_UPDATE", wait, |data| { - voice_state_matches(data, connection_id, expected_channel_id) && predicate(data) - }) - .await - .with_context(|| { - format!( - "{} did not receive VOICE_STATE_UPDATE for connection {connection_id}", - gateway.name - ) - })?; - if first.is_none() { - first = Some(state); - } - } - first.ok_or_else(|| anyhow!("no gateway clients were selected")) -} - -fn run_native_livekit_harness( - config: &Config, - world: &TestWorld, - connections: &[VoiceConnection], -) -> Result<()> { - if connections.len() < 3 { - bail!("native LiveKit harness requires publisher, subscriber, and secondary publisher"); - } - let livekit_url = config - .livekit_url - .clone() - .unwrap_or_else(|| connections[0].endpoint.clone()); - let room = format!( - "guild_{}_channel_{}", - world.guild_id, world.voice_channel_id - ); - let report_path = native_report_path(); - let mut env = vec![ - ("LIVEKIT_URL".to_owned(), Some(livekit_url)), - ("LIVEKIT_ROOM".to_owned(), Some(room)), - ( - "LIVEKIT_PUBLISHER_TOKEN".to_owned(), - Some(connections[0].token.clone()), - ), - ( - "LIVEKIT_PUBLISHER_IDENTITY".to_owned(), - Some(connections[0].identity.clone()), - ), - ( - "LIVEKIT_SUBSCRIBER_TOKEN".to_owned(), - Some(connections[1].token.clone()), - ), - ( - "LIVEKIT_SUBSCRIBER_IDENTITY".to_owned(), - Some(connections[1].identity.clone()), - ), - ( - "LIVEKIT_SECONDARY_PUBLISHER_TOKEN".to_owned(), - Some(connections[2].token.clone()), - ), - ( - "LIVEKIT_SECONDARY_PUBLISHER_IDENTITY".to_owned(), - Some(connections[2].identity.clone()), - ), - ( - "LIVEKIT_SCREEN_CODECS".to_owned(), - Some(config.native_screen_codecs.clone()), - ), - ( - "LIVEKIT_ENABLE_SECOND_PUBLISHER".to_owned(), - Some("1".to_owned()), - ), - ("LIVEKIT_ENABLE_MICROPHONE".to_owned(), Some("1".to_owned())), - ( - "LIVEKIT_SECOND_PUBLISHER_ENABLE_MICROPHONE".to_owned(), - Some("1".to_owned()), - ), - ( - "LIVEKIT_ENABLE_SCREEN_AUDIO".to_owned(), - Some("1".to_owned()), - ), - ( - "LIVEKIT_SECOND_PUBLISHER_ENABLE_SCREEN_AUDIO".to_owned(), - Some("1".to_owned()), - ), - ( - "LIVEKIT_ENABLE_DATA_PACKET".to_owned(), - Some("1".to_owned()), - ), - ( - "LIVEKIT_ENABLE_SUBSCRIPTION_CYCLE".to_owned(), - Some("1".to_owned()), - ), - ("LIVEKIT_REQUIRED".to_owned(), Some("1".to_owned())), - ( - "FLUXER_WEBRTC_SENDER_LIVEKIT_REQUIRED".to_owned(), - Some("1".to_owned()), - ), - ( - "LIVEKIT_HARNESS_REPORT_PATH".to_owned(), - Some(report_path.display().to_string()), - ), - ]; - if config.native_strict { - env.push(("LIVEKIT_HARNESS_STRICT".to_owned(), Some("1".to_owned()))); - } - if let Some(duration_ms) = config.native_duration_ms { - env.push(( - "LIVEKIT_HARNESS_DURATION_MS".to_owned(), - Some(duration_ms.to_string()), - )); - } - println!( - "Running native VoiceEngine LiveKit harness with backend-issued tokens; report={}", - report_path.display() - ); - run_command( - &[ - "pnpm", - "--dir", - "fluxer_desktop/native/webrtc-sender", - "run", - "test:livekit", - ], - RunOptions { - cwd: ROOT.as_path(), - env, - ..RunOptions::default() - }, - ) - .map(drop) -} - -fn native_report_path() -> PathBuf { - ROOT.join(".fluxer/dev/native-voice-livekit-report.json") -} - -fn parse_json_or_null(text: &str) -> Result { - if text.trim().is_empty() { - return Ok(Value::Null); - } - serde_json::from_str(text).context("response was not JSON") -} - -fn dispatch_data<'a>(frame: &'a Value, event: &str) -> Option<&'a Value> { - if frame.get("op").and_then(Value::as_i64) != Some(0) { - return None; - } - if frame.get("t").and_then(Value::as_str) != Some(event) { - return None; - } - frame.get("d") -} - -fn voice_state_matches( - data: &Value, - connection_id: &str, - expected_channel_id: Option<&str>, -) -> bool { - if data.get("connection_id").and_then(Value::as_str) != Some(connection_id) { - return false; - } - match expected_channel_id { - Some(channel_id) => data.get("channel_id").and_then(Value::as_str) == Some(channel_id), - None => data.get("channel_id").is_some_and(Value::is_null), - } -} - -fn build_guild_stream_key(guild_id: &str, channel_id: &str, connection_id: &str) -> String { - format!("{guild_id}:{channel_id}:{connection_id}") -} - -fn build_dm_stream_key(channel_id: &str, connection_id: &str) -> String { - format!("dm:{channel_id}:{connection_id}") -} - -fn string_field(value: &Value, field: &str) -> Result { - value - .get(field) - .and_then(Value::as_str) - .map(str::to_owned) - .ok_or_else(|| anyhow!("missing string field {field} in {value}")) -} - -fn nested_string_field( - value: &Value, - array_field: &str, - index: usize, - field: &str, -) -> Result { - value - .get(array_field) - .and_then(Value::as_array) - .and_then(|values| values.get(index)) - .and_then(|item| item.get(field)) - .and_then(Value::as_str) - .map(str::to_owned) - .ok_or_else(|| anyhow!("missing {array_field}[{index}].{field} in {value}")) -} - -fn numeric_field(value: &Value, field: &str) -> Option { - value.get(field).and_then(|value| match value { - Value::Number(number) => number.as_u64(), - Value::String(string) => string.parse().ok(), - _ => None, - }) -} - -fn assert_ack_status(ack: &Value, expected: &str) -> Result<()> { - assert_eq_field(ack, "status", expected) -} - -fn assert_eq_field(value: &Value, field: &str, expected: &str) -> Result<()> { - let actual = value - .get(field) - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("missing string field {field} in {value}"))?; - if actual != expected { - bail!("expected {field}={expected}, got {actual}; payload={value}"); - } - Ok(()) -} - -fn assert_bool_field(value: &Value, field: &str, expected: bool) -> Result<()> { - let actual = value - .get(field) - .and_then(Value::as_bool) - .ok_or_else(|| anyhow!("missing bool field {field} in {value}"))?; - if actual != expected { - bail!("expected {field}={expected}, got {actual}; payload={value}"); - } - Ok(()) -} - -fn assert_viewer_keys(value: &Value, expected: &[&str]) -> Result<()> { - let actual = value - .get("viewer_stream_keys") - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("missing viewer_stream_keys array in {value}"))?; - let actual: Vec<&str> = actual.iter().filter_map(Value::as_str).collect(); - if actual != expected { - bail!("expected viewer_stream_keys={expected:?}, got {actual:?}; payload={value}"); - } - Ok(()) -} - -fn assert_viewer_key_strings(value: &Value, expected: &[String]) -> Result<()> { - let actual = viewer_key_strings(value) - .ok_or_else(|| anyhow!("missing viewer_stream_keys array in {value}"))?; - if actual != expected { - bail!("expected viewer_stream_keys={expected:?}, got {actual:?}; payload={value}"); - } - Ok(()) -} - -fn viewer_keys_match(value: &Value, expected: &[String]) -> bool { - viewer_key_strings(value).is_some_and(|actual| actual == expected) -} - -fn viewer_key_strings(value: &Value) -> Option> { - value.get("viewer_stream_keys")?.as_array().map(|values| { - values - .iter() - .filter_map(Value::as_str) - .map(str::to_owned) - .collect() - }) -} - -fn unique_name(prefix: &str) -> String { - format!("{prefix} {}", unique_suffix()) -} - -fn unique_slug(prefix: &str) -> String { - format!("{prefix}_{}", unique_suffix()) - .replace('-', "_") - .to_lowercase() -} - -fn unique_suffix() -> String { - let millis = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or_default(); - format!("{}_{}", std::process::id(), millis) -} - -fn unique_client_ip() -> String { - let millis = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or_default(); - let fourth = (millis % 250) + 1; - format!("198.51.100.{fourth}") -} diff --git a/tools/dev/src/object_store.rs b/tools/dev/src/object_store.rs new file mode 100644 index 000000000..4c9ee06a9 --- /dev/null +++ b/tools/dev/src/object_store.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use crate::cassandra::{apply_schema, config_from_env}; +use crate::proc::{RunOptions, merged_env, run_command}; +use anyhow::{Context, Result, bail}; +use std::env; +use std::process::{Command, Output, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; +use tokio::time::sleep; + +pub const S3_BUCKETS: &[&str] = &[ + "fluxer", + "fluxer-uploads", + "fluxer-downloads", + "fluxer-reports", + "fluxer-harvests", + "fluxer-static", +]; + +pub fn s3_endpoint() -> String { + env::var("FLUXER_S3_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8333".to_owned()) +} + +pub fn s3_env() -> Vec<(String, Option)> { + vec![ + ( + "AWS_ACCESS_KEY_ID".to_owned(), + Some(env::var("FLUXER_S3_ACCESS_KEY_ID").unwrap_or_else(|_| "fluxer".to_owned())), + ), + ( + "AWS_SECRET_ACCESS_KEY".to_owned(), + Some( + env::var("FLUXER_S3_SECRET_ACCESS_KEY") + .unwrap_or_else(|_| "fluxer-secret".to_owned()), + ), + ), + ( + "AWS_DEFAULT_REGION".to_owned(), + Some(env::var("FLUXER_S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned())), + ), + ] +} + +pub async fn wait_s3_api(timeout_secs: u64) -> Result<()> { + let endpoint = s3_endpoint(); + let deadline = Instant::now() + Duration::from_secs(timeout_secs); + let mut last_output = String::new(); + while Instant::now() < deadline { + let env = merged_env(Some(&s3_env()), true)?; + let output = Command::new("aws") + .args(["--endpoint-url", &endpoint, "s3api", "list-buckets"]) + .env_clear() + .envs(env) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context("failed to run aws s3api list-buckets")?; + if output.status.success() { + println!("SeaweedFS S3 API is reachable at {endpoint}"); + return Ok(()); + } + let mut combined = output.stdout; + combined.extend(output.stderr); + last_output = String::from_utf8_lossy(&combined).trim().to_owned(); + sleep(Duration::from_secs(2)).await; + } + bail!("Timed out waiting for SeaweedFS S3 API at {endpoint}: {last_output}"); +} + +pub fn ensure_s3_buckets() -> Result<()> { + let endpoint = s3_endpoint(); + let env = s3_env(); + for bucket in S3_BUCKETS { + ensure_s3_bucket(&endpoint, &env, bucket)?; + } + Ok(()) +} + +fn ensure_s3_bucket(endpoint: &str, env: &[(String, Option)], bucket: &str) -> Result<()> { + let deadline = Instant::now() + Duration::from_secs(30); + + loop { + if s3_bucket_exists(endpoint, env, bucket)? { + println!("S3 bucket exists: {bucket}"); + return Ok(()); + } + + let output = create_s3_bucket(endpoint, env, bucket)?; + if output.status.success() { + println!("Created S3 bucket: {bucket}"); + return Ok(()); + } + + let text = command_output_text(&output); + if !s3_bucket_already_exists_output(&text) { + let code = output.status.code().unwrap_or(-1); + bail!( + "Command failed with exit code {code}: aws --endpoint-url {endpoint} s3api create-bucket --bucket {bucket}" + ); + } + + if Instant::now() >= deadline { + bail!( + "Timed out waiting for S3 bucket {bucket} to become readable after create-bucket reported it already exists: {text}" + ); + } + thread::sleep(Duration::from_secs(1)); + } +} + +fn s3_bucket_exists( + endpoint: &str, + env: &[(String, Option)], + bucket: &str, +) -> Result { + let output = run_command( + &[ + "aws", + "--endpoint-url", + endpoint, + "s3api", + "head-bucket", + "--bucket", + bucket, + ], + RunOptions { + env: env.to_vec(), + check: false, + capture: true, + ..RunOptions::default() + }, + )?; + Ok(output.status.success()) +} + +fn create_s3_bucket( + endpoint: &str, + env: &[(String, Option)], + bucket: &str, +) -> Result { + run_command( + &[ + "aws", + "--endpoint-url", + endpoint, + "s3api", + "create-bucket", + "--bucket", + bucket, + ], + RunOptions { + env: env.to_vec(), + check: false, + capture: true, + ..RunOptions::default() + }, + ) +} + +fn command_output_text(output: &Output) -> String { + let mut combined = output.stdout.clone(); + combined.extend_from_slice(&output.stderr); + String::from_utf8_lossy(&combined).trim().to_owned() +} + +fn s3_bucket_already_exists_output(output: &str) -> bool { + output.contains("BucketAlreadyExists") || output.contains("BucketAlreadyOwnedByYou") +} + +pub async fn bootstrap_schema_and_object_store() -> Result<()> { + if cassandra_backend_enabled() { + apply_schema(Some(config_from_env()?)).await?; + } + ensure_s3_buckets() +} + +fn cassandra_backend_enabled() -> bool { + env::var("FLUXER_DATABASE_BACKEND").as_deref() == Ok("cassandra") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn s3_env_uses_fluxer_defaults() { + let env = s3_env(); + assert!(env.iter().any( + |(key, value)| key == "AWS_DEFAULT_REGION" && value.as_deref() == Some("us-east-1") + )); + } + + #[test] + fn bucket_already_exists_output_matches_aws_errors() { + assert!(s3_bucket_already_exists_output( + "An error occurred (BucketAlreadyExists) when calling the CreateBucket operation" + )); + assert!(s3_bucket_already_exists_output( + "An error occurred (BucketAlreadyOwnedByYou) when calling the CreateBucket operation" + )); + assert!(!s3_bucket_already_exists_output( + "An error occurred (AccessDenied) when calling the CreateBucket operation" + )); + } +} diff --git a/tools/dev/src/smoke.rs b/tools/dev/src/smoke.rs deleted file mode 100644 index d288aafcc..000000000 --- a/tools/dev/src/smoke.rs +++ /dev/null @@ -1,446 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::cassandra::{apply_schema, config_from_env, verify_schema}; -use crate::manifest::{API_PORT, DEV_PROXY_PORT, LOOPBACK_HOST}; -use crate::proc::{RunOptions, merged_env, run_command, wait_http, wait_tcp}; -use anyhow::{Context, Result, bail}; -use futures_util::StreamExt; -use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; -use serde_json::json; -use std::env; -use std::process::{Command, Output, Stdio}; -use std::thread; -use std::time::{Duration, Instant}; -use tokio::time::sleep; -use tokio_tungstenite::{connect_async, tungstenite::Message}; -use url::Url; - -pub const S3_BUCKETS: &[&str] = &[ - "fluxer", - "fluxer-uploads", - "fluxer-downloads", - "fluxer-reports", - "fluxer-harvests", - "fluxer-static", -]; - -pub async fn run_smoke(quick: bool, public: bool) -> Result<()> { - let timeout = if quick { 5 } else { 120 }; - wait_tcp( - "Valkey", - &env::var("VALKEY_HOST").unwrap_or_else(|_| "valkey".to_owned()), - env::var("VALKEY_PORT") - .unwrap_or_else(|_| "6379".to_owned()) - .parse()?, - timeout, - ) - .await?; - wait_tcp( - "NATS", - &env::var("NATS_HOST").unwrap_or_else(|_| "nats".to_owned()), - env::var("NATS_PORT") - .unwrap_or_else(|_| "4222".to_owned()) - .parse()?, - timeout, - ) - .await?; - wait_tcp( - "LiveKit", - &env::var("LIVEKIT_HOST").unwrap_or_else(|_| "livekit".to_owned()), - env::var("LIVEKIT_PORT") - .unwrap_or_else(|_| "7880".to_owned()) - .parse()?, - timeout, - ) - .await?; - wait_tcp( - "Mailpit SMTP", - &env::var("MAILPIT_HOST").unwrap_or_else(|_| "mailpit".to_owned()), - env::var("MAILPIT_SMTP_PORT") - .unwrap_or_else(|_| "1025".to_owned()) - .parse()?, - timeout, - ) - .await?; - wait_tcp( - "SeaweedFS S3", - &env::var("S3_HOST").unwrap_or_else(|_| "127.0.0.1".to_owned()), - env::var("S3_PORT") - .unwrap_or_else(|_| "8333".to_owned()) - .parse()?, - timeout, - ) - .await?; - wait_http( - "SeaweedFS master", - &env::var("SEAWEEDFS_MASTER_URL") - .unwrap_or_else(|_| "http://127.0.0.1:9333/cluster/status".to_owned()), - timeout, - ) - .await?; - wait_s3_api(timeout).await?; - if !quick { - if cassandra_backend_enabled() { - verify_schema(None, None).await?; - } - check_s3_buckets()?; - check_config_load()?; - } - if public { - check_gateway_internal_rpc().await?; - check_public_routes(timeout).await?; - } - println!("Fluxer smoke checks passed."); - Ok(()) -} - -fn check_config_load() -> Result<()> { - let script = concat!( - "void import('./packages/config/src/ConfigLoader.ts')", - ".then(async (module) => { const config = await module.loadConfig(); ", - "if (!config.integrations.voice.enabled) throw new Error('voice disabled'); ", - "console.log('config: ok'); })", - ".catch((error) => { console.error(error); process.exit(1); });", - ); - crate::proc::run(&["pnpm", "exec", "tsx", "-e", script]) -} - -pub fn s3_endpoint() -> String { - env::var("FLUXER_S3_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8333".to_owned()) -} - -pub fn s3_env() -> Vec<(String, Option)> { - vec![ - ( - "AWS_ACCESS_KEY_ID".to_owned(), - Some(env::var("FLUXER_S3_ACCESS_KEY_ID").unwrap_or_else(|_| "fluxer".to_owned())), - ), - ( - "AWS_SECRET_ACCESS_KEY".to_owned(), - Some( - env::var("FLUXER_S3_SECRET_ACCESS_KEY") - .unwrap_or_else(|_| "fluxer-secret".to_owned()), - ), - ), - ( - "AWS_DEFAULT_REGION".to_owned(), - Some(env::var("FLUXER_S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned())), - ), - ] -} - -pub async fn wait_s3_api(timeout_secs: u64) -> Result<()> { - let endpoint = s3_endpoint(); - let deadline = Instant::now() + Duration::from_secs(timeout_secs); - let mut last_output = String::new(); - while Instant::now() < deadline { - let env = merged_env(Some(&s3_env()), true)?; - let output = Command::new("aws") - .args(["--endpoint-url", &endpoint, "s3api", "list-buckets"]) - .env_clear() - .envs(env) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .context("failed to run aws s3api list-buckets")?; - if output.status.success() { - println!("SeaweedFS S3 API is reachable at {endpoint}"); - return Ok(()); - } - let mut combined = output.stdout; - combined.extend(output.stderr); - last_output = String::from_utf8_lossy(&combined).trim().to_owned(); - sleep(Duration::from_secs(2)).await; - } - bail!("Timed out waiting for SeaweedFS S3 API at {endpoint}: {last_output}"); -} - -pub fn ensure_s3_buckets() -> Result<()> { - let endpoint = s3_endpoint(); - let env = s3_env(); - for bucket in S3_BUCKETS { - ensure_s3_bucket(&endpoint, &env, bucket)?; - } - Ok(()) -} - -fn ensure_s3_bucket(endpoint: &str, env: &[(String, Option)], bucket: &str) -> Result<()> { - let deadline = Instant::now() + Duration::from_secs(30); - - loop { - if s3_bucket_exists(endpoint, env, bucket)? { - println!("S3 bucket exists: {bucket}"); - return Ok(()); - } - - let output = create_s3_bucket(endpoint, env, bucket)?; - if output.status.success() { - println!("Created S3 bucket: {bucket}"); - return Ok(()); - } - - let text = command_output_text(&output); - if !s3_bucket_already_exists_output(&text) { - let code = output.status.code().unwrap_or(-1); - bail!( - "Command failed with exit code {code}: aws --endpoint-url {endpoint} s3api create-bucket --bucket {bucket}" - ); - } - - if Instant::now() >= deadline { - bail!( - "Timed out waiting for S3 bucket {bucket} to become readable after create-bucket reported it already exists: {text}" - ); - } - thread::sleep(Duration::from_secs(1)); - } -} - -fn s3_bucket_exists( - endpoint: &str, - env: &[(String, Option)], - bucket: &str, -) -> Result { - let output = run_command( - &[ - "aws", - "--endpoint-url", - endpoint, - "s3api", - "head-bucket", - "--bucket", - bucket, - ], - RunOptions { - env: env.to_vec(), - check: false, - capture: true, - ..RunOptions::default() - }, - )?; - Ok(output.status.success()) -} - -fn create_s3_bucket( - endpoint: &str, - env: &[(String, Option)], - bucket: &str, -) -> Result { - run_command( - &[ - "aws", - "--endpoint-url", - endpoint, - "s3api", - "create-bucket", - "--bucket", - bucket, - ], - RunOptions { - env: env.to_vec(), - check: false, - capture: true, - ..RunOptions::default() - }, - ) -} - -fn command_output_text(output: &Output) -> String { - let mut combined = output.stdout.clone(); - combined.extend_from_slice(&output.stderr); - String::from_utf8_lossy(&combined).trim().to_owned() -} - -fn s3_bucket_already_exists_output(output: &str) -> bool { - output.contains("BucketAlreadyExists") || output.contains("BucketAlreadyOwnedByYou") -} - -fn check_s3_buckets() -> Result<()> { - let endpoint = s3_endpoint(); - let env = s3_env(); - for bucket in S3_BUCKETS { - run_command( - &[ - "aws", - "--endpoint-url", - &endpoint, - "s3api", - "head-bucket", - "--bucket", - bucket, - ], - RunOptions { - env: env.clone(), - capture: true, - ..RunOptions::default() - }, - )?; - } - println!("S3 buckets: ok"); - Ok(()) -} - -async fn check_public_routes(timeout_secs: u64) -> Result<()> { - let base_url = public_smoke_base_url(); - wait_http( - "dev proxy api", - &format!("{base_url}/api/_health"), - timeout_secs, - ) - .await?; - wait_http( - "dev proxy media", - &format!("{base_url}/media/_health"), - timeout_secs, - ) - .await?; - wait_http( - "dev proxy gateway", - &format!("{base_url}/gateway/_health"), - timeout_secs, - ) - .await?; - check_gateway_websocket(&base_url, timeout_secs).await?; - wait_http("dev proxy app", &format!("{base_url}/"), timeout_secs).await?; - wait_http("devmail", &format!("{base_url}/devmail/"), timeout_secs).await -} - -async fn check_gateway_websocket(base_url: &str, timeout_secs: u64) -> Result<()> { - let url = gateway_websocket_url(base_url)?; - let connect_timeout = Duration::from_secs(timeout_secs.clamp(5, 30)); - let (mut socket, _) = tokio::time::timeout(connect_timeout, connect_async(url.as_str())) - .await - .with_context(|| format!("timed out connecting to gateway websocket at {url}"))? - .with_context(|| format!("failed to connect to gateway websocket at {url}"))?; - let frame = tokio::time::timeout(Duration::from_secs(10), socket.next()) - .await - .with_context(|| format!("timed out waiting for gateway websocket hello at {url}"))? - .ok_or_else(|| anyhow::anyhow!("gateway websocket ended before hello at {url}"))? - .with_context(|| format!("gateway websocket receive failed at {url}"))?; - if matches!(frame, Message::Close(_)) { - bail!("gateway websocket closed before hello at {url}"); - } - println!("gateway websocket is reachable at {url}"); - Ok(()) -} - -fn gateway_websocket_url(base_url: &str) -> Result { - let mut url = - Url::parse(base_url).with_context(|| format!("invalid public URL: {base_url}"))?; - let scheme = match url.scheme() { - "https" => "wss", - "http" => "ws", - scheme => bail!("public URL must use http or https for websocket smoke: {scheme}"), - }; - url.set_scheme(scheme) - .map_err(|_| anyhow::anyhow!("failed to set websocket scheme for {base_url}"))?; - url.set_path("/gateway"); - url.set_query(Some("v=1&encoding=json&compress=zstd-stream&stream=1")); - url.set_fragment(None); - Ok(url.to_string()) -} - -fn public_smoke_base_url() -> String { - if let Ok(url) = env::var("FLUXER_DEV_PUBLIC_SMOKE_URL") { - let url = url.trim(); - if !url.is_empty() { - return url.trim_end_matches('/').to_owned(); - } - } - if let Ok(url) = env::var("FLUXER_PUBLIC_URL") { - let url = url.trim().trim_end_matches('/'); - if !url.is_empty() && !matches!(url, "http://localhost:8088" | "http://127.0.0.1:8088") { - return url.to_owned(); - } - } - if let Ok(url) = crate::tunnel::resolve_cloudflare_public_url(None) { - return url.trim_end_matches('/').to_owned(); - } - format!("http://{LOOPBACK_HOST}:{DEV_PROXY_PORT}") -} - -async fn check_gateway_internal_rpc() -> Result<()> { - let base_endpoint = env::var("FLUXER_INTERNAL_API_ENDPOINT") - .unwrap_or_else(|_| format!("http://{LOOPBACK_HOST}:{API_PORT}")); - let endpoint = env::var("FLUXER_GATEWAY_API_RPC_ENDPOINT") - .unwrap_or_else(|_| format!("{}/internal/rpc", base_endpoint.trim_end_matches('/'))); - let token = env::var("FLUXER_GATEWAY_RPC_AUTH_TOKEN").unwrap_or_default(); - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert("x-forwarded-for", HeaderValue::from_static(LOOPBACK_HOST)); - headers.insert("x-fluxer-rpc-auth", HeaderValue::from_str(&token)?); - let payload: serde_json::Value = reqwest::Client::new() - .post(endpoint) - .headers(headers) - .json(&json!({"type": "get_gateway_rollout_config"})) - .send() - .await? - .json() - .await?; - if payload.get("type").and_then(|value| value.as_str()) != Some("get_gateway_rollout_config") - || payload.pointer("/data/config").is_none() - { - bail!("Gateway internal RPC returned unexpected payload: {payload}"); - } - println!("Gateway internal RPC: ok"); - Ok(()) -} - -pub async fn bootstrap_schema_and_object_store() -> Result<()> { - if cassandra_backend_enabled() { - apply_schema(Some(config_from_env()?)).await?; - } - ensure_s3_buckets() -} - -fn cassandra_backend_enabled() -> bool { - env::var("FLUXER_DATABASE_BACKEND").as_deref() == Ok("cassandra") -} - -pub async fn http_ok(url: &str) -> bool { - reqwest::Client::new() - .get(url) - .timeout(Duration::from_secs(5)) - .send() - .await - .map(|response| response.status().as_u16() < 500) - .unwrap_or(false) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn s3_env_uses_fluxer_defaults() { - let env = s3_env(); - assert!(env.iter().any( - |(key, value)| key == "AWS_DEFAULT_REGION" && value.as_deref() == Some("us-east-1") - )); - } - - #[test] - fn bucket_already_exists_output_matches_aws_errors() { - assert!(s3_bucket_already_exists_output( - "An error occurred (BucketAlreadyExists) when calling the CreateBucket operation" - )); - assert!(s3_bucket_already_exists_output( - "An error occurred (BucketAlreadyOwnedByYou) when calling the CreateBucket operation" - )); - assert!(!s3_bucket_already_exists_output( - "An error occurred (AccessDenied) when calling the CreateBucket operation" - )); - } - - #[test] - fn gateway_websocket_url_uses_public_scheme() { - assert_eq!( - gateway_websocket_url("https://dev.example.test").unwrap(), - "wss://dev.example.test/gateway?v=1&encoding=json&compress=zstd-stream&stream=1" - ); - assert_eq!( - gateway_websocket_url("http://localhost:8088").unwrap(), - "ws://localhost:8088/gateway?v=1&encoding=json&compress=zstd-stream&stream=1" - ); - } -} diff --git a/tools/integration/expected/event_log_replay_determinism.json b/tools/integration/expected/event_log_replay_determinism.json deleted file mode 100644 index 7bf0b997d..000000000 --- a/tools/integration/expected/event_log_replay_determinism.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema": 1, - "scenario": "event_log_replay_determinism", - "seed": 4242, - "workload": "network-partition-during-screen-share", - "snapshot_hash": "f957df06", - "event_count": 22, - "final_tick": 401, - "description": "FNV-1a hash of the simulator snapshot after the network-partition-during-screen-share scenario at seed 4242. The hash must be byte-identical across macOS, Linux, Windows. Any divergence indicates a platform-leak: floating-point determinism, HashMap iteration order, or non-deterministic clock/random.", - "recorded_on_platform": "macos", - "recorded_at_iso8601": "2026-06-09T00:00:00Z" -} diff --git a/tools/integration/harness.toml b/tools/integration/harness.toml deleted file mode 100644 index 75f6fb6e0..000000000 --- a/tools/integration/harness.toml +++ /dev/null @@ -1,103 +0,0 @@ -schema_version = 1 - -[platforms.macos] -description = "macOS ARM64 (this host) - real ScreenCaptureKit, CoreAudio, Metal via wgpu" -runner = "local" -cargo_build = true -ts_driver = true -scenarios = [ - "scenario_audio_capture_to_mix", - "scenario_screen_capture_to_pool", - "scenario_gpu_device_loss_recovery", - "scenario_event_log_replay_determinism", - "scenario_ffi_airlock_negative_space", - "scenario_encoder_handoff_dryrun", -] - -[platforms.linux-fedora] -description = "Fedora 42 ARM64 in Parallels VM, reached via prlctl - real PipeWire, Vulkan via wgpu" -runner = "prlctl" -prlctl_vm = "Fedora 42 ARM64" -prlctl_source = "/media/psf/fluxer-dev" -cargo_build = true -ts_driver = true -scenarios = [ - "scenario_audio_capture_to_mix", - "scenario_screen_capture_to_pool", - "scenario_gpu_device_loss_recovery", - "scenario_event_log_replay_determinism", - "scenario_ffi_airlock_negative_space", - "scenario_encoder_handoff_dryrun", -] - -[platforms.windows-tailnet] -description = "Windows 11 desktop on Tailnet (desktop-5ba12hj) - NVIDIA MX450 + Intel Iris Xe, WGC, DXGI, D3D11, NVENC, QSV" -runner = "ssh" -ssh_host = "desktop-5ba12hj" -ssh_user = "Hampus" -cargo_build = true -ts_driver = true -scenarios = [ - "scenario_audio_capture_to_mix", - "scenario_screen_capture_to_pool", - "scenario_gpu_device_loss_recovery", - "scenario_event_log_replay_determinism", - "scenario_ffi_airlock_negative_space", - "scenario_encoder_handoff_dryrun", -] - -[platforms.windows-vm] -description = "Windows 11 ARM Parallels VM - ARM-only, limited GPU access" -runner = "prlctl" -prlctl_vm = "Windows 11" -cargo_build = true -ts_driver = true -scenarios = [ - "scenario_audio_capture_to_mix", - "scenario_screen_capture_to_pool", - "scenario_event_log_replay_determinism", - "scenario_ffi_airlock_negative_space", - "scenario_encoder_handoff_dryrun", -] - -[scenario.scenario_audio_capture_to_mix] -description = "Real PCM capture -> SourceRing -> AudioMixSession -> APM -> mix output" -expected_runtime_seconds = 5 -applicable_platforms = ["macos", "linux-fedora", "windows-tailnet", "windows-vm"] -driver = "rust" -subcommand = "audio-capture-to-mix" - -[scenario.scenario_screen_capture_to_pool] -description = "Real screen capture -> FramePool with allocation-free steady state" -expected_runtime_seconds = 5 -applicable_platforms = ["macos", "linux-fedora", "windows-tailnet", "windows-vm"] -driver = "rust" -subcommand = "screen-capture-to-pool" - -[scenario.scenario_gpu_device_loss_recovery] -description = "wgpu device loss -> GpuLossRegistry rebuild walk -> Nv12Packer ready" -expected_runtime_seconds = 10 -applicable_platforms = ["macos", "linux-fedora", "windows-tailnet"] -driver = "rust" -subcommand = "gpu-device-loss-recovery" - -[scenario.scenario_event_log_replay_determinism] -description = "Fixed-seed simulator scenario - hash agreement across platforms" -expected_runtime_seconds = 20 -applicable_platforms = ["macos", "linux-fedora", "windows-tailnet", "windows-vm"] -driver = "typescript" -ts_entry = "tools/integration/ts-driver/event_log_replay_determinism.ts" - -[scenario.scenario_ffi_airlock_negative_space] -description = "Malformed audio frame -> Rust receive-side assert panic with expected message" -expected_runtime_seconds = 3 -applicable_platforms = ["macos", "linux-fedora", "windows-tailnet", "windows-vm"] -driver = "rust" -subcommand = "ffi-airlock-negative" - -[scenario.scenario_encoder_handoff_dryrun] -description = "EncoderInputRing - D3D11KeyedMutexBackend on Windows, CpuMemcpyBackend elsewhere - 8 slots, DTS offset, skip-don't-block" -expected_runtime_seconds = 5 -applicable_platforms = ["macos", "linux-fedora", "windows-tailnet", "windows-vm"] -driver = "rust" -subcommand = "encoder-handoff-dryrun" diff --git a/tools/integration/integration-driver/.gitignore b/tools/integration/integration-driver/.gitignore deleted file mode 100644 index a2d439c7b..000000000 --- a/tools/integration/integration-driver/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -Cargo.lock - -target/ -target-*/ diff --git a/tools/integration/integration-driver/Cargo.toml b/tools/integration/integration-driver/Cargo.toml deleted file mode 100644 index 252af1adb..000000000 --- a/tools/integration/integration-driver/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "fluxer_integration_driver" -version = "0.0.0" -edition = "2024" -license = "AGPL-3.0-or-later" -publish = false - -[workspace] -resolver = "2" - -[[bin]] -name = "integration-driver" -path = "src/main.rs" - -[dependencies] -fluxer_screen_frame_bus = {path = "../../../fluxer_desktop/native/screen-frame-bus", features = ["wgpu"]} -fluxer_gpu_rebuild = {path = "../../../fluxer_desktop/native/gpu-rebuild", features = ["wgpu"]} -fluxer_audio_mix = {path = "../../../fluxer_desktop/native/audio-mix"} -fluxer_audio_apm = {path = "../../../fluxer_desktop/native/audio-apm"} -fluxer_nv12_gpu_pack = {path = "../../../fluxer_desktop/native/nv12-gpu-pack"} -fluxer_encoder_ring = {path = "../../../fluxer_desktop/native/encoder-ring"} -fluxer_rt_thread = {path = "../../../fluxer_desktop/native/rt-thread"} -wgpu = "29" -serde = {version = "1", features = ["derive"]} -serde_json = "1" diff --git a/tools/integration/integration-driver/src/main.rs b/tools/integration/integration-driver/src/main.rs deleted file mode 100644 index 28f3301e1..000000000 --- a/tools/integration/integration-driver/src/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#![deny(warnings)] -#![deny(clippy::unwrap_used)] -#![deny(clippy::panic)] -#![deny(clippy::too_many_lines)] - -mod scenarios; - -use std::process::ExitCode; - -const USAGE: &str = "usage: integration-driver [args...]\n\ - scenarios:\n \ - audio-capture-to-mix\n \ - screen-capture-to-pool\n \ - gpu-device-loss-recovery\n \ - encoder-handoff-dryrun\n \ - ffi-airlock-negative [invalid-sample-rate|valid-baseline]\n"; - -fn main() -> ExitCode { - let args: Vec = std::env::args().collect(); - assert!(!args.is_empty(), "argv must contain program name"); - if args.len() < 2 { - eprintln!("{}", USAGE); - return ExitCode::from(64); - } - let scenario = args[1].as_str(); - let tail: Vec<&str> = args[2..].iter().map(String::as_str).collect(); - let result = match scenario { - "audio-capture-to-mix" => scenarios::audio_capture_to_mix::run(&tail), - "screen-capture-to-pool" => scenarios::screen_capture_to_pool::run(&tail), - "gpu-device-loss-recovery" => scenarios::gpu_device_loss_recovery::run(&tail), - "encoder-handoff-dryrun" => scenarios::encoder_handoff_dryrun::run(&tail), - "ffi-airlock-negative" => scenarios::ffi_airlock_negative::run(&tail), - other => { - eprintln!("unknown scenario {other}\n{USAGE}"); - return ExitCode::from(64); - } - }; - match result { - Ok(report) => { - println!("{}", report); - ExitCode::SUCCESS - } - Err(report) => { - println!("{}", report); - ExitCode::from(1) - } - } -} diff --git a/tools/integration/integration-driver/src/scenarios/audio_capture_to_mix.rs b/tools/integration/integration-driver/src/scenarios/audio_capture_to_mix.rs deleted file mode 100644 index 9d7361879..000000000 --- a/tools/integration/integration-driver/src/scenarios/audio_capture_to_mix.rs +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use fluxer_audio_apm::{ApmConfigBuilder, AudioProcessor, StubAudioProcessor}; -use fluxer_audio_mix::{AUDIO_OUTPUT_FRAMES, AudioMixSession, SourceRing}; -use fluxer_rt_thread::TickInfo; -use serde_json::json; - -use super::ScenarioReport; - -const TONE_AMPLITUDE: i16 = 8_000; -const TONE_SAMPLE_RATE_HZ: u32 = 48_000; -const TONE_FREQUENCY_HZ: u32 = 440; -const TONE_TICK_COUNT: u64 = 4; -const APM_SAMPLE_RATE_HZ: u32 = 48_000; -const APM_CHANNELS: u16 = 1; -const APM_FRAME_SAMPLES: usize = 480; - -pub fn run(_args: &[&str]) -> Result { - assert_eq!(APM_FRAME_SAMPLES, 480, "APM 10 ms @ 48 kHz invariant"); - let measurements = match drive_pipeline() { - Ok(m) => m, - Err(reason) => { - return Err(ScenarioReport::fail( - "audio_capture_to_mix", - json!({"reason": reason}), - Vec::new(), - )); - } - }; - let mut assertions = Vec::new(); - if measurements.tone_mix_peak < (TONE_AMPLITUDE as i64) / 4 { - return Err(ScenarioReport::fail( - "audio_capture_to_mix", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec!["tone peak below quarter-amplitude floor".to_string()], - )); - } - assertions.push("tone present in mix output above quarter-amplitude floor".to_string()); - if measurements.apm_capture_frames_processed == 0 { - return Err(ScenarioReport::fail( - "audio_capture_to_mix", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec!["APM processed zero capture frames".to_string()], - )); - } - assertions.push("APM processed at least one capture frame".to_string()); - if measurements.mix_ticks_completed != TONE_TICK_COUNT { - return Err(ScenarioReport::fail( - "audio_capture_to_mix", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec![format!( - "expected {} mix ticks, observed {}", - TONE_TICK_COUNT, measurements.mix_ticks_completed - )], - )); - } - assertions.push("mix session completed the full tick schedule".to_string()); - Ok(ScenarioReport::pass( - "audio_capture_to_mix", - serde_json::to_value(&measurements).unwrap_or(json!({})), - assertions, - )) -} - -#[derive(Debug, serde::Serialize)] -struct Measurements { - tone_mix_peak: i64, - tone_mix_rms: i64, - mix_ticks_completed: u64, - apm_capture_frames_processed: u64, - source_ring_pushed_total: u64, - source_ring_drained_total: u64, - sample_rate_hz: u32, - tone_frequency_hz: u32, -} - -fn drive_pipeline() -> Result { - let (mut producer, consumer) = SourceRing::create(8192, TONE_SAMPLE_RATE_HZ) - .map_err(|err| format!("ring create failed: {err:?}"))?; - let pushed_per_tick = AUDIO_OUTPUT_FRAMES; - let mut phase: u32 = 0; - let mut apm_buffer: Vec = vec![0i16; APM_FRAME_SAMPLES]; - let apm_config = ApmConfigBuilder::new() - .aec(false) - .ns(false) - .agc(false) - .build(); - let mut apm = StubAudioProcessor::new(apm_config, APM_SAMPLE_RATE_HZ, APM_CHANNELS) - .map_err(|err| format!("apm init failed: {err:?}"))?; - let mut source_ring_pushed_total: u64 = 0; - for _ in 0..TONE_TICK_COUNT { - let tone = generate_tone_chunk(pushed_per_tick, &mut phase); - assert_eq!(tone.len(), pushed_per_tick, "tone chunk length"); - let mut apm_offset: usize = 0; - while apm_offset + APM_FRAME_SAMPLES <= tone.len() { - apm_buffer.copy_from_slice(&tone[apm_offset..apm_offset + APM_FRAME_SAMPLES]); - let _ = apm - .process_capture_frame(&mut apm_buffer, APM_SAMPLE_RATE_HZ, APM_CHANNELS) - .map_err(|err| format!("apm process failed: {err:?}"))?; - apm_offset += APM_FRAME_SAMPLES; - } - let pushed = producer.try_push_slice(&tone); - assert_eq!(pushed, tone.len(), "ring must accept full tone chunk"); - source_ring_pushed_total = source_ring_pushed_total.saturating_add(pushed as u64); - } - let mut session = AudioMixSession::new(vec![consumer], AUDIO_OUTPUT_FRAMES) - .map_err(|err| format!("mix init failed: {err:?}"))?; - let mut peak_observed: i64 = 0; - let mut accumulator_sq: u64 = 0; - let mut counted: u64 = 0; - for tick_index in 0..TONE_TICK_COUNT { - let tick = synthetic_tick(tick_index); - let _ = session.tick(tick); - let output = session.last_output(); - for sample in output.iter() { - let abs = (*sample as i64).abs(); - if abs > peak_observed { - peak_observed = abs; - } - let sq = (*sample as i64).saturating_mul(*sample as i64); - accumulator_sq = accumulator_sq.saturating_add(sq as u64); - counted = counted.saturating_add(1); - } - } - let rms = accumulator_sq.checked_div(counted).unwrap_or(0).isqrt() as i64; - Ok(Measurements { - tone_mix_peak: peak_observed, - tone_mix_rms: rms, - mix_ticks_completed: session.ticks_completed(), - apm_capture_frames_processed: apm.capture_frames_processed(), - source_ring_pushed_total, - source_ring_drained_total: 0, - sample_rate_hz: TONE_SAMPLE_RATE_HZ, - tone_frequency_hz: TONE_FREQUENCY_HZ, - }) -} - -fn generate_tone_chunk(samples: usize, phase: &mut u32) -> Vec { - assert!(samples > 0, "tone chunk samples positive"); - assert!(samples <= 1 << 14, "tone chunk within sanity cap"); - let mut out = Vec::with_capacity(samples); - let period_samples = TONE_SAMPLE_RATE_HZ / TONE_FREQUENCY_HZ; - assert!(period_samples > 0, "period must be positive"); - for _ in 0..samples { - let progress = (*phase as f32) / (period_samples as f32); - let radians = progress * std::f32::consts::TAU; - let value = (radians.sin() * TONE_AMPLITUDE as f32) as i16; - out.push(value); - *phase = phase.wrapping_add(1); - if *phase >= period_samples { - *phase = 0; - } - } - assert_eq!(out.len(), samples, "tone chunk length post-condition"); - out -} - -fn synthetic_tick(index: u64) -> TickInfo { - let scheduled_ns = index.saturating_mul(21_333_333); - TickInfo { - tick_index: index, - scheduled_ns, - actual_ns: scheduled_ns, - lag_ns: 0, - } -} diff --git a/tools/integration/integration-driver/src/scenarios/encoder_handoff_dryrun.rs b/tools/integration/integration-driver/src/scenarios/encoder_handoff_dryrun.rs deleted file mode 100644 index 681127719..000000000 --- a/tools/integration/integration-driver/src/scenarios/encoder_handoff_dryrun.rs +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use fluxer_encoder_ring::{ - CpuMemcpyBackend, EncoderInputRing, RingError, TextureFormat, apply_dts_offset, - compute_dts_offset_us, -}; -use serde_json::json; - -use super::ScenarioReport; - -const FRAME_WIDTH_PX: u32 = 1280; -const FRAME_HEIGHT_PX: u32 = 720; -const RING_DEPTH_TARGET: u32 = 8; -const LAG_OVERSUBSCRIBE_COUNT: u32 = 4; -const FRAME_INTERVAL_US: u64 = 16_666; -const NUM_B_FRAMES: u32 = 2; -const FIRST_PTS_US: u64 = 0; - -pub fn run(_args: &[&str]) -> Result { - let measurements = match drive_ring() { - Ok(m) => m, - Err(reason) => { - return Err(ScenarioReport::fail( - "encoder_handoff_dryrun", - json!({"reason": reason}), - Vec::new(), - )); - } - }; - let mut assertions = Vec::new(); - if measurements.completed_count != RING_DEPTH_TARGET as u64 { - return Err(ScenarioReport::fail( - "encoder_handoff_dryrun", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec![format!( - "expected {} completed slots, observed {}", - RING_DEPTH_TARGET, measurements.completed_count - )], - )); - } - assertions.push("eight slots submitted and completed".to_string()); - if measurements.dropped_count != LAG_OVERSUBSCRIBE_COUNT as u64 { - return Err(ScenarioReport::fail( - "encoder_handoff_dryrun", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec![format!( - "expected {} dropped under oversubscription, observed {}", - LAG_OVERSUBSCRIBE_COUNT, measurements.dropped_count - )], - )); - } - assertions.push("oversubscribed submits report skip-don't-block drop counter".to_string()); - if measurements.dispatched_count != RING_DEPTH_TARGET as u64 { - return Err(ScenarioReport::fail( - "encoder_handoff_dryrun", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec![format!( - "expected {} dispatched ready frames, observed {}", - RING_DEPTH_TARGET, measurements.dispatched_count - )], - )); - } - assertions.push("dispatched_count equals submitted_count in steady state".to_string()); - if measurements.dts_first_frame > measurements.pts_first_frame { - return Err(ScenarioReport::fail( - "encoder_handoff_dryrun", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec!["first DTS must not exceed PTS".to_string()], - )); - } - assertions.push("DTS offset is non-positive given B-frames".to_string()); - Ok(ScenarioReport::pass( - "encoder_handoff_dryrun", - serde_json::to_value(&measurements).unwrap_or(json!({})), - assertions, - )) -} - -#[derive(Debug, serde::Serialize)] -struct Measurements { - backend: &'static str, - submitted_count: u64, - completed_count: u64, - dispatched_count: u64, - dropped_count: u64, - capacity: usize, - pts_first_frame: u64, - dts_first_frame: u64, - dts_offset_us: i64, - texture_skipped_frames: u32, -} - -fn drive_ring() -> Result { - let backend = CpuMemcpyBackend::new(); - let mut ring: EncoderInputRing = EncoderInputRing::new(backend); - ring.initialise(FRAME_WIDTH_PX, FRAME_HEIGHT_PX, TextureFormat::Nv12) - .map_err(|err| format!("ring init failed: {err:?}"))?; - assert_eq!( - ring.capacity(), - RING_DEPTH_TARGET as usize, - "ring capacity invariant" - ); - assert_eq!( - ring.free_count(), - RING_DEPTH_TARGET as usize, - "fresh ring fully free" - ); - for _ in 0..RING_DEPTH_TARGET { - ring.submit(noop_fill) - .map_err(|err| format!("first round submit failed: {err:?}"))?; - } - let mut texture_skipped_frames: u32 = 0; - for _ in 0..LAG_OVERSUBSCRIBE_COUNT { - match ring.submit(noop_fill) { - Err(RingError::FullDropped { .. }) => { - texture_skipped_frames = texture_skipped_frames.saturating_add(1); - } - Err(err) => return Err(format!("unexpected submit error: {err:?}")), - Ok(()) => return Err("oversubscribed submit must drop".to_string()), - } - } - let dts_offset_us = compute_dts_offset_us(FIRST_PTS_US, NUM_B_FRAMES, FRAME_INTERVAL_US); - assert!(dts_offset_us <= 0, "dts offset non-positive"); - let pts_first_frame = FRAME_INTERVAL_US.saturating_mul(NUM_B_FRAMES as u64); - let dts_first_frame = apply_dts_offset(pts_first_frame, dts_offset_us); - let mut released_sequences: Vec = Vec::with_capacity(RING_DEPTH_TARGET as usize); - while let Some(ready) = ring.poll_next_ready() { - released_sequences.push(ready.sequence); - ring.release_completed(ready) - .map_err(|err| format!("release failed: {err:?}"))?; - } - assert_eq!( - released_sequences.len(), - RING_DEPTH_TARGET as usize, - "expected all submitted frames to drain" - ); - for (idx, seq) in released_sequences.iter().enumerate() { - let expected = (idx + 1) as u64; - assert_eq!(*seq, expected, "FIFO release order"); - } - let metrics = ring.metrics(); - Ok(Measurements { - backend: backend_label(), - submitted_count: metrics.submitted_count, - completed_count: metrics.completed_count, - dispatched_count: metrics.dispatched_count, - dropped_count: metrics.dropped_count, - capacity: ring.capacity(), - pts_first_frame, - dts_first_frame, - dts_offset_us, - texture_skipped_frames, - }) -} - -#[cfg(target_os = "windows")] -const fn backend_label() -> &'static str { - "cpu_memcpy_with_d3d11_keyed_mutex_available" -} - -#[cfg(not(target_os = "windows"))] -const fn backend_label() -> &'static str { - "cpu_memcpy_backend" -} - -fn noop_fill(_slot: &mut fluxer_encoder_ring::CpuSlotHandle) {} diff --git a/tools/integration/integration-driver/src/scenarios/ffi_airlock_negative.rs b/tools/integration/integration-driver/src/scenarios/ffi_airlock_negative.rs deleted file mode 100644 index 9693aa921..000000000 --- a/tools/integration/integration-driver/src/scenarios/ffi_airlock_negative.rs +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use serde_json::json; -use std::fmt; -use std::panic::{AssertUnwindSafe, catch_unwind}; - -use super::ScenarioReport; - -const VALID_SAMPLE_RATE_HZ: u32 = 48_000; -const INVALID_SAMPLE_RATE_HZ: u32 = 44_100; -const VALID_CHANNELS: u32 = 1; -const VALID_FRAME_BYTES: u32 = 960; -const VALID_TIMESTAMP_NS: u64 = 1_000_000; -const AUDIO_FRAME_BYTES_MAX: u32 = 1 << 20; -const AUDIO_SAMPLE_RATES_HZ: [u32; 3] = [16_000, 32_000, 48_000]; -const AUDIO_CHANNELS_VALID: [u32; 2] = [1, 2]; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct VoiceEngineV2AudioFrameInvariants { - sample_rate_hz: u32, - num_channels: u32, - frame_bytes: u32, - timestamp_ns: u64, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum VoiceEngineV2AssertError { - FrameBytesOutOfRange { bytes: u32, max: u32 }, - SampleRateInvalid { hz: u32 }, - ChannelsInvalid { channels: u32 }, - TimestampRegressed { previous_ns: u64, received_ns: u64 }, -} - -impl fmt::Display for VoiceEngineV2AssertError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::FrameBytesOutOfRange { bytes, max } => { - write!( - formatter, - "AudioFrameBytesOutOfRange: audio frame bytes {bytes} not in (0, {max}]" - ) - } - Self::SampleRateInvalid { hz } => { - write!( - formatter, - "AudioSampleRateInvalid: audio sample rate {hz} not in [16000, 32000, 48000]" - ) - } - Self::ChannelsInvalid { channels } => { - write!( - formatter, - "AudioChannelsInvalid: audio channels {channels} not in [1, 2]" - ) - } - Self::TimestampRegressed { - previous_ns, - received_ns, - } => { - write!( - formatter, - "AudioTimestampRegressed: audio timestamp {received_ns} did not exceed previous {previous_ns}" - ) - } - } - } -} - -fn check_audio_frame_invariants( - frame: VoiceEngineV2AudioFrameInvariants, - previous_timestamp_ns: Option, -) -> Result<(), VoiceEngineV2AssertError> { - if frame.frame_bytes == 0 || frame.frame_bytes > AUDIO_FRAME_BYTES_MAX { - return Err(VoiceEngineV2AssertError::FrameBytesOutOfRange { - bytes: frame.frame_bytes, - max: AUDIO_FRAME_BYTES_MAX, - }); - } - if !AUDIO_SAMPLE_RATES_HZ.contains(&frame.sample_rate_hz) { - return Err(VoiceEngineV2AssertError::SampleRateInvalid { - hz: frame.sample_rate_hz, - }); - } - if !AUDIO_CHANNELS_VALID.contains(&frame.num_channels) { - return Err(VoiceEngineV2AssertError::ChannelsInvalid { - channels: frame.num_channels, - }); - } - if let Some(previous_ns) = previous_timestamp_ns - && frame.timestamp_ns <= previous_ns - { - return Err(VoiceEngineV2AssertError::TimestampRegressed { - previous_ns, - received_ns: frame.timestamp_ns, - }); - } - Ok(()) -} - -#[allow(clippy::panic)] -fn assert_audio_frame_invariants( - frame: VoiceEngineV2AudioFrameInvariants, - previous_timestamp_ns: Option, -) { - let result = check_audio_frame_invariants(frame, previous_timestamp_ns); - if let Err(err) = result { - panic!("{err}"); - } -} - -pub fn run(args: &[&str]) -> Result { - let prior_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(|_info| {})); - let result = run_inner(args); - std::panic::set_hook(prior_hook); - result -} - -fn run_inner(args: &[&str]) -> Result { - let mode = args.first().copied().unwrap_or("invalid-sample-rate"); - let measurements = match mode { - "invalid-sample-rate" => exercise_invalid_sample_rate(), - "valid-baseline" => exercise_valid_baseline(), - other => { - return Err(ScenarioReport::fail( - "ffi_airlock_negative", - json!({"reason": format!("unknown mode: {other}")}), - Vec::new(), - )); - } - }; - match measurements { - Ok(m) => Ok(ScenarioReport::pass( - "ffi_airlock_negative", - serde_json::to_value(&m).unwrap_or(json!({})), - m.assertions, - )), - Err(report) => Err(report), - } -} - -#[derive(Debug, serde::Serialize)] -struct Measurements { - mode: &'static str, - panic_observed: bool, - panic_message: Option, - valid_baseline_passed: bool, - #[serde(skip_serializing)] - assertions: Vec, -} - -fn exercise_invalid_sample_rate() -> Result { - let frame = VoiceEngineV2AudioFrameInvariants { - sample_rate_hz: INVALID_SAMPLE_RATE_HZ, - num_channels: VALID_CHANNELS, - frame_bytes: VALID_FRAME_BYTES, - timestamp_ns: VALID_TIMESTAMP_NS, - }; - let outcome = catch_unwind(AssertUnwindSafe(|| { - assert_audio_frame_invariants(frame, None); - })); - match outcome { - Ok(()) => Err(ScenarioReport::fail( - "ffi_airlock_negative", - json!({"mode": "invalid-sample-rate", "panic_observed": false}), - vec!["receive-side assertion failed to panic on invalid sample rate".to_string()], - )), - Err(payload) => { - let message = panic_payload_to_string(&payload); - if !message.contains("AudioSampleRateInvalid") - && !message.contains("not in [16000, 32000, 48000]") - { - return Err(ScenarioReport::fail( - "ffi_airlock_negative", - json!({"mode": "invalid-sample-rate", "panic_observed": true, "panic_message": message}), - vec!["panic message did not mention AudioSampleRateInvalid".to_string()], - )); - } - let assertions = vec![ - "receive-side assertion panicked as expected".to_string(), - "panic message names AudioSampleRateInvalid".to_string(), - ]; - Ok(Measurements { - mode: "invalid-sample-rate", - panic_observed: true, - panic_message: Some(message), - valid_baseline_passed: false, - assertions, - }) - } - } -} - -fn exercise_valid_baseline() -> Result { - let frame = VoiceEngineV2AudioFrameInvariants { - sample_rate_hz: VALID_SAMPLE_RATE_HZ, - num_channels: VALID_CHANNELS, - frame_bytes: VALID_FRAME_BYTES, - timestamp_ns: VALID_TIMESTAMP_NS, - }; - let outcome = catch_unwind(AssertUnwindSafe(|| { - assert_audio_frame_invariants(frame, None); - })); - match outcome { - Ok(()) => { - let assertions = vec!["valid-frame baseline did not panic".to_string()]; - Ok(Measurements { - mode: "valid-baseline", - panic_observed: false, - panic_message: None, - valid_baseline_passed: true, - assertions, - }) - } - Err(payload) => Err(ScenarioReport::fail( - "ffi_airlock_negative", - json!({ - "mode": "valid-baseline", - "panic_observed": true, - "panic_message": panic_payload_to_string(&payload), - }), - vec!["valid baseline frame must not panic".to_string()], - )), - } -} - -fn panic_payload_to_string(payload: &Box) -> String { - if let Some(s) = payload.downcast_ref::<&'static str>() { - return (*s).to_string(); - } - if let Some(s) = payload.downcast_ref::() { - return s.clone(); - } - "".to_string() -} diff --git a/tools/integration/integration-driver/src/scenarios/gpu_device_loss_recovery.rs b/tools/integration/integration-driver/src/scenarios/gpu_device_loss_recovery.rs deleted file mode 100644 index 9cab74921..000000000 --- a/tools/integration/integration-driver/src/scenarios/gpu_device_loss_recovery.rs +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use fluxer_gpu_rebuild::{GpuLossCallback, GpuLossRegistry}; -use fluxer_nv12_gpu_pack::Nv12Packer; -use fluxer_screen_frame_bus::gpu_loss::{ - WgpuStagingBackend, WgpuStagingConfig, try_acquire_device, -}; -use serde_json::json; - -use super::ScenarioReport; - -const PACKER_MAX_WIDTH: u32 = 1280; -const PACKER_MAX_HEIGHT: u32 = 720; -const STAGING_BYTE_LEN: u64 = 64 * 1024; - -pub fn run(_args: &[&str]) -> Result { - let (device, queue, _instance) = acquire_gpu_context()?; - let measurements = drive_rebuild(&device, &queue).map_err(|reason| { - ScenarioReport::fail( - "gpu_device_loss_recovery", - json!({"reason": reason}), - Vec::new(), - ) - })?; - let assertions = check_measurements(&measurements)?; - Ok(ScenarioReport::pass( - "gpu_device_loss_recovery", - serde_json::to_value(&measurements).unwrap_or(json!({})), - assertions, - )) -} - -fn acquire_gpu_context() -> Result<(wgpu::Device, wgpu::Queue, wgpu::Instance), ScenarioReport> { - let acquired = std::panic::catch_unwind(std::panic::AssertUnwindSafe(try_acquire_device)); - match acquired { - Ok(Some(triple)) => Ok(triple), - Ok(None) => Err(ScenarioReport::fail( - "gpu_device_loss_recovery", - json!({"reason": "no wgpu adapter available"}), - Vec::new(), - )), - Err(_) => Err(ScenarioReport::fail( - "gpu_device_loss_recovery", - json!({"reason": "wgpu adapter acquisition panicked"}), - Vec::new(), - )), - } -} - -fn check_measurements(measurements: &Measurements) -> Result, ScenarioReport> { - if measurements.released_count != 2 { - return Err(fail_with( - measurements, - format!( - "expected 2 release calls, observed {}", - measurements.released_count - ), - )); - } - if measurements.rebuilt_count != 2 { - return Err(fail_with( - measurements, - format!( - "expected 2 rebuilt owners, observed {}", - measurements.rebuilt_count - ), - )); - } - if measurements.failed_count != 0 { - return Err(fail_with( - measurements, - format!( - "expected 0 failures, observed {}", - measurements.failed_count - ), - )); - } - if !measurements.packer_ready_after_rebuild { - return Err(fail_with( - measurements, - "Nv12Packer not ready after rebuild".to_string(), - )); - } - if !measurements.staging_ready_after_rebuild { - return Err(fail_with( - measurements, - "WgpuStagingBackend not ready after rebuild".to_string(), - )); - } - Ok(vec![ - "registry released both owners".to_string(), - "registry rebuilt both owners".to_string(), - "zero failures during rebuild walk".to_string(), - "Nv12Packer reports is_ready true after rebuild".to_string(), - "WgpuStagingBackend reports is_ready true after rebuild".to_string(), - ]) -} - -fn fail_with(measurements: &Measurements, message: String) -> ScenarioReport { - ScenarioReport::fail( - "gpu_device_loss_recovery", - serde_json::to_value(measurements).unwrap_or(json!({})), - vec![message], - ) -} - -#[derive(Debug, serde::Serialize)] -struct Measurements { - released_count: u32, - rebuilt_count: u32, - failed_count: u32, - vacant_count: u32, - packer_ready_after_rebuild: bool, - staging_ready_after_rebuild: bool, - adapter_info: String, -} - -fn drive_rebuild(device: &wgpu::Device, queue: &wgpu::Queue) -> Result { - let registry = GpuLossRegistry::new(); - let packer = Box::new(Nv12Packer::new(device, PACKER_MAX_WIDTH, PACKER_MAX_HEIGHT)); - let staging = Box::new(WgpuStagingBackend::new( - device, - WgpuStagingConfig::new(STAGING_BYTE_LEN), - )); - let packer_built_before = GpuLossCallback::is_ready(packer.as_ref()); - let staging_built_before = GpuLossCallback::is_ready(staging.as_ref()); - assert!(packer_built_before, "packer must be ready before rebuild"); - assert!(staging_built_before, "staging must be ready before rebuild"); - let _packer_guard = registry.register(packer); - let _staging_guard = registry.register(staging); - let report = registry.handle_device_lost(device, queue); - assert!( - report.released_count as usize <= 2, - "no more than two owners released" - ); - let mut packer_ready = false; - let mut staging_ready = false; - for outcome in report.outcomes.iter() { - match outcome { - fluxer_gpu_rebuild::RebuildOutcome::Rebuilt { label, .. } => { - if label.contains("nv12") { - packer_ready = true; - } - if label.contains("staging") { - staging_ready = true; - } - } - fluxer_gpu_rebuild::RebuildOutcome::Failed { .. } => { - return Err("rebuild outcome reported failure".to_string()); - } - fluxer_gpu_rebuild::RebuildOutcome::Vacant { .. } => {} - } - } - Ok(Measurements { - released_count: report.released_count, - rebuilt_count: report.rebuilt_count, - failed_count: report.failed_count, - vacant_count: report.vacant_count, - packer_ready_after_rebuild: packer_ready, - staging_ready_after_rebuild: staging_ready, - adapter_info: format!("{:?}", device.limits().max_texture_dimension_2d), - }) -} diff --git a/tools/integration/integration-driver/src/scenarios/mod.rs b/tools/integration/integration-driver/src/scenarios/mod.rs deleted file mode 100644 index e85026572..000000000 --- a/tools/integration/integration-driver/src/scenarios/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pub mod audio_capture_to_mix; -pub mod encoder_handoff_dryrun; -pub mod ffi_airlock_negative; -pub mod gpu_device_loss_recovery; -pub mod screen_capture_to_pool; - -use serde::Serialize; - -pub const SCHEMA_VERSION: u32 = 1; - -#[derive(Debug, Serialize)] -pub struct ScenarioReport { - pub schema: u32, - pub scenario: &'static str, - pub platform: &'static str, - pub status: &'static str, - pub measurements: serde_json::Value, - pub assertions: Vec, -} - -impl ScenarioReport { - pub fn pass( - scenario: &'static str, - measurements: serde_json::Value, - assertions: Vec, - ) -> Self { - let report = Self { - schema: SCHEMA_VERSION, - scenario, - platform: current_platform(), - status: "pass", - measurements, - assertions, - }; - assert_eq!(report.status, "pass", "pass constructor status invariant"); - assert_eq!(report.schema, SCHEMA_VERSION, "schema version stable"); - report - } - - pub fn fail( - scenario: &'static str, - measurements: serde_json::Value, - assertions: Vec, - ) -> Self { - let report = Self { - schema: SCHEMA_VERSION, - scenario, - platform: current_platform(), - status: "fail", - measurements, - assertions, - }; - assert_eq!(report.status, "fail", "fail constructor status invariant"); - assert_eq!(report.schema, SCHEMA_VERSION, "schema version stable"); - report - } -} - -impl std::fmt::Display for ScenarioReport { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let serialised = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; - f.write_str(&serialised) - } -} - -pub const fn current_platform() -> &'static str { - if cfg!(target_os = "macos") { - "macos" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "windows") { - "windows" - } else { - "unknown" - } -} diff --git a/tools/integration/integration-driver/src/scenarios/screen_capture_to_pool.rs b/tools/integration/integration-driver/src/scenarios/screen_capture_to_pool.rs deleted file mode 100644 index b72582609..000000000 --- a/tools/integration/integration-driver/src/scenarios/screen_capture_to_pool.rs +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use fluxer_screen_frame_bus::frame_pool::{CpuFrameBuilder, FramePool}; -use serde_json::json; - -use super::ScenarioReport; - -const FRAME_WIDTH_PX: u32 = 1920; -const FRAME_HEIGHT_PX: u32 = 1080; -const FRAME_BYTES_PER_PIXEL: u32 = 4; -const STEADY_STATE_FRAMES: u32 = 64; - -pub fn run(_args: &[&str]) -> Result { - let bytes_per_slot = (FRAME_WIDTH_PX as usize) - .saturating_mul(FRAME_HEIGHT_PX as usize) - .saturating_mul(FRAME_BYTES_PER_PIXEL as usize); - assert!(bytes_per_slot > 0, "bytes per slot positive"); - assert!( - bytes_per_slot <= 1 << 28, - "bytes per slot within sanity cap" - ); - let pool = CpuFrameBuilder::build_pool(bytes_per_slot).map_err(|err| { - ScenarioReport::fail( - "screen_capture_to_pool", - json!({"reason": format!("pool init failed: {err:?}")}), - Vec::new(), - ) - })?; - let measurements = drive_steady_state(&pool); - let mut assertions = Vec::new(); - if measurements.steady_state_skipped != 0 { - return Err(ScenarioReport::fail( - "screen_capture_to_pool", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec![format!( - "steady state should not skip; observed {} skips", - measurements.steady_state_skipped - )], - )); - } - assertions.push("FramePool steady state shows zero skips".to_string()); - if measurements.in_flight_after_release != 0 { - return Err(ScenarioReport::fail( - "screen_capture_to_pool", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec![format!( - "expected in-flight to drop to zero after release; observed {}", - measurements.in_flight_after_release - )], - )); - } - assertions.push("PooledFrame Drop returns slot to free list".to_string()); - if measurements.acquired_total_post_run < STEADY_STATE_FRAMES as u64 { - return Err(ScenarioReport::fail( - "screen_capture_to_pool", - serde_json::to_value(&measurements).unwrap_or(json!({})), - vec!["acquired_total below expected".to_string()], - )); - } - assertions.push("acquired_total matches steady-state frame count".to_string()); - Ok(ScenarioReport::pass( - "screen_capture_to_pool", - serde_json::to_value(&measurements).unwrap_or(json!({})), - assertions, - )) -} - -#[derive(Debug, serde::Serialize)] -struct Measurements { - pool_capacity: usize, - steady_state_frames: u32, - steady_state_skipped: u64, - acquired_total_post_run: u64, - in_flight_after_release: u64, - bytes_per_slot: usize, -} - -fn drive_steady_state(pool: &FramePool) -> Measurements { - let capacity = pool.capacity(); - assert!(capacity > 0, "pool capacity positive"); - let baseline_skipped = pool.skipped_total(); - for _ in 0..STEADY_STATE_FRAMES { - let pooled = pool - .try_acquire() - .expect("steady-state acquire must succeed since prior frame dropped"); - let _ = pooled.slot_index(); - } - let in_flight = pool.currently_in_flight(); - let skipped = pool.skipped_total().saturating_sub(baseline_skipped); - Measurements { - pool_capacity: capacity, - steady_state_frames: STEADY_STATE_FRAMES, - steady_state_skipped: skipped, - acquired_total_post_run: pool.acquired_total(), - in_flight_after_release: in_flight, - bytes_per_slot: ((FRAME_WIDTH_PX as usize) - .saturating_mul(FRAME_HEIGHT_PX as usize) - .saturating_mul(FRAME_BYTES_PER_PIXEL as usize)), - } -} diff --git a/tools/integration/lib/transfer.sh b/tools/integration/lib/transfer.sh deleted file mode 100755 index 07467efaf..000000000 --- a/tools/integration/lib/transfer.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -INTEGRATION_TRANSFER_LIB_LOADED=1 - -integration_transfer_b64_encode() { - local src_path="$1" - if [ ! -f "$src_path" ]; then - printf 'integration_transfer: missing source %s\n' "$src_path" >&2 - return 64 - fi - if command -v base64 >/dev/null 2>&1; then - base64 < "$src_path" | tr -d '\n' - else - printf 'integration_transfer: base64 not available\n' >&2 - return 64 - fi -} - -integration_transfer_ssh_write_text() { - local ssh_target="$1" - local password="$2" - local dest_path="$3" - local src_path="$4" - local encoded - encoded="$(integration_transfer_b64_encode "$src_path")" || return $? - local powershell_cmd - powershell_cmd="\$bytes = [System.Convert]::FromBase64String('${encoded}'); [System.IO.File]::WriteAllBytes('${dest_path}', \$bytes)" - if command -v sshpass >/dev/null 2>&1; then - sshpass -p "$password" ssh -o StrictHostKeyChecking=accept-new "$ssh_target" "powershell -NoProfile -Command \"${powershell_cmd}\"" - else - ssh -o StrictHostKeyChecking=accept-new "$ssh_target" "powershell -NoProfile -Command \"${powershell_cmd}\"" - fi -} - -integration_transfer_ssh_run() { - local ssh_target="$1" - local password="$2" - local command_line="$3" - if command -v sshpass >/dev/null 2>&1; then - sshpass -p "$password" ssh -o StrictHostKeyChecking=accept-new "$ssh_target" "$command_line" - else - ssh -o StrictHostKeyChecking=accept-new "$ssh_target" "$command_line" - fi -} - -integration_transfer_ssh_powershell() { - local ssh_target="$1" - local password="$2" - local powershell_script="$3" - integration_transfer_ssh_run "$ssh_target" "$password" "powershell -NoProfile -Command \"${powershell_script}\"" -} - -integration_transfer_prlctl_exec() { - local vm_name="$1" - local command_line="$2" - if ! command -v prlctl >/dev/null 2>&1; then - printf 'integration_transfer: prlctl not available\n' >&2 - return 64 - fi - prlctl exec "$vm_name" "$command_line" -} diff --git a/tools/integration/results/.gitignore b/tools/integration/results/.gitignore deleted file mode 100644 index 1effb3136..000000000 --- a/tools/integration/results/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.json -latest-summary.txt diff --git a/tools/integration/runner.sh b/tools/integration/runner.sh deleted file mode 100755 index f2c228c1f..000000000 --- a/tools/integration/runner.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -PLATFORM="" -ONLY_SCENARIO="" -DRY_RUN="0" - -for arg in "$@"; do - case "$arg" in - --platform=*) PLATFORM="${arg#--platform=}" ;; - --only=*) ONLY_SCENARIO="${arg#--only=}" ;; - --dry-run) DRY_RUN="1" ;; - -h|--help) - cat <<'EOF' -usage: runner.sh --platform= [--only=] [--dry-run] - - --platform=NAME Required. Selects the harness matrix entry. - --only=NAME Optional. Run a single scenario (script basename without .sh). - --dry-run Print which scenarios would run without invoking them. -EOF - exit 0 - ;; - *) - printf 'runner.sh: unknown argument %s\n' "$arg" >&2 - exit 64 - ;; - esac -done - -if [ -z "$PLATFORM" ]; then - printf 'runner.sh: --platform= is required\n' >&2 - exit 64 -fi - -RUNNER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -RESULTS_DIR="${RUNNER_DIR}/results" -SCENARIOS_DIR="${RUNNER_DIR}/scenarios" -mkdir -p "$RESULTS_DIR" - -case "$PLATFORM" in - macos) - SCENARIO_LIST=( - scenario_audio_capture_to_mix - scenario_screen_capture_to_pool - scenario_gpu_device_loss_recovery - scenario_event_log_replay_determinism - scenario_ffi_airlock_negative_space - scenario_encoder_handoff_dryrun - ) - ;; - linux-fedora) - SCENARIO_LIST=( - scenario_audio_capture_to_mix - scenario_screen_capture_to_pool - scenario_gpu_device_loss_recovery - scenario_event_log_replay_determinism - scenario_ffi_airlock_negative_space - scenario_encoder_handoff_dryrun - ) - ;; - windows-tailnet) - SCENARIO_LIST=( - scenario_audio_capture_to_mix - scenario_screen_capture_to_pool - scenario_gpu_device_loss_recovery - scenario_event_log_replay_determinism - scenario_ffi_airlock_negative_space - scenario_encoder_handoff_dryrun - ) - ;; - windows-vm) - SCENARIO_LIST=( - scenario_audio_capture_to_mix - scenario_screen_capture_to_pool - scenario_event_log_replay_determinism - scenario_ffi_airlock_negative_space - scenario_encoder_handoff_dryrun - ) - ;; - *) - printf 'runner.sh: unknown platform %s; expected macos|linux-fedora|windows-tailnet|windows-vm\n' "$PLATFORM" >&2 - exit 64 - ;; -esac - -if [ -n "$ONLY_SCENARIO" ]; then - FILTERED=() - for s in "${SCENARIO_LIST[@]}"; do - if [ "$s" = "$ONLY_SCENARIO" ]; then - FILTERED+=("$s") - fi - done - SCENARIO_LIST=("${FILTERED[@]}") - if [ "${#SCENARIO_LIST[@]}" -eq 0 ]; then - printf 'runner.sh: scenario %s not in platform matrix for %s\n' "$ONLY_SCENARIO" "$PLATFORM" >&2 - exit 64 - fi -fi - -TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)" -RESULT_FILE="${RESULTS_DIR}/${PLATFORM}-${TIMESTAMP}.json" -SUMMARY_FILE="${RESULTS_DIR}/latest-summary.txt" - -if [ "$DRY_RUN" = "1" ]; then - printf 'Would run on platform=%s:\n' "$PLATFORM" - for s in "${SCENARIO_LIST[@]}"; do - printf ' %s\n' "$s" - done - exit 0 -fi - -PASSED=0 -FAILED=0 -RESULT_ENTRIES=() -SUMMARY_LINES=() - -emit_result_entry() { - local scenario="$1" - local status="$2" - local body="$3" - local entry - entry="{\"scenario\":\"${scenario}\",\"status\":\"${status}\",\"output\":${body}}" - RESULT_ENTRIES+=("$entry") -} - -for scenario in "${SCENARIO_LIST[@]}"; do - script="${SCENARIOS_DIR}/${scenario}.sh" - if [ ! -x "$script" ]; then - chmod +x "$script" 2>/dev/null || true - fi - if [ ! -f "$script" ]; then - emit_result_entry "$scenario" "missing" "{\"reason\":\"scenario script not found at ${script}\"}" - SUMMARY_LINES+=("[MISS] ${scenario}") - FAILED=$((FAILED + 1)) - continue - fi - set +e - raw_output="$("$script" 2>&1)" - exit_code=$? - set -e - last_json_line="$(printf '%s\n' "$raw_output" | awk '/^\{.*\}$/ {last=$0} END {print last}')" - if [ -z "$last_json_line" ]; then - emit_result_entry "$scenario" "fail" "{\"reason\":\"scenario produced no JSON line\",\"raw\":$(jq -Rs . <<<"$raw_output" 2>/dev/null || echo '\"\"')}" - SUMMARY_LINES+=("[FAIL] ${scenario} (no json)") - FAILED=$((FAILED + 1)) - continue - fi - if [ "$exit_code" -eq 0 ]; then - emit_result_entry "$scenario" "pass" "$last_json_line" - SUMMARY_LINES+=("[PASS] ${scenario}") - PASSED=$((PASSED + 1)) - else - emit_result_entry "$scenario" "fail" "$last_json_line" - SUMMARY_LINES+=("[FAIL] ${scenario} (exit ${exit_code})") - FAILED=$((FAILED + 1)) - fi -done - -ENTRIES_JOINED="$(IFS=,; echo "${RESULT_ENTRIES[*]}")" -printf '{"schema":1,"platform":"%s","timestamp":"%s","passed":%d,"failed":%d,"results":[%s]}\n' \ - "$PLATFORM" "$TIMESTAMP" "$PASSED" "$FAILED" "$ENTRIES_JOINED" > "$RESULT_FILE" - -{ - printf 'platform: %s\n' "$PLATFORM" - printf 'timestamp: %s\n' "$TIMESTAMP" - printf 'passed: %d\n' "$PASSED" - printf 'failed: %d\n' "$FAILED" - printf 'scenarios:\n' - for line in "${SUMMARY_LINES[@]}"; do - printf ' %s\n' "$line" - done - printf 'result_file: %s\n' "$RESULT_FILE" -} > "$SUMMARY_FILE" - -cat "$SUMMARY_FILE" - -if [ "$FAILED" -gt 0 ]; then - exit 1 -fi -exit 0 diff --git a/tools/integration/scenarios/_common.sh b/tools/integration/scenarios/_common.sh deleted file mode 100755 index 474cf0187..000000000 --- a/tools/integration/scenarios/_common.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -if [ -z "${INTEGRATION_HARNESS_ROOT:-}" ]; then - SCENARIO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - INTEGRATION_HARNESS_ROOT="$(cd "${SCENARIO_DIR}/.." && pwd)" -fi -export INTEGRATION_HARNESS_ROOT - -INTEGRATION_REPO_ROOT="$(cd "${INTEGRATION_HARNESS_ROOT}/../.." && pwd)" -export INTEGRATION_REPO_ROOT - -detect_platform() { - local uname_out - uname_out="$(uname -s 2>/dev/null || echo unknown)" - case "$uname_out" in - Darwin) echo "macos" ;; - Linux) echo "linux" ;; - MINGW*|MSYS*|CYGWIN*|Windows_NT) echo "windows" ;; - *) - if [ -n "${OS:-}" ] && [ "$OS" = "Windows_NT" ]; then - echo "windows" - else - echo "unknown" - fi - ;; - esac -} - -driver_target_dir() { - local platform="$1" - echo "${INTEGRATION_HARNESS_ROOT}/integration-driver/target-${platform}" -} - -driver_binary_path() { - local platform="$1" - local target_dir - target_dir="$(driver_target_dir "$platform")" - if [ "$platform" = "windows" ]; then - echo "${target_dir}/debug/integration-driver.exe" - else - echo "${target_dir}/debug/integration-driver" - fi -} - -driver_binary_runs() { - local binary="$1" - if [ ! -f "$binary" ]; then - return 1 - fi - if "$binary" --help >/dev/null 2>&1; then - return 0 - fi - if "$binary" 2>/dev/null | head -c 1 >/dev/null; then - return 0 - fi - return 1 -} - -ensure_driver_built() { - local platform - platform="$(detect_platform)" - local binary - binary="$(driver_binary_path "$platform")" - local target_dir - target_dir="$(driver_target_dir "$platform")" - if ! driver_binary_runs "$binary"; then - ( cd "${INTEGRATION_HARNESS_ROOT}/integration-driver" && CARGO_TARGET_DIR="$target_dir" cargo build >&2 ) || return $? - fi - if [ ! -f "$binary" ]; then - printf '{"schema":1,"scenario":"%s","platform":"%s","status":"fail","measurements":{"reason":"driver binary missing after build"},"assertions":[]}\n' \ - "${SCENARIO_NAME:-unknown}" "$platform" - return 70 - fi - echo "$binary" -} - -run_rust_subcommand() { - local subcommand="$1" - shift || true - local binary - binary="$(ensure_driver_built)" || return $? - "$binary" "$subcommand" "$@" -} - -run_ts_driver() { - local entry="$1" - shift || true - local resolved="${INTEGRATION_REPO_ROOT}/${entry}" - if [ ! -f "$resolved" ]; then - printf '{"schema":1,"scenario":"%s","platform":"%s","status":"fail","measurements":{"reason":"ts driver entry missing: %s"},"assertions":[]}\n' \ - "${SCENARIO_NAME:-unknown}" "$(detect_platform)" "$resolved" - return 70 - fi - local pnpm_command - pnpm_command="$(resolve_pnpm_command)" || { - printf '{"schema":1,"scenario":"%s","platform":"%s","status":"fail","measurements":{"reason":"pnpm not available"},"assertions":[]}\n' \ - "${SCENARIO_NAME:-unknown}" "$(detect_platform)" - return 70 - } - if should_use_dlx_tsx; then - ( cd "$INTEGRATION_REPO_ROOT" && $pnpm_command dlx tsx "$entry" "$@" ) - elif ( cd "$INTEGRATION_REPO_ROOT" && $pnpm_command exec tsx --version >/dev/null 2>&1 ); then - ( cd "$INTEGRATION_REPO_ROOT" && $pnpm_command exec tsx "$entry" "$@" ) - else - ( cd "$INTEGRATION_REPO_ROOT" && $pnpm_command dlx tsx "$entry" "$@" ) - fi -} - -resolve_pnpm_command() { - if command -v pnpm >/dev/null 2>&1; then - echo "pnpm" - return 0 - fi - if command -v corepack >/dev/null 2>&1; then - echo "corepack pnpm" - return 0 - fi - return 1 -} - -should_use_dlx_tsx() { - if [ "${FLUXER_INTEGRATION_TSX_MODE:-}" = "dlx" ]; then - return 0 - fi - case "$INTEGRATION_REPO_ROOT" in - /media/psf/*|/mnt/psf/*) - return 0 - ;; - *) - return 1 - ;; - esac -} diff --git a/tools/integration/scenarios/scenario_audio_capture_to_mix.sh b/tools/integration/scenarios/scenario_audio_capture_to_mix.sh deleted file mode 100755 index 29065a99f..000000000 --- a/tools/integration/scenarios/scenario_audio_capture_to_mix.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -SCENARIO_NAME="scenario_audio_capture_to_mix" -export SCENARIO_NAME - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" - -run_rust_subcommand "audio-capture-to-mix" diff --git a/tools/integration/scenarios/scenario_encoder_handoff_dryrun.sh b/tools/integration/scenarios/scenario_encoder_handoff_dryrun.sh deleted file mode 100755 index d1990707a..000000000 --- a/tools/integration/scenarios/scenario_encoder_handoff_dryrun.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -SCENARIO_NAME="scenario_encoder_handoff_dryrun" -export SCENARIO_NAME - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" - -run_rust_subcommand "encoder-handoff-dryrun" diff --git a/tools/integration/scenarios/scenario_event_log_replay_determinism.sh b/tools/integration/scenarios/scenario_event_log_replay_determinism.sh deleted file mode 100755 index 11bc496ae..000000000 --- a/tools/integration/scenarios/scenario_event_log_replay_determinism.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -SCENARIO_NAME="scenario_event_log_replay_determinism" -export SCENARIO_NAME - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" - -EXPECTED_FILE="${INTEGRATION_HARNESS_ROOT}/expected/event_log_replay_determinism.json" -EXPECTED_HASH="" -if [ -f "$EXPECTED_FILE" ]; then - EXPECTED_HASH="$(grep -E '"snapshot_hash"' "$EXPECTED_FILE" | head -1 | sed -E 's/.*"snapshot_hash"[^"]*"([^"]+)".*/\1/' || true)" -fi - -if [ -n "$EXPECTED_HASH" ]; then - run_ts_driver "tools/integration/ts-driver/event_log_replay_determinism.ts" "$EXPECTED_HASH" -else - run_ts_driver "tools/integration/ts-driver/event_log_replay_determinism.ts" -fi diff --git a/tools/integration/scenarios/scenario_ffi_airlock_negative_space.sh b/tools/integration/scenarios/scenario_ffi_airlock_negative_space.sh deleted file mode 100755 index 145a802c7..000000000 --- a/tools/integration/scenarios/scenario_ffi_airlock_negative_space.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -SCENARIO_NAME="scenario_ffi_airlock_negative_space" -export SCENARIO_NAME - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" - -run_rust_subcommand "ffi-airlock-negative" "invalid-sample-rate" diff --git a/tools/integration/scenarios/scenario_gpu_device_loss_recovery.sh b/tools/integration/scenarios/scenario_gpu_device_loss_recovery.sh deleted file mode 100755 index 529b846bc..000000000 --- a/tools/integration/scenarios/scenario_gpu_device_loss_recovery.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -SCENARIO_NAME="scenario_gpu_device_loss_recovery" -export SCENARIO_NAME - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" - -run_rust_subcommand "gpu-device-loss-recovery" diff --git a/tools/integration/scenarios/scenario_screen_capture_to_pool.sh b/tools/integration/scenarios/scenario_screen_capture_to_pool.sh deleted file mode 100755 index 161e48312..000000000 --- a/tools/integration/scenarios/scenario_screen_capture_to_pool.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -u -set -o pipefail - -SCENARIO_NAME="scenario_screen_capture_to_pool" -export SCENARIO_NAME - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" - -run_rust_subcommand "screen-capture-to-pool" diff --git a/tools/integration/ts-driver/event_log_replay_determinism.ts b/tools/integration/ts-driver/event_log_replay_determinism.ts deleted file mode 100644 index 08cd3efa2..000000000 --- a/tools/integration/ts-driver/event_log_replay_determinism.ts +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import assert from 'node:assert/strict'; -import {VoiceEngineV2Simulator} from '../../../packages/voice_engine_v2/src/simulation/Simulator'; -import {defineNetworkPartitionDuringScreenShareScenario} from '../../../packages/voice_engine_v2/src/simulation/scenarios/networkPartitionDuringScreenShare'; - -const REPORT_SCHEMA = 1; -const SCENARIO_NAME = 'event_log_replay_determinism'; -const SEED = 4242; - -interface Report { - schema: number; - scenario: string; - platform: string; - status: 'pass' | 'fail'; - measurements: Record; - assertions: Array; -} - -function platform(): string { - if (process.platform === 'darwin') return 'macos'; - if (process.platform === 'linux') return 'linux'; - if (process.platform === 'win32') return 'windows'; - return 'unknown'; -} - -async function runOnce(): Promise<{snapshotHash: string; eventCount: number; finalTick: number}> { - const scenario = defineNetworkPartitionDuringScreenShareScenario(SEED); - assert.ok(scenario.workload, 'scenario must yield a workload'); - const simulator = new VoiceEngineV2Simulator({ - seed: SEED, - workload: scenario.workload, - faults: scenario.faultPlan, - mode: scenario.mode, - }); - const result = await simulator.run(); - assert.ok(typeof result.snapshotHash === 'string', 'snapshot hash must be a string'); - assert.equal(result.snapshotHash.length, 8, 'snapshot hash is eight hex characters'); - return { - snapshotHash: result.snapshotHash, - eventCount: result.eventLog.length, - finalTick: result.finalTick, - }; -} - -async function main(): Promise { - const expectedHashArg = process.argv[2] ?? null; - const first = await runOnce(); - const second = await runOnce(); - const assertions: Array = []; - if (first.snapshotHash !== second.snapshotHash) { - emit({ - schema: REPORT_SCHEMA, - scenario: SCENARIO_NAME, - platform: platform(), - status: 'fail', - measurements: {first, second}, - assertions: ['two runs of the same scenario must agree on snapshot hash'], - }); - process.exit(1); - } - assertions.push('two runs of the same scenario agree on snapshot hash'); - if (first.eventCount !== second.eventCount) { - emit({ - schema: REPORT_SCHEMA, - scenario: SCENARIO_NAME, - platform: platform(), - status: 'fail', - measurements: {first, second}, - assertions: ['event log length must agree across runs'], - }); - process.exit(1); - } - assertions.push('event log length agrees across runs'); - if (expectedHashArg !== null && expectedHashArg !== first.snapshotHash) { - emit({ - schema: REPORT_SCHEMA, - scenario: SCENARIO_NAME, - platform: platform(), - status: 'fail', - measurements: {expected: expectedHashArg, observed: first.snapshotHash, runs: [first, second]}, - assertions: [`snapshot hash mismatch: expected ${expectedHashArg}, observed ${first.snapshotHash}`], - }); - process.exit(1); - } - if (expectedHashArg !== null) { - assertions.push('snapshot hash matches expected fixture'); - } - emit({ - schema: REPORT_SCHEMA, - scenario: SCENARIO_NAME, - platform: platform(), - status: 'pass', - measurements: { - snapshot_hash: first.snapshotHash, - event_count: first.eventCount, - final_tick: first.finalTick, - seed: SEED, - }, - assertions, - }); -} - -function emit(report: Report): void { - process.stdout.write(`${JSON.stringify(report)}\n`); -} - -main().catch((error) => { - emit({ - schema: REPORT_SCHEMA, - scenario: SCENARIO_NAME, - platform: platform(), - status: 'fail', - measurements: {error: String(error)}, - assertions: ['driver threw before finishing scenario'], - }); - process.exit(1); -});