diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 08fea8e..7c9cf89 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,99 +1,31 @@ -# Contributing to GFNClient +# Contributing to OpenNOW -Thank you for your interest in contributing to GFNClient! This document provides guidelines and instructions for contributing. +Thanks for contributing. -## Getting Started +## Project Layout -### Prerequisites +- Active desktop client: `opennow-stable/` (Electron + React + TypeScript) -- [Rust](https://rustup.rs/) (latest stable) -- [Bun](https://bun.sh/) (latest) -- Platform-specific dependencies for Tauri - -### Development Setup +## Local Setup ```bash -# Clone the repository -git clone https://github.com/zortos293/GFNClient.git -cd GFNClient - -# Install dependencies -bun install - -# Run in development mode -bun tauri dev +git clone https://github.com/OpenCloudGaming/OpenNOW.git +cd OpenNOW/opennow-stable +npm install +npm run dev ``` -## How to Contribute - -### Reporting Bugs - -1. Check if the bug has already been reported in [Issues](https://github.com/zortos293/GFNClient/issues) -2. If not, create a new issue using the **Bug Report** template -3. Provide as much detail as possible, including: - - Steps to reproduce - - Expected vs actual behavior - - Platform and version information - - Logs and screenshots - -### Suggesting Features - -1. Check if the feature has already been requested in [Issues](https://github.com/zortos293/GFNClient/issues) -2. Create a new issue using the **Feature Request** template -3. Clearly describe the use case and proposed solution - -### Submitting Pull Requests - -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/your-feature-name` -3. Make your changes -4. Test your changes locally on your platform -5. Commit with clear, descriptive messages -6. Push to your fork -7. Open a Pull Request using the PR template - -## Code Guidelines - -### Rust (Backend) - -- Follow standard Rust conventions -- Use `cargo fmt` before committing -- Run `cargo clippy` and address warnings -- Add appropriate error handling - -### TypeScript (Frontend) +## Build and Checks -- Use TypeScript strict mode -- Follow existing code patterns -- Use meaningful variable and function names - -### Commit Messages - -Use clear, descriptive commit messages: - -``` -type: short description - -Longer description if needed - -Fixes #123 +```bash +npm run typecheck +npm run build +npm run dist ``` -Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` - -## Platform-Specific Notes - -### macOS -- H.265/HEVC and Opus Stereo features are macOS-only due to VideoToolbox/CoreAudio -- Fullscreen uses Tauri's window API instead of browser API - -### Windows/Linux -- H.265 codec option is disabled (WebRTC limitation) -- Test with appropriate GPU drivers - -## Questions? - -- Open a **Question** issue -- Check existing issues and discussions +## Pull Requests -Thank you for contributing! +1. Create a feature branch +2. Keep commits focused and clear +3. Ensure `typecheck` and `build` pass locally +4. Open a PR with a concise summary diff --git a/.github/workflows/auto-build.yml b/.github/workflows/auto-build.yml index 155d22e..7510d03 100644 --- a/.github/workflows/auto-build.yml +++ b/.github/workflows/auto-build.yml @@ -1,1198 +1,134 @@ -name: Auto Build and Release +name: auto-build on: + workflow_dispatch: + pull_request: + paths: + - "opennow-stable/**" + - ".github/workflows/auto-build.yml" push: branches: - main - dev - - Native - paths-ignore: - - "**.md" - - "LICENSE" - - ".gitignore" - workflow_dispatch: + tags: + - "v*" + - "opennow-stable-v*" + paths: + - "opennow-stable/**" + - ".github/workflows/auto-build.yml" -permissions: - contents: write - -env: - CARGO_TERM_COLOR: always +concurrency: + group: auto-build-${{ github.ref }} + cancel-in-progress: true jobs: - # First job: Calculate the version and generate release notes - get-version: - # Skip build if commit message contains [skip ci], [no build], or [skip build] - if: | - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[no build]') && - !contains(github.event.head_commit.message, '[skip build]') - runs-on: ubuntu-latest - outputs: - version: ${{ steps.get_version.outputs.version }} - version_number: ${{ steps.get_version.outputs.version_number }} - release_notes: ${{ steps.generate_notes.outputs.release_notes }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get latest release version - id: get_version - run: | - # Get the latest release tag, default to v0.1.0 if none exists - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.1.0") - echo "Latest tag: $LATEST_TAG" - echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT - - # Extract version numbers (remove 'v' prefix and any suffix like '-dev') - VERSION=$(echo "$LATEST_TAG" | sed 's/^v//' | sed 's/-.*$//') - echo "Parsed version: $VERSION" - - # Split into major, minor, patch - MAJOR=$(echo "$VERSION" | cut -d. -f1 | grep -E '^[0-9]+$' || echo "0") - MINOR=$(echo "$VERSION" | cut -d. -f2 | grep -E '^[0-9]+$' || echo "1") - PATCH=$(echo "$VERSION" | cut -d. -f3 | grep -E '^[0-9]+$' || echo "0") - - # Ensure values are valid numbers (default if empty/invalid) - MAJOR=${MAJOR:-0} - MINOR=${MINOR:-1} - PATCH=${PATCH:-0} - - echo "Parsed: MAJOR=$MAJOR, MINOR=$MINOR, PATCH=$PATCH" - - # Increment patch version - NEW_PATCH=$((PATCH + 1)) - - # Force minimum version to v0.2.0 - if [ "$MAJOR" -eq 0 ] && [ "$MINOR" -lt 2 ]; then - echo "Current version ($MAJOR.$MINOR.$PATCH) is older than 0.2.0. Bumping to v0.2.0" - NEW_VERSION="v0.2.0" - VERSION_NUMBER="0.2.0" - else - NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}" - VERSION_NUMBER="${MAJOR}.${MINOR}.${NEW_PATCH}" - fi - - echo "New version: $NEW_VERSION" - echo "Version number: $VERSION_NUMBER" - - echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "version_number=$VERSION_NUMBER" >> $GITHUB_OUTPUT - - - name: Get commits since last release - id: get_commits - run: | - LATEST_TAG="${{ steps.get_version.outputs.latest_tag }}" - - # Get commits since last tag (or all commits if no tag exists) - # Format: subject line + body (indented) for better AI context - FORMAT="- %s (%h)%n%w(0,2,2)%b" - if git rev-parse "$LATEST_TAG" >/dev/null 2>&1; then - git log "$LATEST_TAG"..HEAD --pretty=format:"$FORMAT" --no-merges | head -150 > commits.txt - else - git log --pretty=format:"$FORMAT" --no-merges | head -150 > commits.txt - fi - - # Clean up excessive blank lines from commits without bodies - sed -i 's/^[[:space:]]*$//g' commits.txt - cat -s commits.txt > commits_clean.txt && mv commits_clean.txt commits.txt - - # Ensure file ends with newline (git log --pretty=format doesn't add one) - echo "" >> commits.txt - - # Output commits for next step - echo "commits<> $GITHUB_OUTPUT - cat commits.txt >> $GITHUB_OUTPUT - echo "COMMITS_HEREDOC_END" >> $GITHUB_OUTPUT - - - name: Generate AI release notes - id: generate_notes - env: - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - COMMITS: ${{ steps.get_commits.outputs.commits }} - VERSION: ${{ steps.get_version.outputs.version }} - run: | - # Create the prompt for OpenRouter (commits are safely passed via env var) - PROMPT="You are a release notes generator for OpenNOW, an open source native GeForce NOW client built with Rust. Generate clean, user-friendly release notes from these git commits. Categorize them into sections: Added (new features), Changed (updates/improvements), Fixed (bug fixes), and Removed (if any). Keep descriptions concise and user-focused. If a commit doesn't fit these categories, skip it. Only include sections that have items. Format in markdown. Do not include any introduction or conclusion text, just the categorized list." - PROMPT=$(printf "%s\n\nGit commits:\n%s" "$PROMPT" "$COMMITS") - - # Build JSON payload safely using jq - PAYLOAD=$(jq -n \ - --arg prompt "$PROMPT" \ - '{ - "model": "google/gemini-3-flash-preview", - "messages": [{"role": "user", "content": $prompt}], - "max_tokens": 1000 - }') - - # Call OpenRouter API - RESPONSE=$(curl -s -X POST "https://openrouter.ai/api/v1/chat/completions" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $OPENROUTER_API_KEY" \ - -H "HTTP-Referer: https://github.com/zortos293/GFNClient" \ - -H "X-Title: OpenNOW Release Notes" \ - -d "$PAYLOAD") - - # Extract the content from response - NOTES=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty') - - # If API failed, create basic notes from commits - if [ -z "$NOTES" ]; then - NOTES=$(printf "### Changes\n\n%s" "$COMMITS") - fi - - # Save to file for multi-line output (ensure trailing newline) - echo "$NOTES" > release_notes.md - echo "" >> release_notes.md - - # Output release notes - echo "release_notes<> $GITHUB_OUTPUT - cat release_notes.md >> $GITHUB_OUTPUT - echo "NOTES_HEREDOC_END" >> $GITHUB_OUTPUT - - # Second job: Build on all platforms build: - needs: get-version + name: ${{ matrix.label }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - - platform: ubuntu-22.04 - target: linux - arch: x86_64 - rust_target: x86_64-unknown-linux-gnu - - platform: ubuntu-22.04 - target: linux-arm64 - arch: aarch64 - rust_target: aarch64-unknown-linux-gnu - - platform: macos-latest - target: macos - arch: arm64 - rust_target: aarch64-apple-darwin - # NOTE: Legacy macOS Intel build temporarily disabled - macOS-13 runners retired - # TODO: Re-enable when macos-15-intel runners are available or use cross-compilation - # - platform: macos-13-large - # target: macos-intel - # arch: x86_64 - # rust_target: x86_64-apple-darwin - # features: legacy-macos - - platform: windows-latest - target: windows - arch: x86_64 - rust_target: x86_64-pc-windows-msvc - - platform: windows-latest - target: windows-arm64 - arch: aarch64 - rust_target: aarch64-pc-windows-msvc - - runs-on: ${{ matrix.platform }} + - label: windows + os: blacksmith-2vcpu-windows-2025 + builder_args: "--win nsis portable" + - label: macos-x64 + os: macos-latest + builder_args: "--mac dmg zip --x64" + - label: macos-arm64 + os: macos-latest + builder_args: "--mac dmg zip --arm64" + - label: linux-x64 + os: blacksmith-2vcpu-ubuntu-2404 + builder_args: "--linux AppImage deb --x64" + - label: linux-arm64 + os: blacksmith-2vcpu-ubuntu-2404-arm + builder_args: "--linux AppImage deb --arm64" + + defaults: + run: + working-directory: opennow-stable + + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron + ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder + npm_config_audit: "false" + npm_config_fund: "false" + # Use system fpm on arm64 (no prebuilt binaries available) + USE_SYSTEM_FPM: ${{ matrix.label == 'linux-arm64' && 'true' || 'false' }} steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 - - name: Update version in opennow-streamer/Cargo.toml - shell: bash - run: | - VERSION="${{ needs.get-version.outputs.version_number }}" - echo "Setting version to: $VERSION" - - # Validate version is a valid semver (x.y.z format) - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Invalid version format '$VERSION', using fallback 0.1.0" - VERSION="0.1.0" - fi - - # Update opennow-streamer/Cargo.toml - only replace the package version (in first 10 lines) - # Using sed's line range to avoid changing dependency versions like wgpu-hal - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "1,10s/^version = \"[^\"]*\"/version = \"$VERSION\"/" opennow-streamer/Cargo.toml - else - sed -i "1,10s/^version = \"[^\"]*\"/version = \"$VERSION\"/" opennow-streamer/Cargo.toml - fi - - # Verify the version was set correctly - echo "=== Verifying version ===" - echo "opennow-streamer/Cargo.toml: $(grep '^version' opennow-streamer/Cargo.toml | head -1)" - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - - # macOS builds natively for ARM64 (macos-latest is ARM64) - # Intel Mac users can use Rosetta 2 to run ARM64 binaries - - - name: Add Linux ARM64 target - if: matrix.target == 'linux-arm64' - run: rustup target add aarch64-unknown-linux-gnu - - - name: Add Windows ARM64 target - if: matrix.target == 'windows-arm64' - run: rustup target add aarch64-pc-windows-msvc - - - name: Rust cache - uses: Swatinem/rust-cache@v2 - with: - workspaces: "opennow-streamer -> target" - key: ${{ matrix.target }} - - # ==================== GStreamer Installation (Windows/Linux) ==================== - - - name: Install GStreamer (Windows x64) - if: matrix.target == 'windows' - shell: pwsh - run: | - # Install GStreamer runtime and development packages via Chocolatey - # (avoids bot protection on direct downloads from gstreamer.freedesktop.org) - Write-Host "Installing GStreamer runtime via Chocolatey..." - choco install gstreamer -y --no-progress - - Write-Host "Installing GStreamer development package via Chocolatey..." - choco install gstreamer-devel -y --no-progress - - # Refresh environment variables from registry - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - refreshenv - - # Also manually refresh Path - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") - - # Get GStreamer path from environment variable set by installer - $gstDir = [System.Environment]::GetEnvironmentVariable("GSTREAMER_1_0_ROOT_MSVC_X86_64","Machine") - - # Fallback to common installation paths - if (-not $gstDir -or -not (Test-Path $gstDir)) { - $possiblePaths = @( - "C:\gstreamer\1.0\msvc_x86_64", - "C:\gstreamer\1.0\x86_64", - "C:\Program Files\gstreamer\1.0\msvc_x86_64" - ) - foreach ($path in $possiblePaths) { - if (Test-Path $path) { - $gstDir = $path - Write-Host "Found GStreamer at fallback path: $gstDir" - break - } - } - } - - if (-not $gstDir -or -not (Test-Path $gstDir)) { - Write-Error "GStreamer installation not found!" - exit 1 - } - - Write-Host "GStreamer directory: $gstDir" - - # Set environment variables for build and bundling - echo "GSTREAMER_1_0_ROOT_MSVC_X86_64=$gstDir" >> $env:GITHUB_ENV - echo "PKG_CONFIG_PATH=$gstDir\lib\pkgconfig" >> $env:GITHUB_ENV - echo "PATH=$gstDir\bin;$env:PATH" >> $env:GITHUB_ENV - echo "GST_PLUGIN_PATH=$gstDir\lib\gstreamer-1.0" >> $env:GITHUB_ENV - - # Store paths for later bundling - echo "GSTREAMER_BIN_DIR=$gstDir\bin" >> $env:GITHUB_ENV - echo "GSTREAMER_LIB_DIR=$gstDir\lib" >> $env:GITHUB_ENV - echo "GSTREAMER_PLUGIN_DIR=$gstDir\lib\gstreamer-1.0" >> $env:GITHUB_ENV - - Write-Host "`n=== GStreamer directory structure ===" - Get-ChildItem $gstDir -ErrorAction SilentlyContinue - - Write-Host "`n=== GStreamer bin directory ===" - Get-ChildItem "$gstDir\bin" -Filter "*.dll" | Select-Object -First 10 - - Write-Host "`n=== Verifying GStreamer installation ===" - & "$gstDir\bin\gst-inspect-1.0.exe" --version - - Write-Host "GStreamer x64 setup complete" - - - name: Install GStreamer (Windows ARM64) - if: matrix.target == 'windows-arm64' - shell: pwsh - run: | - # Install GStreamer runtime and development packages via Chocolatey - # Note: Chocolatey installs x64 version which will run under emulation on ARM64 - Write-Host "Installing GStreamer runtime via Chocolatey..." - choco install gstreamer -y --no-progress - - Write-Host "Installing GStreamer development package via Chocolatey..." - choco install gstreamer-devel -y --no-progress - - # Refresh environment variables from registry - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - refreshenv - - # Also manually refresh Path - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") - - # Get GStreamer path from environment variable set by installer - $gstDir = [System.Environment]::GetEnvironmentVariable("GSTREAMER_1_0_ROOT_MSVC_X86_64","Machine") - - # Fallback to common installation paths - if (-not $gstDir -or -not (Test-Path $gstDir)) { - $possiblePaths = @( - "C:\gstreamer\1.0\msvc_x86_64", - "C:\gstreamer\1.0\x86_64", - "C:\Program Files\gstreamer\1.0\msvc_x86_64" - ) - foreach ($path in $possiblePaths) { - if (Test-Path $path) { - $gstDir = $path - Write-Host "Found GStreamer at fallback path: $gstDir" - break - } - } - } - - if (-not $gstDir -or -not (Test-Path $gstDir)) { - Write-Error "GStreamer installation not found!" - exit 1 - } - - Write-Host "GStreamer directory: $gstDir" - - # Set environment variables for build and bundling - echo "GSTREAMER_1_0_ROOT_MSVC_X86_64=$gstDir" >> $env:GITHUB_ENV - echo "PKG_CONFIG_PATH=$gstDir\lib\pkgconfig" >> $env:GITHUB_ENV - echo "PATH=$gstDir\bin;$env:PATH" >> $env:GITHUB_ENV - echo "GST_PLUGIN_PATH=$gstDir\lib\gstreamer-1.0" >> $env:GITHUB_ENV - - # Store paths for later bundling - echo "GSTREAMER_BIN_DIR=$gstDir\bin" >> $env:GITHUB_ENV - echo "GSTREAMER_LIB_DIR=$gstDir\lib" >> $env:GITHUB_ENV - echo "GSTREAMER_PLUGIN_DIR=$gstDir\lib\gstreamer-1.0" >> $env:GITHUB_ENV - - Write-Host "GStreamer ARM64 setup complete (using x64 under emulation)" - - # ==================== FFmpeg Installation (macOS only) ==================== - - - name: Install FFmpeg (macOS) - if: matrix.target == 'macos' - run: | - brew install ffmpeg pkg-config - echo "FFmpeg installed via Homebrew" - # Note: macOS uses FFmpeg for video (VideoToolbox) and audio (Opus) decoding - # GStreamer is NOT used on macOS - only on Linux and Windows x64 - - # ==================== GStreamer Installation (Linux) ==================== - - - name: Install GStreamer (Linux x64) - if: matrix.target == 'linux' - run: | - sudo apt-get update - sudo apt-get install -y \ - pkg-config \ - libasound2-dev \ - libx11-dev \ - libxrandr-dev \ - libxi-dev \ - libgl1-mesa-dev \ - libudev-dev \ - clang \ - libclang-dev \ - libunwind-dev \ - libgstreamer1.0-dev \ - libgstreamer-plugins-base1.0-dev \ - libgstreamer-plugins-bad1.0-dev \ - gstreamer1.0-plugins-base \ - gstreamer1.0-plugins-good \ - gstreamer1.0-plugins-bad \ - gstreamer1.0-plugins-ugly \ - gstreamer1.0-libav \ - gstreamer1.0-tools - - echo "GStreamer development libraries installed" - gst-inspect-1.0 --version - - - name: Install GStreamer (Linux ARM64) - if: matrix.target == 'linux-arm64' - run: | - sudo apt-get update - - # Enable ARM64 architecture for cross-compilation - sudo dpkg --add-architecture arm64 - - # Add ARM64 repositories - sudo sed -i 's/^deb /deb [arch=amd64] /' /etc/apt/sources.list - echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list - echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list - - sudo apt-get update - - # Install cross-compilation toolchain and build dependencies - sudo apt-get install -y \ - gcc-aarch64-linux-gnu \ - g++-aarch64-linux-gnu \ - pkg-config \ - clang \ - libclang-dev \ - wget \ - libssl-dev:arm64 \ - libasound2-dev:arm64 \ - libudev-dev:arm64 \ - libunwind-dev:arm64 \ - libgstreamer1.0-dev:arm64 \ - libgstreamer-plugins-base1.0-dev:arm64 \ - libgstreamer-plugins-bad1.0-dev:arm64 \ - gstreamer1.0-plugins-base:arm64 \ - gstreamer1.0-plugins-good:arm64 \ - gstreamer1.0-plugins-bad:arm64 \ - gstreamer1.0-plugins-ugly:arm64 \ - gstreamer1.0-libav:arm64 - - # Set cross-compilation environment - echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV - echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV - echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV - - # Set OpenSSL paths for ARM64 cross-compilation - echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV - echo "OPENSSL_LIB_DIR=/usr/lib/aarch64-linux-gnu" >> $GITHUB_ENV - echo "OPENSSL_INCLUDE_DIR=/usr/include" >> $GITHUB_ENV - - echo "GStreamer ARM64 setup complete" - - # ==================== Build Native Client ==================== - - - name: Build native client (Windows x64) - if: matrix.target == 'windows' - shell: bash - working-directory: opennow-streamer - run: | - echo "Building for Windows x64..." - cargo build --release --verbose - - - name: Build native client (Windows ARM64) - if: matrix.target == 'windows-arm64' - shell: bash - working-directory: opennow-streamer - run: | - echo "Building for Windows ARM64..." - cargo build --release --target aarch64-pc-windows-msvc --verbose - - - name: Build native client (macOS ARM64) - if: matrix.target == 'macos' - shell: bash - working-directory: opennow-streamer - run: | - echo "Building for macOS ARM64 (Apple Silicon)..." - cargo build --release --verbose - echo "macOS ARM64 build complete" - - # NOTE: Legacy macOS Intel build temporarily disabled - macOS-13 runners retired - # - name: Build native client (macOS Intel - Legacy) - # if: matrix.target == 'macos-intel' - # shell: bash - # working-directory: opennow-streamer - # run: | - # echo "Building for macOS Intel x64 (Legacy mode for 2015 and older Macs)..." - # cargo build --release --features legacy-macos --verbose - # echo "macOS Intel build complete" - - - name: Build native client (Linux x64) - if: matrix.target == 'linux' - shell: bash - working-directory: opennow-streamer - run: | - echo "Building for Linux x64..." - cargo build --release --verbose - - - name: Build native client (Linux ARM64) - if: matrix.target == 'linux-arm64' - shell: bash - working-directory: opennow-streamer - run: | - echo "Building for Linux ARM64..." - cargo build --release --target aarch64-unknown-linux-gnu --verbose - - # ==================== Bundle GStreamer Libraries ==================== - - - name: Bundle GStreamer DLLs (Windows x64) - if: matrix.target == 'windows' - shell: pwsh - run: | - $targetDir = "opennow-streamer/target/release" - $gstBinDir = $env:GSTREAMER_BIN_DIR - $gstPluginDir = $env:GSTREAMER_PLUGIN_DIR - - # GStreamer DLLs MUST be next to the exe because Rust links to them at load time - # (Windows loads DLLs before any Rust code runs, so we can't set PATH first) - # Core DLLs go next to exe, plugins go in lib/gstreamer-1.0/ - $gstBundlePlugins = "$targetDir\lib\gstreamer-1.0" - - New-Item -ItemType Directory -Force -Path $gstBundlePlugins | Out-Null - - Write-Host "Copying ALL GStreamer DLLs from $gstBinDir to $targetDir (next to exe for load-time linking)..." - - # Copy ALL DLLs from GStreamer bin directory - # This ensures we don't miss any dependencies (ffi, intl, glib, etc.) - # The GStreamer Rust bindings link at load time, so ALL dependencies must be present - $allDlls = Get-ChildItem -Path $gstBinDir -Filter "*.dll" -ErrorAction SilentlyContinue - $copiedCount = 0 - foreach ($dll in $allDlls) { - Copy-Item $dll.FullName -Destination $targetDir -Force - $copiedCount++ - } - Write-Host "Copied $copiedCount DLLs from GStreamer bin directory" - - # Copy ALL plugins from GStreamer plugin directory - # This ensures we don't miss any required plugins (opus, audio, video, etc.) - Write-Host "`nCopying ALL GStreamer plugins to $gstBundlePlugins..." - $allPlugins = Get-ChildItem -Path $gstPluginDir -Filter "*.dll" -ErrorAction SilentlyContinue - $pluginCount = 0 - foreach ($plugin in $allPlugins) { - Copy-Item $plugin.FullName -Destination $gstBundlePlugins -Force - $pluginCount++ - } - Write-Host "Copied $pluginCount plugins from GStreamer plugin directory" - - $totalDlls = (Get-ChildItem $targetDir -Filter "*.dll").Count - $totalPlugins = (Get-ChildItem $gstBundlePlugins -Filter "*.dll").Count - Write-Host "`n=== Bundle Summary ===" - Write-Host " Core DLLs (next to exe): $totalDlls" - Write-Host " Plugins (lib/gstreamer-1.0/): $totalPlugins" - - - name: Bundle GStreamer DLLs (Windows ARM64) - if: matrix.target == 'windows-arm64' - shell: pwsh - run: | - $targetDir = "opennow-streamer/target/aarch64-pc-windows-msvc/release" - $gstBinDir = $env:GSTREAMER_BIN_DIR - $gstPluginDir = $env:GSTREAMER_PLUGIN_DIR - - # GStreamer DLLs MUST be next to the exe because Rust links to them at load time - # (Windows loads DLLs before any Rust code runs, so we can't set PATH first) - # Core DLLs go next to exe, plugins go in lib/gstreamer-1.0/ - $gstBundlePlugins = "$targetDir\lib\gstreamer-1.0" - - New-Item -ItemType Directory -Force -Path $gstBundlePlugins | Out-Null - - Write-Host "Copying ALL GStreamer DLLs from $gstBinDir to $targetDir (next to exe for load-time linking)..." - - # Copy ALL DLLs from GStreamer bin directory - # This ensures we don't miss any dependencies (ffi, intl, glib, etc.) - # The GStreamer Rust bindings link at load time, so ALL dependencies must be present - $allDlls = Get-ChildItem -Path $gstBinDir -Filter "*.dll" -ErrorAction SilentlyContinue - $copiedCount = 0 - foreach ($dll in $allDlls) { - Copy-Item $dll.FullName -Destination $targetDir -Force - $copiedCount++ - } - Write-Host "Copied $copiedCount DLLs from GStreamer bin directory" - - # Copy ALL plugins from GStreamer plugin directory - # This ensures we don't miss any required plugins (opus, audio, video, etc.) - Write-Host "`nCopying ALL GStreamer plugins to $gstBundlePlugins..." - $allPlugins = Get-ChildItem -Path $gstPluginDir -Filter "*.dll" -ErrorAction SilentlyContinue - $pluginCount = 0 - foreach ($plugin in $allPlugins) { - Copy-Item $plugin.FullName -Destination $gstBundlePlugins -Force - $pluginCount++ - } - Write-Host "Copied $pluginCount plugins from GStreamer plugin directory" - - $totalDlls = (Get-ChildItem $targetDir -Filter "*.dll").Count - $totalPlugins = (Get-ChildItem $gstBundlePlugins -Filter "*.dll").Count - Write-Host "`n=== Bundle Summary ===" - Write-Host " Core DLLs (next to exe): $totalDlls" - Write-Host " Plugins (lib/gstreamer-1.0/): $totalPlugins" - - - name: Bundle macOS App - if: matrix.target == 'macos' || matrix.target == 'macos-intel' - shell: bash - env: - VERSION: ${{ needs.get-version.outputs.version_number }} - TARGET: ${{ matrix.target }} - ARCH: ${{ matrix.arch }} - run: | - set -e - cd opennow-streamer - - BINARY="target/release/opennow-streamer" - APP_NAME="OpenNOW" - APP_DIR="target/release/$APP_NAME.app" - CONTENTS_DIR="$APP_DIR/Contents" - MACOS_DIR="$CONTENTS_DIR/MacOS" - FRAMEWORKS_DIR="$CONTENTS_DIR/Frameworks" - RESOURCES_DIR="$CONTENTS_DIR/Resources" - - echo "Creating .app bundle structure for $APP_NAME v$VERSION..." - rm -rf "$APP_DIR" - mkdir -p "$MACOS_DIR" "$FRAMEWORKS_DIR" "$RESOURCES_DIR" - - # Check binary - if [ ! -f "$BINARY" ]; then - echo "ERROR: Binary not found at $BINARY" - exit 1 - fi - - # Copy and rename binary - echo "Copying binary..." - cp "$BINARY" "$MACOS_DIR/$APP_NAME" - chmod +x "$MACOS_DIR/$APP_NAME" - - # Create Info.plist - echo "Creating Info.plist..." - cat > "$CONTENTS_DIR/Info.plist" < - - - - CFBundleExecutable - $APP_NAME - CFBundleIdentifier - com.opennow.streamer - CFBundleName - $APP_NAME - CFBundleDisplayName - $APP_NAME - CFBundleVersion - ${VERSION} - CFBundleShortVersionString - ${VERSION} - CFBundlePackageType - APPL - LSMinimumSystemVersion - 12.0 - NSHighResolutionCapable - - - - EOF - - # Function to copy a library and fix its install name - copy_lib() { - local lib="$1" - local libname=$(basename "$lib") - - if [ ! -f "$FRAMEWORKS_DIR/$libname" ] && [ -f "$lib" ]; then - echo "Copying: $libname" - cp "$lib" "$FRAMEWORKS_DIR/" - chmod 755 "$FRAMEWORKS_DIR/$libname" - - # Fix the library's own install name to be relative to the framework folder - install_name_tool -id "@executable_path/../Frameworks/$libname" "$FRAMEWORKS_DIR/$libname" 2>/dev/null || true - return 0 - fi - return 0 - } - - # Function to fix references in a binary/library - fix_refs() { - local target="$1" - # Only fix references to Homebrew/system libs that we are bundling - for dep in $(otool -L "$target" 2>/dev/null | grep -E '/opt/homebrew|/usr/local' | awk '{print $1}'); do - local depname=$(basename "$dep") - install_name_tool -change "$dep" "@executable_path/../Frameworks/$depname" "$target" 2>/dev/null || true - done - } - - echo "" - echo "=== Phase 1a: Explicitly copy FFmpeg libraries ===" - # FFmpeg libraries might not show up in otool if dlopened or linked with @rpath - # So we explicitly find and copy them - - FFMPEG_PREFIX=$(brew --prefix ffmpeg) - echo "FFmpeg prefix: $FFMPEG_PREFIX" - - # List of core FFmpeg libraries to bundle - FFMPEG_LIBS=( - "libavcodec" - "libavdevice" - "libavfilter" - "libavformat" - "libavutil" - "libswresample" - "libswscale" - ) - - for lib_base in "${FFMPEG_LIBS[@]}"; do - echo "Looking for $lib_base in $FFMPEG_PREFIX/lib..." - FOUND_LIBS=$(find "$FFMPEG_PREFIX/lib" -name "${lib_base}.*.dylib" -type f ) - - for lib in $FOUND_LIBS; do - echo "Found FFmpeg lib: $lib" - copy_lib "$lib" - done - done - - # Note: GStreamer is NOT used on macOS - only FFmpeg is used for video/audio decoding - # GStreamer is only used on Linux and Windows x64 - - echo "" - echo "=== Phase 2: Copy direct dependencies detected by otool ===" - DIRECT_DEPS=$(otool -L "$BINARY" 2>/dev/null | grep -E '/opt/homebrew|/usr/local' | awk '{print $1}' || true) - if [ -z "$DIRECT_DEPS" ]; then - echo "No Homebrew dependencies found via otool (may be statically linked or using system libs)" - else - for lib in $DIRECT_DEPS; do - copy_lib "$lib" - done - fi - - echo "" - echo "=== Phase 3: Copy transitive dependencies (3 passes) ===" - BREW_PREFIX=$(brew --prefix) - echo "Resolving @rpath against: $BREW_PREFIX/lib" - - for pass in 1 2 3; do - echo "Pass $pass..." - if ls "$FRAMEWORKS_DIR/"*.dylib 1>/dev/null 2>&1; then - for bundled_lib in "$FRAMEWORKS_DIR/"*.dylib; do - [ -f "$bundled_lib" ] || continue - # Grep for /opt/homebrew, /usr/local, AND @rpath - for dep in $(otool -L "$bundled_lib" 2>/dev/null | grep -E '/opt/homebrew|/usr/local|@rpath' | awk '{print $1}'); do - - # Handle @rpath references - if [[ "$dep" == "@rpath/"* ]]; then - # Extract filename - filename="${dep#@rpath/}" - # Construct absolute path assuming it's in Homebrew lib - resolved_path="$BREW_PREFIX/lib/$filename" - - if [ -f "$resolved_path" ]; then - # echo "Resolved $dep to $resolved_path" - copy_lib "$resolved_path" - fi - else - # Absolute path - copy_lib "$dep" - fi - done - done - else - echo " No dylibs to process" - break - fi - done - - echo "" - echo "=== Phase 4: Fix all library references ===" - # Fix the main binary - fix_refs "$MACOS_DIR/$APP_NAME" - - # Fix all bundled libraries - if ls "$FRAMEWORKS_DIR/"*.dylib 1>/dev/null 2>&1; then - for bundled_lib in "$FRAMEWORKS_DIR/"*.dylib; do - [ -f "$bundled_lib" ] || continue - fix_refs "$bundled_lib" - done - fi - - # Special pass: Fix FFmpeg internal references - # e.g. libavformat linking to libavcodec - echo "Fixing internal references in bundled libs..." - if ls "$FRAMEWORKS_DIR/"*.dylib 1>/dev/null 2>&1; then - for bundled_lib in "$FRAMEWORKS_DIR/"*.dylib; do - # For each bundled lib, check if it references other bundled libs via absolute path - # and rewrite those to @loader_path - for dep in $(otool -L "$bundled_lib" 2>/dev/null | grep -E '/opt/homebrew|/usr/local' | awk '{print $1}'); do - local depname=$(basename "$dep") - if [ -f "$FRAMEWORKS_DIR/$depname" ]; then - echo " Fixing ref in $(basename "$bundled_lib") to $depname" - install_name_tool -change "$dep" "@loader_path/$depname" "$bundled_lib" 2>/dev/null || true - fi - done - done - fi - - # Special pass: Rewrite @rpath references (e.g. libwebp -> libsharpyuv) - echo "Rewriting @rpath references in bundled libs..." - if ls "$FRAMEWORKS_DIR/"*.dylib 1>/dev/null 2>&1; then - for bundled_lib in "$FRAMEWORKS_DIR/"*.dylib; do - # Grep for @rpath references - for dep in $(otool -L "$bundled_lib" 2>/dev/null | grep "@rpath/" | awk '{print $1}'); do - filename="${dep#@rpath/}" - if [ -f "$FRAMEWORKS_DIR/$filename" ]; then - echo " Rewriting @rpath ref in $(basename "$bundled_lib"): $dep -> @loader_path/$filename" - install_name_tool -change "$dep" "@loader_path/$filename" "$bundled_lib" 2>/dev/null || true - fi - done - done - fi - - echo "" - echo "=== Final verification ===" - echo "Binary dependencies:" - otool -L "$MACOS_DIR/$APP_NAME" - - echo "" - echo "Bundled Frameworks:" - ls -1 "$FRAMEWORKS_DIR" - - echo "" - echo "Verifying no remaining Homebrew paths..." - if otool -L "$MACOS_DIR/$APP_NAME" | grep -E '/opt/homebrew|/usr/local'; then - echo "WARNING: Some Homebrew paths remain (may be system libs)" - else - echo "All Homebrew dependencies bundled!" - fi - - echo "" - echo "=== Phase 5: Code Signing ===" - echo "Signing app bundle..." - - # Sign all dylibs in Frameworks - echo "Signing framework libraries..." - for lib in "$FRAMEWORKS_DIR/"*.dylib; do - if [ -f "$lib" ]; then - codesign --force --sign - "$lib" 2>/dev/null || true - fi - done - - # Sign the main binary - echo "Signing main binary..." - codesign --force --sign - "$MACOS_DIR/$APP_NAME" 2>/dev/null || true - - # Finally sign the whole app bundle - echo "Signing app bundle..." - codesign --force --sign - "$APP_DIR" - - # Zip the app bundle for upload/release - echo "Zipping .app bundle..." - cd target/release - # Use architecture from matrix (arm64 or x86_64) - ZIP_NAME="OpenNOW-macos-${ARCH}.zip" - zip -r "$ZIP_NAME" "$APP_NAME.app" - echo "Created: $ZIP_NAME" - - - name: Bundle Linux ARM64 binary - if: matrix.target == 'linux-arm64' - shell: bash - run: | - cd opennow-streamer - - BINARY="target/aarch64-unknown-linux-gnu/release/opennow-streamer" - BUNDLE_DIR="target/aarch64-unknown-linux-gnu/release/bundle" - LIB_DIR="$BUNDLE_DIR/lib" - GST_PLUGIN_DIR="$BUNDLE_DIR/lib/gstreamer-1.0" - - echo "Creating bundle directory structure..." - mkdir -p "$BUNDLE_DIR" "$LIB_DIR" "$GST_PLUGIN_DIR" - - # Copy the binary - cp "$BINARY" "$BUNDLE_DIR/" - chmod +x "$BUNDLE_DIR/opennow-streamer" - - # Copy GStreamer libraries for ARM64 - echo "Bundling GStreamer libraries..." - - # Core GStreamer libraries and their dependencies - for lib in /usr/lib/aarch64-linux-gnu/libgstreamer-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstbase-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstapp-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstaudio-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstvideo-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstpbutils-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstrtp-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstcodecs-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libgstgl-1.0.so* \ - /usr/lib/aarch64-linux-gnu/libglib-2.0.so* \ - /usr/lib/aarch64-linux-gnu/libgobject-2.0.so* \ - /usr/lib/aarch64-linux-gnu/libgmodule-2.0.so* \ - /usr/lib/aarch64-linux-gnu/libgio-2.0.so* \ - /usr/lib/aarch64-linux-gnu/liborc-0.4.so* \ - /usr/lib/aarch64-linux-gnu/libpcre.so* \ - /usr/lib/aarch64-linux-gnu/libpcre2-8.so* \ - /usr/lib/aarch64-linux-gnu/libffi.so*; do - if [ -f "$lib" ]; then - cp -L "$lib" "$LIB_DIR/" 2>/dev/null || true - fi - done - - # Copy GStreamer plugins - echo "Bundling GStreamer plugins..." - if [ -d "/usr/lib/aarch64-linux-gnu/gstreamer-1.0" ]; then - cp -L /usr/lib/aarch64-linux-gnu/gstreamer-1.0/*.so "$GST_PLUGIN_DIR/" 2>/dev/null || true - fi - - # Count bundled files - LIB_COUNT=$(ls -1 "$LIB_DIR"/*.so* 2>/dev/null | wc -l) - PLUGIN_COUNT=$(ls -1 "$GST_PLUGIN_DIR"/*.so 2>/dev/null | wc -l) - echo "Bundled $LIB_COUNT libraries and $PLUGIN_COUNT plugins" - - # Create wrapper script that sets library paths - echo '#!/bin/bash' > "$BUNDLE_DIR/run.sh" - echo 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' >> "$BUNDLE_DIR/run.sh" - echo 'export LD_LIBRARY_PATH="$SCRIPT_DIR/lib:$LD_LIBRARY_PATH"' >> "$BUNDLE_DIR/run.sh" - echo 'export GST_PLUGIN_PATH="$SCRIPT_DIR/lib/gstreamer-1.0"' >> "$BUNDLE_DIR/run.sh" - echo 'export GST_PLUGIN_SYSTEM_PATH=""' >> "$BUNDLE_DIR/run.sh" - echo 'exec "$SCRIPT_DIR/opennow-streamer" "$@"' >> "$BUNDLE_DIR/run.sh" - chmod +x "$BUNDLE_DIR/run.sh" - - echo "" - echo "Bundle created at: $BUNDLE_DIR" - echo "Run with: ./run.sh (uses bundled libraries)" - echo "Or install GStreamer system-wide and run ./opennow-streamer directly" - - # ==================== Upload Artifacts ==================== - - - name: Zip Windows x64 artifacts - if: matrix.target == 'windows' - shell: pwsh - run: | - $releaseDir = "opennow-streamer/target/release" - $zipName = "OpenNOW-windows-x64.zip" - # Include exe, all DLLs (GStreamer core), and lib folder (GStreamer plugins) - $items = @( - "$releaseDir\opennow-streamer.exe" - ) - # Add all DLLs (GStreamer core libraries next to exe) - $items += Get-ChildItem "$releaseDir\*.dll" | ForEach-Object { $_.FullName } - # Add lib folder if it exists (contains GStreamer plugins) - if (Test-Path "$releaseDir\lib") { - $items += "$releaseDir\lib" - } - Compress-Archive -Path $items -DestinationPath "$releaseDir\$zipName" - - - name: Upload Windows x64 artifacts - if: matrix.target == 'windows' - uses: actions/upload-artifact@v4 - with: - name: opennow-streamer-${{ needs.get-version.outputs.version }}-windows-x64 - path: opennow-streamer/target/release/OpenNOW-windows-x64.zip - retention-days: 30 - - - name: Zip Windows ARM64 artifacts - if: matrix.target == 'windows-arm64' - shell: pwsh - run: | - $releaseDir = "opennow-streamer/target/aarch64-pc-windows-msvc/release" - $zipName = "OpenNOW-windows-arm64.zip" - # Include exe, all DLLs (GStreamer core), and lib folder (GStreamer plugins) - $items = @( - "$releaseDir\opennow-streamer.exe" - ) - # Add all DLLs (GStreamer core libraries next to exe) - $items += Get-ChildItem "$releaseDir\*.dll" | ForEach-Object { $_.FullName } - # Add lib folder if it exists (contains GStreamer plugins) - if (Test-Path "$releaseDir\lib") { - $items += "$releaseDir\lib" - } - Compress-Archive -Path $items -DestinationPath "$releaseDir\$zipName" - - - name: Upload Windows ARM64 artifacts - if: matrix.target == 'windows-arm64' - uses: actions/upload-artifact@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - name: opennow-streamer-${{ needs.get-version.outputs.version }}-windows-arm64 - path: opennow-streamer/target/aarch64-pc-windows-msvc/release/OpenNOW-windows-arm64.zip - retention-days: 30 + node-version: 22 + cache: npm + cache-dependency-path: "**/package-lock.json" - - name: Upload macOS ARM64 artifacts - if: matrix.target == 'macos' - uses: actions/upload-artifact@v4 + - name: Cache Electron binaries + uses: actions/cache@v4 with: - name: opennow-streamer-${{ needs.get-version.outputs.version }}-macos-arm64 - path: opennow-streamer/target/release/OpenNOW-macos-arm64.zip - retention-days: 30 - - # NOTE: Legacy macOS Intel build temporarily disabled - macOS-13 runners retired - # - name: Upload macOS Intel (Legacy) artifacts - # if: matrix.target == 'macos-intel' - # uses: actions/upload-artifact@v4 - # with: - # name: opennow-streamer-${{ needs.get-version.outputs.version }}-macos-intel - # path: opennow-streamer/target/release/OpenNOW-macos-x86_64.zip - # retention-days: 30 + path: | + ${{ github.workspace }}/.cache/electron + ${{ github.workspace }}/.cache/electron-builder + key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-electron- - - name: Bundle Linux x64 binary - if: matrix.target == 'linux' - shell: bash + - name: Install Linux packaging tools + if: runner.os == 'Linux' run: | - cd opennow-streamer - - BINARY="target/release/opennow-streamer" - BUNDLE_DIR="target/release/bundle" - LIB_DIR="$BUNDLE_DIR/lib" - GST_PLUGIN_DIR="$BUNDLE_DIR/lib/gstreamer-1.0" - - echo "Creating bundle directory structure..." - mkdir -p "$BUNDLE_DIR" "$LIB_DIR" "$GST_PLUGIN_DIR" - - # Copy the binary - cp "$BINARY" "$BUNDLE_DIR/" - chmod +x "$BUNDLE_DIR/opennow-streamer" - - # Copy GStreamer libraries for x64 - echo "Bundling GStreamer libraries..." - - # Core GStreamer libraries and their dependencies - for lib in /usr/lib/x86_64-linux-gnu/libgstreamer-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstbase-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstapp-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstaudio-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstvideo-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstpbutils-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstrtp-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstcodecs-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libgstgl-1.0.so* \ - /usr/lib/x86_64-linux-gnu/libglib-2.0.so* \ - /usr/lib/x86_64-linux-gnu/libgobject-2.0.so* \ - /usr/lib/x86_64-linux-gnu/libgmodule-2.0.so* \ - /usr/lib/x86_64-linux-gnu/libgio-2.0.so* \ - /usr/lib/x86_64-linux-gnu/liborc-0.4.so* \ - /usr/lib/x86_64-linux-gnu/libpcre.so* \ - /usr/lib/x86_64-linux-gnu/libpcre2-8.so* \ - /usr/lib/x86_64-linux-gnu/libffi.so*; do - if [ -f "$lib" ]; then - cp -L "$lib" "$LIB_DIR/" 2>/dev/null || true - fi - done - - # Copy GStreamer plugins - echo "Bundling GStreamer plugins..." - if [ -d "/usr/lib/x86_64-linux-gnu/gstreamer-1.0" ]; then - cp -L /usr/lib/x86_64-linux-gnu/gstreamer-1.0/*.so "$GST_PLUGIN_DIR/" 2>/dev/null || true - fi - - # Count bundled files - LIB_COUNT=$(ls -1 "$LIB_DIR"/*.so* 2>/dev/null | wc -l) - PLUGIN_COUNT=$(ls -1 "$GST_PLUGIN_DIR"/*.so 2>/dev/null | wc -l) - echo "Bundled $LIB_COUNT libraries and $PLUGIN_COUNT plugins" - - # Create wrapper script that sets library paths - echo '#!/bin/bash' > "$BUNDLE_DIR/run.sh" - echo 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' >> "$BUNDLE_DIR/run.sh" - echo 'export LD_LIBRARY_PATH="$SCRIPT_DIR/lib:$LD_LIBRARY_PATH"' >> "$BUNDLE_DIR/run.sh" - echo 'export GST_PLUGIN_PATH="$SCRIPT_DIR/lib/gstreamer-1.0"' >> "$BUNDLE_DIR/run.sh" - echo 'export GST_PLUGIN_SYSTEM_PATH=""' >> "$BUNDLE_DIR/run.sh" - echo 'exec "$SCRIPT_DIR/opennow-streamer" "$@"' >> "$BUNDLE_DIR/run.sh" - chmod +x "$BUNDLE_DIR/run.sh" + sudo apt-get update + sudo apt-get install -y fakeroot rpm + - name: Install FPM for arm64 deb builds + if: matrix.label == 'linux-arm64' + run: sudo apt-get install -y ruby ruby-dev build-essential && sudo gem install fpm - echo "" - echo "Bundle created at: $BUNDLE_DIR" + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --progress=false - - name: Zip Linux x64 artifacts - if: matrix.target == 'linux' - shell: bash - run: | - cd opennow-streamer/target/release - zip -r "OpenNOW-linux-x64.zip" bundle/ + - name: Build app bundles + run: npm run build - - name: Upload Linux x64 artifacts - if: matrix.target == 'linux' - uses: actions/upload-artifact@v4 - with: - name: opennow-streamer-${{ needs.get-version.outputs.version }}-linux-x64 - path: opennow-streamer/target/release/OpenNOW-linux-x64.zip - retention-days: 30 - - - name: Zip Linux ARM64 artifacts - if: matrix.target == 'linux-arm64' - shell: bash - run: | - cd opennow-streamer/target/aarch64-unknown-linux-gnu/release - zip -r "OpenNOW-linux-arm64.zip" bundle/ + - name: Package installers + run: npx electron-builder --publish never ${{ matrix.builder_args }} - - name: Upload Linux ARM64 artifacts - if: matrix.target == 'linux-arm64' + - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: opennow-streamer-${{ needs.get-version.outputs.version }}-linux-arm64 - path: opennow-streamer/target/aarch64-unknown-linux-gnu/release/OpenNOW-linux-arm64.zip - retention-days: 30 - - # ==================== Upload to GitHub Release (main branch only) ==================== - - - name: Create GitHub Release - if: github.ref == 'refs/heads/main' && matrix.target == 'linux' - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.get-version.outputs.version }} - name: "OpenNOW ${{ needs.get-version.outputs.version }}" - body: | - ## OpenNOW ${{ needs.get-version.outputs.version }} - - Open source native GeForce NOW client built with Rust. - - ${{ needs.get-version.outputs.release_notes }} - - --- - - ### Downloads - - **Windows x64**: Portable executable with bundled GStreamer - - **Windows ARM64**: Portable executable with bundled GStreamer (Surface Pro X, etc.) - - **macOS ARM64**: Apple Silicon native binary with bundled FFmpeg + GStreamer (Intel Macs can use Rosetta 2) - - **Linux x64**: Portable binary (requires system GStreamer - see below) - - **Linux ARM64**: Portable binary (requires system GStreamer - see below) - - **Linux users**: Install GStreamer via your package manager: - ```bash - # Ubuntu/Debian - sudo apt install gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav - - # Fedora/RHEL - sudo dnf install gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-plugins-ugly-free gstreamer1-libav - - # Arch - sudo pacman -S gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav - ``` - - [Sponsor this project](https://github.com/sponsors/zortos293) - draft: false - prerelease: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Windows x64 to release - if: matrix.target == 'windows' && github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.get-version.outputs.version }} - files: opennow-streamer/target/release/OpenNOW-windows-x64.zip - fail_on_unmatched_files: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + name: opennow-stable-${{ matrix.label }} + path: | + opennow-stable/dist-release/*.exe + opennow-stable/dist-release/*.dmg + opennow-stable/dist-release/*.zip + opennow-stable/dist-release/*.AppImage + opennow-stable/dist-release/*.deb + if-no-files-found: error + + release: + name: publish-release + runs-on: blacksmith-2vcpu-ubuntu-2404 + needs: build + if: startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/opennow-stable-v') + permissions: + contents: write - - name: Upload Windows ARM64 to release - if: matrix.target == 'windows-arm64' && github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.get-version.outputs.version }} - files: opennow-streamer/target/aarch64-pc-windows-msvc/release/OpenNOW-windows-arm64.zip - fail_on_unmatched_files: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload macOS ARM64 to release - if: matrix.target == 'macos' && github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.get-version.outputs.version }} - files: opennow-streamer/target/release/OpenNOW-macos-arm64.zip - fail_on_unmatched_files: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # NOTE: Legacy macOS Intel build temporarily disabled - macOS-13 runners retired - # - name: Upload macOS Intel (Legacy) to release - # if: matrix.target == 'macos-intel' && github.ref == 'refs/heads/main' - # uses: softprops/action-gh-release@v1 - # with: - # tag_name: ${{ needs.get-version.outputs.version }} - # files: opennow-streamer/target/release/OpenNOW-macos-x86_64.zip - # fail_on_unmatched_files: false - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Linux x64 to release - if: matrix.target == 'linux' && github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 with: - tag_name: ${{ needs.get-version.outputs.version }} - files: opennow-streamer/target/release/OpenNOW-linux-x64.zip - fail_on_unmatched_files: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + path: release-artifacts + merge-multiple: true - - name: Upload Linux ARM64 to release - if: matrix.target == 'linux-arm64' && github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 with: - tag_name: ${{ needs.get-version.outputs.version }} - files: opennow-streamer/target/aarch64-unknown-linux-gnu/release/OpenNOW-linux-arm64.zip - fail_on_unmatched_files: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref_name }} + name: OpenNOW ${{ github.ref_name }} + generate_release_notes: true + files: release-artifacts/**/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 89d8764..1f2ed0b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,11 +22,6 @@ target3/ Thumbs.db nul -# Tauri -src-tauri/target/ -src-tauri/target2/ -src-tauri/target3/ - # Logs *.log @@ -37,18 +32,13 @@ src-tauri/target3/ # Sensitive data *.tokens.json gfn_tokens.json -src-tauri/gfn_tokens.json # Test files test/ -# Generated schemas -src-tauri/gen/ - package-lock.json +!opennow-stable/package-lock.json # AI Agent guidelines (local only) AGENTS.md release-notes.md -opennow-streamer/.claude/settings.local.json -CLAUDE.md diff --git a/README.md b/README.md index db9f7bc..2b97154 100644 --- a/README.md +++ b/README.md @@ -1,196 +1,226 @@

OpenNOW

- Open source GeForce NOW client built from the ground up in Native Rust + OpenNOW logo

- - Download + An open-source GeForce NOW client — play your games, your way. +

+ +

+ OpenNOW +

+ +

+ + Download Documentation + + Auto Build + Discord

- - Stars + + Stars - - Downloads + + Downloads - - License + + License

--- > **Warning** -> OpenNOW is under **active development**. Expect bugs and performance issues. -> Check the [Known Issues](#known-issues) section and [full documentation](https://opennow.zortos.me) for details. +> OpenNOW is under active development. Bugs and performance issues are expected while features are finalized. --- -## About - -OpenNOW is a custom GeForce NOW client rewritten entirely in **Native Rust** for maximum performance and lower resource usage. Built with `wgpu` and `egui` for a seamless cloud gaming experience. - - - - - - -
- -**Why OpenNOW?** -- Native Performance (Rust + wgpu) -- No artificial FPS/resolution/bitrate limits -- No telemetry by default -- Cross-platform (Windows, macOS, Linux) - - - -**Key Features** -- Zero-copy hardware decoding -- Raw input capture (low latency) -- Gamepad & racing wheel support -- Alliance Partner support +## What is OpenNOW? + +OpenNOW is a community-built desktop client for [NVIDIA GeForce NOW](https://www.nvidia.com/en-us/geforce-now/), built with Electron and TypeScript. It gives you a fully open-source, cross-platform alternative to the official app — with zero telemetry, full transparency, and features the official client doesn't have. + +- 🔓 **Fully open source** — audit every line, fork it, improve it +- 🚫 **No telemetry** — OpenNOW collects nothing +- 🖥️ **Cross-platform** — Windows, macOS, Linux, and ARM64 +- ⚡ **Community-driven** — faster fixes, transparent development +- 🎮 **Anti-AFK, Stats Overlay, Adjustable Shortcuts** — power-user features built in + +## OpenNOW vs Official GeForce NOW + +| Feature | OpenNOW | Official GFN | Notes | +|---------|:-------:|:------------:|-------| +| **Streaming** | | | | +| WebRTC Streaming | ✅ | ✅ | Chromium-based in OpenNOW | +| H.264 Codec | ✅ | ✅ | | +| H.265 / HEVC Codec | ✅ | ✅ | Full support | +| AV1 Codec | ✅ | ✅ | | +| Up to 1080p | ✅ | ✅ | | +| Up to 4K | ✅ | ✅ | Configurable in settings | +| 5K Resolution | ✅ | ✅ | Up to 5K@120fps | +| 120+ FPS | ✅ | ✅ | Configurable: 30/60/120/144/240 | +| HDR Streaming | 📋 | ✅ | 10-bit color supported, full HDR pipeline planned | +| AI-Enhanced Stream Mode | ❌ | ✅ | NVIDIA Cinematic Quality — not available | +| Adjustable Bitrate | ✅ | ✅ | Up to 200 Mbps in OpenNOW | +| Color Quality (8/10-bit, 4:2:0/4:4:4) | ✅ | ✅ | Full chroma/bit-depth control | +| **Input** | | | | +| Keyboard + Mouse | ✅ | ✅ | Full input over GFN data channels | +| Gamepad Support | ✅ | ✅ | Up to 4 controllers simultaneously | +| Flight Controls | ❌ | ✅ | Added in official client v2.0.81 | +| Mouse Sensitivity | ✅ | ❌ | OpenNOW-exclusive setting | +| Clipboard Paste | ✅ | ❌ | Paste text into cloud session | +| **Features** | | | | +| Authentication + Session Restore | ✅ | ✅ | OAuth PKCE, auto-restore on startup | +| Game Library + Catalog | ✅ | ✅ | Main catalog, library, and public games | +| Alliance Partners | ✅ | ✅ | NVIDIA + partner providers | +| Audio Playback | ✅ | ✅ | | +| Microphone Support | 📋 | ✅ | Planned for future release | +| Instant Replay | 📋 | ✅ | Planned for future release | +| Screenshots | 📋 | ✅ | Planned for future release | +| Stats Overlay | ✅ | ✅ | Detailed: RTT, decode, render, jitter, loss, input queue | +| Anti-AFK | ✅ | ❌ | OpenNOW-exclusive — prevents idle disconnects | +| Adjustable Shortcuts | ✅ | 🚧 | Fully customizable in OpenNOW | +| Session Conflict Resolution | ✅ | ✅ | Resume / New / Cancel existing sessions | +| Subscription Info | ✅ | ✅ | Hours, tier, entitled resolutions | +| Region Selection | ✅ | ✅ | Dynamic region discovery | +| Install-to-Play | ✅ | ✅ | For games not in standard catalog | +| Discord Integration | ❌ | ✅ | | +| **Platform Support** | | | | +| Windows | ✅ | ✅ | NSIS installer + portable | +| macOS (x64 + ARM) | ✅ | ✅ | Universal builds | +| Linux | ✅ | 🚧 | Official client has beta native app | +| ARM64 / Raspberry Pi | ✅ | ❌ | OpenNOW builds for ARM64 Linux | +| Steam Deck | 📋 | ✅ | | +| Android / iOS / TV | ❌ | ✅ | Desktop-only for now | +| **Privacy & Openness** | | | | +| Open Source | ✅ | ❌ | MIT licensed | +| No Telemetry | ✅ | ❌ | Zero data collection | +| Auditable Code | ✅ | ❌ | | + +> 💡 **Legend:** ✅ Working · 🚧 In Progress · 📋 Planned · ❌ Not Available + +## Roadmap + +| Priority | Feature | Status | Description | +|:--------:|---------|:------:|-------------| +| 🔴 | ~~H.265 codec tuning~~ | ✅ Completed | Full HEVC support implemented | +| 🔴 | Microphone support | 📋 Planned | Voice chat in cloud sessions | +| 🟡 | Instant replay | 📋 Planned | Clip and save gameplay moments | +| 🟡 | Screenshots | 📋 Planned | Capture in-stream screenshots | +| 🟡 | HDR streaming pipeline | 📋 Planned | Full HDR end-to-end support | +| 🟢 | Latency optimizations | 🚧 Ongoing | Input and render path improvements | +| 🟢 | Platform stability | 🚧 Ongoing | Cross-platform bug fixes | + +> 🔴 High priority · 🟡 Medium priority · 🟢 Ongoing effort -
- -📖 **Full Documentation:** [opennow.zortos.me](https://opennow.zortos.me) - ---- - -## Quick Start - -### Download - -| Platform | Download | Notes | -|----------|----------|-------| -| **Windows x64** | [OpenNOW-windows-x64.zip](https://github.com/zortos293/OpenNOW/releases/latest) | Portable, GStreamer bundled | -| **Windows ARM64** | [OpenNOW-windows-arm64.zip](https://github.com/zortos293/OpenNOW/releases/latest) | Surface Pro X, etc. | -| **macOS (Apple Silicon)** | [OpenNOW-macos-arm64.zip](https://github.com/zortos293/OpenNOW/releases/latest) | M1/M2/M3 native | -| **Linux x64** | [OpenNOW-linux-x64.AppImage](https://github.com/zortos293/OpenNOW/releases/latest) | AppImage, GStreamer bundled | -| **Linux ARM64** | [OpenNOW-linux-arm64.zip](https://github.com/zortos293/OpenNOW/releases/latest) | Requires system GStreamer | +## Features -### Run +**Streaming** +`H.264` `AV1` `H.265 (WIP)` · Up to 4K@240fps · Adjustable bitrate · 8/10-bit color · 4:2:0/4:4:4 chroma -1. **Download** the release for your platform -2. **Extract** and run the executable -3. **Login** with your NVIDIA GeForce NOW account -4. **Play!** +**Input** +`Keyboard` `Mouse` `Gamepad ×4` · Mouse sensitivity · Clipboard paste -> **macOS:** If blocked, run: `xattr -d com.apple.quarantine OpenNOW.app` +**Client** +`Stats Overlay` `Anti-AFK` `Adjustable Shortcuts` · OAuth + session restore · Region selection · Alliance partners ---- +**Platforms** +`Windows` `macOS` `Linux` `ARM64` · Installer, portable, AppImage, deb, dmg ## Platform Support -| Platform | Status | Hardware Decoding | -|----------|:------:|-------------------| -| Windows x64 | ✅ Working | D3D11VA (NVIDIA, AMD, Intel) | -| Windows ARM64 | ❓ Untested | Should work | -| macOS ARM64 | ✅ Working | VideoToolbox | -| macOS Intel | ✅ Working | VideoToolbox (Rosetta 2) | -| Linux x64 | ⚠️ Buggy | Vulkan Video | -| Linux ARM64 | ⚠️ Buggy | GStreamer | -| Raspberry Pi | ❌ Broken | Under investigation | - ---- - -## Features +| Platform | Status | Builds | +|----------|:------:|--------| +| Windows | ✅ Working | NSIS installer + portable | +| macOS | ✅ Working | dmg + zip (x64 and arm64) | +| Linux x64 | ✅ Working | AppImage + deb | +| Linux ARM64 | 🚧 Experimental | AppImage + deb (Raspberry Pi 4/5) | -| Feature | Status | Feature | Status | -|---------|:------:|---------|:------:| -| Authentication | ✅ | Gamepad Support | ✅ | -| Game Library | ✅ | Audio Playback | ✅ | -| WebRTC Streaming | ✅ | Stats Overlay | ✅ | -| Hardware Decoding | ✅ | Anti-AFK | ✅ | -| Zero-Copy Rendering | ✅ | Alliance Partners | ✅ | -| Mouse/Keyboard | ✅ | Clipboard Paste | ✅ | -| AV1 Codec | ✅ | H.264/H.265 | ✅ | +## Quick Start -**Coming Soon:** Microphone, Instant Replay, Screenshots, Plugin System, Theming +```bash +git clone https://github.com/OpenCloudGaming/OpenNOW.git +cd OpenNOW/opennow-stable +npm install +npm run dev +``` ---- +See [opennow-stable/README.md](./opennow-stable/README.md) for build and packaging details. -## Keyboard Shortcuts +## Download -| Key | Action | -|-----|--------| -| `F3` | Toggle stats overlay | -| `F8` | Toggle mouse capture | -| `F11` | Toggle fullscreen | -| `Ctrl+Shift+Q` | Quit session | -| `Ctrl+Shift+F10` | Toggle anti-AFK | +Grab the latest release for your platform: ---- +👉 **[Download from GitHub Releases](https://github.com/OpenCloudGaming/OpenNOW/releases)** -## Known Issues +| Platform | File | +|----------|------| +| Windows (installer) | `OpenNOW-v0.2.4-setup-x64.exe` | +| Windows (portable) | `OpenNOW-v0.2.4-portable-x64.exe` | +| macOS (x64) | `OpenNOW-v0.2.4-mac-x64.dmg` | +| macOS (ARM) | `OpenNOW-v0.2.4-mac-arm64.dmg` | +| Linux (x64) | `OpenNOW-v0.2.4-linux-x86_64.AppImage` | +| Linux (ARM64) | `OpenNOW-v0.2.4-linux-arm64.AppImage` | -| Issue | Workaround | -|-------|------------| -| High CPU usage | Lower FPS/resolution in settings | -| Green screen flashes | Switch to H.264 codec | -| Audio stuttering | Restart stream | -| Laggy input | Enable `low_latency_mode` | -| Linux instability | Use Windows/macOS for now | +## Architecture ---- +OpenNOW is an Electron app with three processes: -## Building from Source +| Layer | Technology | Role | +|-------|-----------|------| +| **Main** | Node.js + Electron | OAuth, CloudMatch API, WebSocket signaling, settings | +| **Renderer** | React 19 + TypeScript | UI, WebRTC streaming, input encoding, stats | +| **Preload** | Electron contextBridge | Secure IPC between main and renderer | -```bash -git clone https://github.com/zortos293/OpenNOW.git -cd OpenNOW/opennow-streamer -cargo build --release +``` +opennow-stable/src/ +├── main/ # Electron main process +│ ├── gfn/ # Auth, CloudMatch, signaling, games, subscription +│ ├── index.ts # Entry point, IPC handlers, window management +│ └── settings.ts # Persistent user settings +├── renderer/src/ # React UI +│ ├── components/ # Login, Home, Library, Settings, StreamView +│ ├── gfn/ # WebRTC client, SDP, input protocol +│ └── App.tsx # Root component with routing and state +├── shared/ # Shared types and IPC channel definitions +│ ├── gfn.ts # All TypeScript interfaces +│ └── ipc.ts # IPC channel constants +└── preload/ # Context bridge (safe API exposure) ``` -See the [full build guide](https://opennow.zortos.me/guides/getting-started/) for platform-specific requirements. +## FAQ ---- +**Is this the official GeForce NOW client?** +No. OpenNOW is a community-built alternative. It uses the same NVIDIA streaming infrastructure but is not affiliated with or endorsed by NVIDIA. -## Documentation +**Was this project built in Rust before?** +Yes. OpenNOW originally used Rust/Tauri but switched to Electron for better cross-platform compatibility and faster development. -Full documentation available at **[opennow.zortos.me](https://opennow.zortos.me)** +**Does OpenNOW collect any data?** +No. OpenNOW has zero telemetry. Your credentials are stored locally and only sent to NVIDIA's authentication servers. -- [Getting Started](https://opennow.zortos.me/guides/getting-started/) -- [Architecture Overview](https://opennow.zortos.me/architecture/overview/) -- [Configuration Reference](https://opennow.zortos.me/reference/configuration/) -- [WebRTC Protocol](https://opennow.zortos.me/reference/webrtc/) +## Contributing ---- +Contributions are welcome! Open an issue or PR on [GitHub](https://github.com/OpenCloudGaming/OpenNOW). -## Support the Project - -OpenNOW is a passion project developed in my free time. If you enjoy using it, please consider sponsoring! +## Support Me

- Sponsor + Support Me

---- - -## Disclaimer - -This is an **independent project** not affiliated with NVIDIA Corporation. Created for educational purposes. GeForce NOW is a trademark of NVIDIA. Use at your own risk. - ---- +## License -

- Documentation · - Discord · - Sponsor -

- -

- Made with ❤️ by zortos293 -

+[MIT](./LICENSE) © Zortos diff --git a/flatpak/com.zortos.opennow.binary.yml b/flatpak/com.zortos.opennow.binary.yml deleted file mode 100644 index 4d53a9d..0000000 --- a/flatpak/com.zortos.opennow.binary.yml +++ /dev/null @@ -1,34 +0,0 @@ -# Simpler manifest that uses a pre-built binary -# First build the binary natively, then package it -app-id: com.zortos.opennow -runtime: org.gnome.Platform -runtime-version: '46' -sdk: org.gnome.Sdk -command: opennow -finish-args: - - --socket=fallback-x11 - - --socket=wayland - - --device=dri - - --socket=pulseaudio - - --share=network - - --share=ipc - - --filesystem=xdg-run/discord-ipc-0 - - --talk-name=org.freedesktop.Notifications - - --talk-name=org.freedesktop.secrets - - --filesystem=xdg-config/opennow:create - - --filesystem=xdg-data/opennow:create - -modules: - - name: opennow - buildsystem: simple - build-commands: - - install -Dm755 opennow /app/bin/opennow - - install -Dm644 com.zortos.opennow.desktop /app/share/applications/com.zortos.opennow.desktop - - install -Dm644 icon.png /app/share/icons/hicolor/128x128/apps/com.zortos.opennow.png - sources: - - type: file - path: opennow - - type: file - path: com.zortos.opennow.desktop - - type: file - path: icon.png diff --git a/flatpak/com.zortos.opennow.desktop b/flatpak/com.zortos.opennow.desktop deleted file mode 100644 index a91e36b..0000000 --- a/flatpak/com.zortos.opennow.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=OpenNOW -Comment=Open source GeForce NOW client -Exec=opennow -Icon=com.zortos.opennow -Terminal=false -Type=Application -Categories=Game; -Keywords=geforce;now;cloud;gaming;streaming;nvidia; diff --git a/flatpak/com.zortos.opennow.yml b/flatpak/com.zortos.opennow.yml deleted file mode 100644 index d75b242..0000000 --- a/flatpak/com.zortos.opennow.yml +++ /dev/null @@ -1,50 +0,0 @@ -app-id: com.zortos.opennow -runtime: org.gnome.Platform -runtime-version: '46' -sdk: org.gnome.Sdk -sdk-extensions: - - org.freedesktop.Sdk.Extension.rust-stable - - org.freedesktop.Sdk.Extension.node20 -command: opennow -finish-args: - # X11 + Wayland - - --socket=fallback-x11 - - --socket=wayland - # GPU acceleration - - --device=dri - # Audio - - --socket=pulseaudio - # Network access for streaming - - --share=network - # IPC for WebKit - - --share=ipc - # For Discord Rich Presence - - --filesystem=xdg-run/discord-ipc-0 - # Notifications - - --talk-name=org.freedesktop.Notifications - # Secrets for storing auth tokens - - --talk-name=org.freedesktop.secrets - # File access for settings - - --filesystem=xdg-config/opennow:create - - --filesystem=xdg-data/opennow:create - -build-options: - append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/node20/bin - env: - CARGO_HOME: /run/build/opennow/cargo - npm_config_nodedir: /usr/lib/sdk/node20 - -modules: - - name: opennow - buildsystem: simple - build-commands: - # Install bun and build - - curl -fsSL https://bun.sh/install | bash - - export PATH="$HOME/.bun/bin:$PATH" && bun install && bun run build - - cd src-tauri && cargo build --release - - install -Dm755 src-tauri/target/release/opennow /app/bin/opennow - - install -Dm644 flatpak/com.zortos.opennow.desktop /app/share/applications/com.zortos.opennow.desktop - - install -Dm644 src-tauri/icons/128x128.png /app/share/icons/hicolor/128x128/apps/com.zortos.opennow.png - sources: - - type: dir - path: .. diff --git a/img.png b/img.png index 9b65cd7..bfb639f 100644 Binary files a/img.png and b/img.png differ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..7e946b4 Binary files /dev/null and b/logo.png differ diff --git a/opennow-stable/.gitignore b/opennow-stable/.gitignore new file mode 100644 index 0000000..5152f10 --- /dev/null +++ b/opennow-stable/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +dist-electron +dist-release +*.log diff --git a/opennow-stable/README.md b/opennow-stable/README.md new file mode 100644 index 0000000..a4f0c24 --- /dev/null +++ b/opennow-stable/README.md @@ -0,0 +1,54 @@ +# OpenNOW Stable (Electron) + +> For features, comparison, and downloads, see the [main README](../README.md). + +Developer reference for the Electron-based OpenNOW client. + +## Development + +```bash +npm install +npm run dev +``` + +## Build + +```bash +npm run build +npm run dist +``` + +## Packaging Targets + +| Platform | Formats | +|----------|---------| +| Windows | NSIS installer + portable | +| macOS | dmg + zip (x64 and arm64 universal) | +| Linux x64 | AppImage + deb | +| Linux ARM64 | AppImage + deb (Raspberry Pi 4/5) | + +## CI/CD + +Workflow: `.github/workflows/auto-build.yml` + +- Triggers on pushes to `dev`/`main` and PRs +- Builds: Windows, macOS (x64/arm64), Linux x64, Linux arm64 +- Artifacts uploaded to GitHub Releases + +## Tagged Releases + +Format: `opennow-stable-vX.Y.Z` (e.g., `opennow-stable-v0.2.4`) + +```bash +git tag opennow-stable-v0.2.4 +git push origin opennow-stable-v0.2.4 +``` + +The workflow automatically builds all platforms and creates/updates the GitHub Release. + +## Technical Notes + +- `ws` runs in the Electron main process for custom signaling behavior and relaxed TLS handling +- WebRTC uses Chromium's built-in stack (no external dependencies) +- OAuth PKCE flow with localhost callback +- Persistent settings stored via `electron-store` diff --git a/opennow-stable/electron.vite.config.ts b/opennow-stable/electron.vite.config.ts new file mode 100644 index 0000000..85b33b9 --- /dev/null +++ b/opennow-stable/electron.vite.config.ts @@ -0,0 +1,40 @@ +import { resolve } from "node:path"; + +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: "dist-electron/main", + }, + resolve: { + alias: { + "@shared": resolve("src/shared"), + }, + }, + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: "dist-electron/preload", + }, + resolve: { + alias: { + "@shared": resolve("src/shared"), + }, + }, + }, + renderer: { + build: { + outDir: "dist", + }, + plugins: [react()], + resolve: { + alias: { + "@shared": resolve("src/shared"), + }, + }, + }, +}); diff --git a/opennow-stable/package-lock.json b/opennow-stable/package-lock.json new file mode 100644 index 0000000..1afd925 --- /dev/null +++ b/opennow-stable/package-lock.json @@ -0,0 +1,7057 @@ +{ + "name": "opennow-stable", + "version": "0.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opennow-stable", + "version": "0.2.4", + "dependencies": { + "lucide-react": "^0.563.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/ws": "^8.5.13", + "@vitejs/plugin-react": "^4.3.4", + "cross-env": "^7.0.3", + "electron": "^40.4.1", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "typescript": "^5.7.2", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", + "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", + "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.6.1", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "25.1.7", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "resedit": "^1.7.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "25.1.8", + "electron-builder-squirrel-windows": "25.1.8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", + "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.10", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/config-file-ts/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/config-file-ts/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/config-file-ts/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/config-file-ts/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/config-file-ts/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/config-file-ts/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "40.4.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.4.1.tgz", + "integrity": "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "dmg-builder": "25.1.8", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.8", + "archiver": "^5.3.1", + "builder-util": "25.1.7", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", + "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz", + "integrity": "sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "cac": "^6.7.14", + "esbuild": "^0.21.5", + "magic-string": "^0.30.10", + "picocolors": "^1.0.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/opennow-stable/package.json b/opennow-stable/package.json new file mode 100644 index 0000000..65fcb59 --- /dev/null +++ b/opennow-stable/package.json @@ -0,0 +1,97 @@ +{ + "name": "opennow-stable", + "version": "0.2.4", + "description": "Electron-based OpenNOW stable client", + "author": { + "name": "zortos293", + "email": "zortos293@users.noreply.github.com" + }, + "homepage": "https://github.com/OpenCloudGaming/OpenNOW", + "repository": { + "type": "git", + "url": "https://github.com/OpenCloudGaming/OpenNOW.git" + }, + "private": true, + "type": "module", + "main": "dist-electron/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "build": "electron-vite build", + "preview": "electron-vite preview", + "dist": "npm run build && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder", + "dist:signed": "npm run build && electron-builder", + "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "lucide-react": "^0.563.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/ws": "^8.5.13", + "@vitejs/plugin-react": "^4.3.4", + "cross-env": "^7.0.3", + "electron": "^40.4.1", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "typescript": "^5.7.2", + "vite": "^5.4.11" + }, + "build": { + "appId": "com.zortos.opennow.stable", + "productName": "OpenNOW", + "publish": [ + { + "provider": "github", + "owner": "OpenCloudGaming", + "repo": "OpenNOW" + } + ], + "icon": "../logo.png", + "npmRebuild": false, + "nodeGypRebuild": false, + "buildDependenciesFromSource": false, + "directories": { + "output": "dist-release" + }, + "files": [ + "dist/**", + "dist-electron/**", + "package.json" + ], + "asar": true, + "win": { + "target": [ + "nsis", + "portable" + ] + }, + "nsis": { + "artifactName": "OpenNOW-v${version}-setup-${arch}.${ext}" + }, + "portable": { + "artifactName": "OpenNOW-v${version}-portable-${arch}.${ext}" + }, + "mac": { + "target": [ + "dmg", + "zip" + ], + "category": "public.app-category.games", + "artifactName": "OpenNOW-v${version}-mac-${arch}.${ext}" + }, + "linux": { + "target": [ + "AppImage", + "deb" + ], + "category": "Game", + "maintainer": "zortos293 ", + "artifactName": "OpenNOW-v${version}-linux-${arch}.${ext}" + } + } +} diff --git a/opennow-stable/src/main/gfn/auth.ts b/opennow-stable/src/main/gfn/auth.ts new file mode 100644 index 0000000..b72cc79 --- /dev/null +++ b/opennow-stable/src/main/gfn/auth.ts @@ -0,0 +1,776 @@ +import { createServer } from "node:http"; +import { createHash, randomBytes } from "node:crypto"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import net from "node:net"; +import os from "node:os"; + +import { shell } from "electron"; + +import type { + AuthLoginRequest, + AuthSession, + AuthSessionResult, + AuthTokens, + AuthUser, + LoginProvider, + StreamRegion, + SubscriptionInfo, +} from "@shared/gfn"; +import { fetchSubscription, fetchDynamicRegions } from "./subscription"; + +const SERVICE_URLS_ENDPOINT = "https://pcs.geforcenow.com/v1/serviceUrls"; +const TOKEN_ENDPOINT = "https://login.nvidia.com/token"; +const USERINFO_ENDPOINT = "https://login.nvidia.com/userinfo"; +const AUTH_ENDPOINT = "https://login.nvidia.com/authorize"; + +const CLIENT_ID = "ZU7sPN-miLujMD95LfOQ453IB0AtjM8sMyvgJ9wCXEQ"; +const SCOPES = "openid consent email tk_client age"; +const DEFAULT_IDP_ID = "PDiAhv2kJTFeQ7WOPqiQ2tRZ7lGhR2X11dXvM4TZSxg"; + +const GFN_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 NVIDIACEFClient/HEAD/debb5919f6 GFN-PC/2.0.80.173"; + +const REDIRECT_PORTS = [2259, 6460, 7119, 8870, 9096]; + +interface PersistedAuthState { + session: AuthSession | null; + selectedProvider: LoginProvider | null; +} + +interface ServiceUrlsResponse { + requestStatus?: { + statusCode?: number; + }; + gfnServiceInfo?: { + gfnServiceEndpoints?: Array<{ + idpId: string; + loginProviderCode: string; + loginProviderDisplayName: string; + streamingServiceUrl: string; + loginProviderPriority?: number; + }>; + }; +} + +interface TokenResponse { + access_token: string; + refresh_token?: string; + id_token?: string; + expires_in?: number; +} + +interface ServerInfoResponse { + requestStatus?: { + serverId?: string; + }; + metaData?: Array<{ + key: string; + value: string; + }>; +} + +function defaultProvider(): LoginProvider { + return { + idpId: DEFAULT_IDP_ID, + code: "NVIDIA", + displayName: "NVIDIA", + streamingServiceUrl: "https://prod.cloudmatchbeta.nvidiagrid.net/", + priority: 0, + }; +} + +function normalizeProvider(provider: LoginProvider): LoginProvider { + return { + ...provider, + streamingServiceUrl: provider.streamingServiceUrl.endsWith("/") + ? provider.streamingServiceUrl + : `${provider.streamingServiceUrl}/`, + }; +} + +function decodeBase64Url(value: string): string { + const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); + const padding = normalized.length % 4; + const padded = padding === 0 ? normalized : `${normalized}${"=".repeat(4 - padding)}`; + return Buffer.from(padded, "base64").toString("utf8"); +} + +function parseJwtPayload(token: string): T | null { + const parts = token.split("."); + if (parts.length !== 3) { + return null; + } + try { + const payload = decodeBase64Url(parts[1]); + return JSON.parse(payload) as T; + } catch { + return null; + } +} + +function generateDeviceId(): string { + const host = os.hostname(); + const username = os.userInfo().username; + return createHash("sha256").update(`${host}:${username}:opennow-stable`).digest("hex"); +} + +function generatePkce(): { verifier: string; challenge: string } { + const verifier = randomBytes(64) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, "") + .slice(0, 86); + + const challenge = createHash("sha256") + .update(verifier) + .digest("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); + + return { verifier, challenge }; +} + +function buildAuthUrl(provider: LoginProvider, challenge: string, port: number): string { + const redirectUri = `http://localhost:${port}`; + const nonce = randomBytes(16).toString("hex"); + const params = new URLSearchParams({ + response_type: "code", + device_id: generateDeviceId(), + scope: SCOPES, + client_id: CLIENT_ID, + redirect_uri: redirectUri, + ui_locales: "en_US", + nonce, + prompt: "select_account", + code_challenge: challenge, + code_challenge_method: "S256", + idp_id: provider.idpId, + }); + return `${AUTH_ENDPOINT}?${params.toString()}`; +} + +async function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.once("error", () => resolve(false)); + server.once("listening", () => { + server.close(() => resolve(true)); + }); + server.listen(port, "127.0.0.1"); + }); +} + +async function findAvailablePort(): Promise { + for (const port of REDIRECT_PORTS) { + if (await isPortAvailable(port)) { + return port; + } + } + + throw new Error("No available OAuth callback ports"); +} + +async function waitForAuthorizationCode(port: number, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const server = createServer((request, response) => { + const url = new URL(request.url ?? "/", `http://localhost:${port}`); + const code = url.searchParams.get("code"); + const error = url.searchParams.get("error"); + + const html = `OpenNOW Login

OpenNOW Login

${ + code + ? "Login complete. You can close this window and return to OpenNOW Stable." + : "Login failed or was cancelled. You can close this window and return to OpenNOW Stable." + }

`; + + response.statusCode = 200; + response.setHeader("Content-Type", "text/html; charset=utf-8"); + response.end(html); + + server.close(() => { + if (code) { + resolve(code); + return; + } + reject(new Error(error ?? "Authorization failed")); + }); + }); + + server.listen(port, "127.0.0.1", () => { + const timer = setTimeout(() => { + server.close(() => reject(new Error("Timed out waiting for OAuth callback"))); + }, timeoutMs); + + server.once("close", () => clearTimeout(timer)); + }); + }); +} + +async function exchangeAuthorizationCode(code: string, verifier: string, port: number): Promise { + const body = new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: `http://localhost:${port}`, + code_verifier: verifier, + }); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + Origin: "https://nvfile", + Referer: "https://nvfile/", + Accept: "application/json, text/plain, */*", + "User-Agent": GFN_USER_AGENT, + }, + body, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Token exchange failed (${response.status}): ${text.slice(0, 400)}`); + } + + const payload = (await response.json()) as TokenResponse; + return { + accessToken: payload.access_token, + refreshToken: payload.refresh_token, + idToken: payload.id_token, + expiresAt: Date.now() + (payload.expires_in ?? 86400) * 1000, + }; +} + +async function refreshAuthTokens(refreshToken: string): Promise { + const body = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: CLIENT_ID, + }); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + Origin: "https://nvfile", + Accept: "application/json, text/plain, */*", + "User-Agent": GFN_USER_AGENT, + }, + body, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Token refresh failed (${response.status}): ${text.slice(0, 400)}`); + } + + const payload = (await response.json()) as TokenResponse; + return { + accessToken: payload.access_token, + refreshToken: payload.refresh_token ?? refreshToken, + idToken: payload.id_token, + expiresAt: Date.now() + (payload.expires_in ?? 86400) * 1000, + }; +} + +async function fetchUserInfo(tokens: AuthTokens): Promise { + const jwtToken = tokens.idToken ?? tokens.accessToken; + const parsed = parseJwtPayload<{ + sub?: string; + email?: string; + preferred_username?: string; + gfn_tier?: string; + picture?: string; + }>(jwtToken); + + if (parsed?.sub) { + return { + userId: parsed.sub, + displayName: parsed.preferred_username ?? parsed.email?.split("@")[0] ?? "User", + email: parsed.email, + avatarUrl: parsed.picture, + membershipTier: parsed.gfn_tier ?? "FREE", + }; + } + + const response = await fetch(USERINFO_ENDPOINT, { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + Origin: "https://nvfile", + Accept: "application/json", + "User-Agent": GFN_USER_AGENT, + }, + }); + + if (!response.ok) { + throw new Error(`User info failed (${response.status})`); + } + + const payload = (await response.json()) as { + sub: string; + preferred_username?: string; + email?: string; + picture?: string; + }; + + return { + userId: payload.sub, + displayName: payload.preferred_username ?? payload.email?.split("@")[0] ?? "User", + email: payload.email, + avatarUrl: payload.picture, + membershipTier: "FREE", + }; +} + +export class AuthService { + private providers: LoginProvider[] = []; + private session: AuthSession | null = null; + private selectedProvider: LoginProvider = defaultProvider(); + private cachedSubscription: SubscriptionInfo | null = null; + private cachedVpcId: string | null = null; + + constructor(private readonly statePath: string) {} + + async initialize(): Promise { + try { + await access(this.statePath); + } catch { + await mkdir(dirname(this.statePath), { recursive: true }); + await this.persist(); + return; + } + + try { + const raw = await readFile(this.statePath, "utf8"); + const parsed = JSON.parse(raw) as PersistedAuthState; + if (parsed.selectedProvider) { + this.selectedProvider = normalizeProvider(parsed.selectedProvider); + } + if (parsed.session) { + this.session = { + ...parsed.session, + provider: normalizeProvider(parsed.session.provider), + }; + + // Refresh the real tier from MES API on session restore + // (persisted tier may be stale or was "FREE" from JWT fallback) + await this.enrichUserTier(); + await this.persist(); + } + } catch { + this.session = null; + this.selectedProvider = defaultProvider(); + await this.persist(); + } + } + + private async persist(): Promise { + const payload: PersistedAuthState = { + session: this.session, + selectedProvider: this.selectedProvider, + }; + + await mkdir(dirname(this.statePath), { recursive: true }); + await writeFile(this.statePath, JSON.stringify(payload, null, 2), "utf8"); + } + + async getProviders(): Promise { + if (this.providers.length > 0) { + return this.providers; + } + + let response: Response; + try { + response = await fetch(SERVICE_URLS_ENDPOINT, { + headers: { + Accept: "application/json", + "User-Agent": GFN_USER_AGENT, + }, + }); + } catch (error) { + console.warn("Failed to fetch providers, using default:", error); + this.providers = [defaultProvider()]; + return this.providers; + } + + if (!response.ok) { + console.warn(`Providers fetch failed with status ${response.status}, using default`); + this.providers = [defaultProvider()]; + return this.providers; + } + + try { + const payload = (await response.json()) as ServiceUrlsResponse; + const endpoints = payload.gfnServiceInfo?.gfnServiceEndpoints ?? []; + + const providers = endpoints + .map((entry) => ({ + idpId: entry.idpId, + code: entry.loginProviderCode, + displayName: + entry.loginProviderCode === "BPC" ? "bro.game" : entry.loginProviderDisplayName, + streamingServiceUrl: entry.streamingServiceUrl, + priority: entry.loginProviderPriority ?? 0, + })) + .sort((a, b) => a.priority - b.priority) + .map(normalizeProvider); + + this.providers = providers.length > 0 ? providers : [defaultProvider()]; + console.log(`Loaded ${this.providers.length} providers`); + return this.providers; + } catch (error) { + console.warn("Failed to parse providers response, using default:", error); + this.providers = [defaultProvider()]; + return this.providers; + } + } + + getSession(): AuthSession | null { + return this.session; + } + + getSelectedProvider(): LoginProvider { + return this.selectedProvider; + } + + async getRegions(explicitToken?: string): Promise { + const provider = this.getSelectedProvider(); + const base = provider.streamingServiceUrl.endsWith("/") + ? provider.streamingServiceUrl + : `${provider.streamingServiceUrl}/`; + + let token = explicitToken; + if (!token) { + const session = await this.ensureValidSession(); + token = session ? session.tokens.idToken ?? session.tokens.accessToken : undefined; + } + + const headers: Record = { + Accept: "application/json", + "nv-client-id": "ec7e38d4-03af-4b58-b131-cfb0495903ab", + "nv-client-type": "BROWSER", + "nv-client-version": "2.0.80.173", + "nv-client-streamer": "WEBRTC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + "User-Agent": GFN_USER_AGENT, + }; + + if (token) { + headers.Authorization = `GFNJWT ${token}`; + } + + let response: Response; + try { + response = await fetch(`${base}v2/serverInfo`, { + headers, + }); + } catch { + return []; + } + + if (!response.ok) { + return []; + } + + const payload = (await response.json()) as ServerInfoResponse; + const regions = (payload.metaData ?? []) + .filter((entry) => entry.value.startsWith("https://")) + .filter((entry) => entry.key !== "gfn-regions" && !entry.key.startsWith("gfn-")) + .map((entry) => ({ + name: entry.key, + url: entry.value.endsWith("/") ? entry.value : `${entry.value}/`, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + return regions; + } + + async login(input: AuthLoginRequest): Promise { + const providers = await this.getProviders(); + const selected = + providers.find((provider) => provider.idpId === input.providerIdpId) ?? + this.selectedProvider ?? + providers[0] ?? + defaultProvider(); + + this.selectedProvider = normalizeProvider(selected); + + const { verifier, challenge } = generatePkce(); + const port = await findAvailablePort(); + const authUrl = buildAuthUrl(this.selectedProvider, challenge, port); + + const codePromise = waitForAuthorizationCode(port, 120000); + await shell.openExternal(authUrl); + const code = await codePromise; + + const tokens = await exchangeAuthorizationCode(code, verifier, port); + const user = await fetchUserInfo(tokens); + + this.session = { + provider: this.selectedProvider, + tokens, + user, + }; + + // Fetch real membership tier from MES subscription API + // (JWT does not contain gfn_tier, so fetchUserInfo always falls back to "FREE") + await this.enrichUserTier(); + + await this.persist(); + return this.session; + } + + async logout(): Promise { + this.session = null; + this.cachedSubscription = null; + this.clearVpcCache(); + await this.persist(); + } + + /** + * Fetch subscription info for the current user. + * Uses caching - call clearSubscriptionCache() to force refresh. + */ + async getSubscription(): Promise { + // Return cached subscription if available + if (this.cachedSubscription) { + return this.cachedSubscription; + } + + const session = await this.ensureValidSession(); + if (!session) { + return null; + } + + const token = session.tokens.idToken ?? session.tokens.accessToken; + const userId = session.user.userId; + + // Fetch dynamic regions to get the VPC ID (handles Alliance partners correctly) + const { vpcId } = await fetchDynamicRegions(token, this.selectedProvider.streamingServiceUrl); + + const subscription = await fetchSubscription(token, userId, vpcId ?? undefined); + this.cachedSubscription = subscription; + return subscription; + } + + /** + * Clear the cached subscription info. + * Called automatically on logout. + */ + clearSubscriptionCache(): void { + this.cachedSubscription = null; + } + + /** + * Get the cached subscription without fetching. + * Returns null if not cached. + */ + getCachedSubscription(): SubscriptionInfo | null { + return this.cachedSubscription; + } + + /** + * Get the VPC ID for the current provider. + * Returns cached value if available, otherwise fetches from serverInfo endpoint. + * The VPC ID is used for Alliance partner support and routing to correct data center. + */ + async getVpcId(explicitToken?: string): Promise { + // Return cached VPC ID if available + if (this.cachedVpcId) { + return this.cachedVpcId; + } + + const provider = this.getSelectedProvider(); + const base = provider.streamingServiceUrl.endsWith("/") + ? provider.streamingServiceUrl + : `${provider.streamingServiceUrl}/`; + + let token = explicitToken; + if (!token) { + const session = await this.ensureValidSession(); + token = session ? session.tokens.idToken ?? session.tokens.accessToken : undefined; + } + + const headers: Record = { + Accept: "application/json", + "nv-client-id": "ec7e38d4-03af-4b58-b131-cfb0495903ab", + "nv-client-type": "BROWSER", + "nv-client-version": "2.0.80.173", + "nv-client-streamer": "WEBRTC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + "User-Agent": GFN_USER_AGENT, + }; + + if (token) { + headers.Authorization = `GFNJWT ${token}`; + } + + try { + const response = await fetch(`${base}v2/serverInfo`, { + headers, + }); + + if (!response.ok) { + return null; + } + + const payload = (await response.json()) as ServerInfoResponse; + const vpcId = payload.requestStatus?.serverId ?? null; + + // Cache the VPC ID + if (vpcId) { + this.cachedVpcId = vpcId; + } + + return vpcId; + } catch { + return null; + } + } + + /** + * Clear the cached VPC ID. + * Called automatically on logout. + */ + clearVpcCache(): void { + this.cachedVpcId = null; + } + + /** + * Get the cached VPC ID without fetching. + * Returns null if not cached. + */ + getCachedVpcId(): string | null { + return this.cachedVpcId; + } + + /** + * Enrich the current session's user with the real membership tier from MES API. + * Falls back silently to the existing tier if the fetch fails. + */ + private async enrichUserTier(): Promise { + if (!this.session) return; + + try { + const subscription = await this.getSubscription(); + if (subscription && subscription.membershipTier) { + this.session = { + ...this.session, + user: { + ...this.session.user, + membershipTier: subscription.membershipTier, + }, + }; + console.log(`Resolved membership tier: ${subscription.membershipTier}`); + } + } catch (error) { + console.warn("Failed to fetch subscription tier, keeping fallback:", error); + } + } + + private shouldRefresh(tokens: AuthTokens): boolean { + return tokens.expiresAt - Date.now() < 10 * 60 * 1000; + } + + async ensureValidSessionWithStatus(forceRefresh = false): Promise { + if (!this.session) { + return { + session: null, + refresh: { + attempted: false, + forced: forceRefresh, + outcome: "not_attempted", + message: "No saved session found.", + }, + }; + } + + const tokens = this.session.tokens; + const shouldRefreshNow = forceRefresh || this.shouldRefresh(tokens); + if (!shouldRefreshNow) { + return { + session: this.session, + refresh: { + attempted: false, + forced: forceRefresh, + outcome: "not_attempted", + message: "Session token is still valid.", + }, + }; + } + + if (!tokens.refreshToken) { + return { + session: this.session, + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "missing_refresh_token", + message: "No refresh token available. Using saved session token.", + }, + }; + } + + try { + const refreshed = await refreshAuthTokens(tokens.refreshToken); + const user = await fetchUserInfo(refreshed); + this.session = { + provider: this.session.provider, + tokens: refreshed, + user, + }; + + // Re-fetch real tier after token refresh + this.clearSubscriptionCache(); + await this.enrichUserTier(); + + await this.persist(); + return { + session: this.session, + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "refreshed", + message: forceRefresh + ? "Saved session token refreshed." + : "Session token refreshed because it was near expiry.", + }, + }; + } catch (error) { + const refreshError = + error instanceof Error ? error.message : "Unknown error while refreshing token"; + return { + session: this.session, + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "failed", + message: "Token refresh failed. Using saved session token.", + error: refreshError, + }, + }; + } + } + + async ensureValidSession(): Promise { + const result = await this.ensureValidSessionWithStatus(false); + return result.session; + } + + async resolveJwtToken(explicitToken?: string): Promise { + if (explicitToken && explicitToken.trim()) { + return explicitToken; + } + + const session = await this.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + + return session.tokens.idToken ?? session.tokens.accessToken; + } +} diff --git a/opennow-stable/src/main/gfn/cloudmatch.ts b/opennow-stable/src/main/gfn/cloudmatch.ts new file mode 100644 index 0000000..931de4a --- /dev/null +++ b/opennow-stable/src/main/gfn/cloudmatch.ts @@ -0,0 +1,918 @@ +import crypto from "node:crypto"; + +import type { + ActiveSessionInfo, + IceServer, + SessionClaimRequest, + SessionCreateRequest, + SessionInfo, + SessionPollRequest, + SessionStopRequest, + StreamSettings, +} from "@shared/gfn"; + +import { + colorQualityBitDepth, + colorQualityChromaFormat, +} from "@shared/gfn"; + +import type { CloudMatchRequest, CloudMatchResponse, GetSessionsResponse } from "./types"; +import { SessionError } from "./errorCodes"; + +const GFN_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 NVIDIACEFClient/HEAD/debb5919f6 GFN-PC/2.0.80.173"; +const GFN_CLIENT_VERSION = "2.0.80.173"; + +function normalizeIceServers(response: CloudMatchResponse): IceServer[] { + const raw = response.session.iceServerConfiguration?.iceServers ?? []; + const servers = raw + .map((entry) => { + const urls = Array.isArray(entry.urls) ? entry.urls : [entry.urls]; + return { + urls, + username: entry.username, + credential: entry.credential, + }; + }) + .filter((entry) => entry.urls.length > 0); + + if (servers.length > 0) { + return servers; + } + + return [ + { urls: ["stun:s1.stun.gamestream.nvidia.com:19308"] }, + { urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"] }, + ]; +} + +/** + * Extract the streaming server IP from the CloudMatch response, matching Rust's + * `streaming_server_ip()` priority chain: + * 1. connectionInfo[usage==14].ip (direct IP) + * 2. Host extracted from connectionInfo[usage==14].resourcePath (for rtsps:// URLs) + * 3. sessionControlInfo.ip (fallback) + */ +function streamingServerIp(response: CloudMatchResponse): string | null { + const connections = response.session.connectionInfo ?? []; + const sigConn = connections.find((conn) => conn.usage === 14); + + if (sigConn) { + // Priority 1: Direct IP field + const rawIp = sigConn.ip; + const directIp = Array.isArray(rawIp) ? rawIp[0] : rawIp; + if (directIp && directIp.length > 0) { + return directIp; + } + + // Priority 2: Extract host from resourcePath (Alliance format: rtsps://host:port) + if (sigConn.resourcePath) { + const host = extractHostFromUrl(sigConn.resourcePath); + if (host) return host; + } + } + + // Priority 3: sessionControlInfo.ip + const controlIp = response.session.sessionControlInfo?.ip; + if (controlIp && controlIp.length > 0) { + return Array.isArray(controlIp) ? controlIp[0] : controlIp; + } + + return null; +} + +/** + * Extract host from a URL string (handles rtsps://, rtsp://, wss://, https://). + * Matches Rust's extract_host_from_url(). + */ +function extractHostFromUrl(url: string): string | null { + const prefixes = ["rtsps://", "rtsp://", "wss://", "https://"]; + let afterProto: string | null = null; + for (const prefix of prefixes) { + if (url.startsWith(prefix)) { + afterProto = url.slice(prefix.length); + break; + } + } + if (!afterProto) return null; + + // Get host (before port or path) + const host = afterProto.split(":")[0]?.split("/")[0]; + if (!host || host.length === 0 || host.startsWith(".")) return null; + return host; +} + +/** + * Check if a given IP/hostname is a CloudMatch zone load balancer hostname + * (not a real game server IP). Zone hostnames look like: + * np-ams-06.cloudmatchbeta.nvidiagrid.net + */ +function isZoneHostname(ip: string): boolean { + return ip.includes("cloudmatchbeta.nvidiagrid.net") || ip.includes("cloudmatch.nvidiagrid.net"); +} + +function resolveSignaling(response: CloudMatchResponse): { + serverIp: string; + signalingServer: string; + signalingUrl: string; + mediaConnectionInfo?: { ip: string; port: number }; +} { + const connections = response.session.connectionInfo ?? []; + const signalingConnection = + connections.find((conn) => conn.usage === 14 && conn.ip) ?? connections.find((conn) => conn.ip); + + // Use the Rust-matching priority chain for server IP + const serverIp = streamingServerIp(response); + if (!serverIp) { + throw new Error("CloudMatch response did not include a signaling host"); + } + + const resourcePath = signalingConnection?.resourcePath ?? "/nvst/"; + + // Build signaling URL matching Rust's build_signaling_url() behavior: + // - rtsps://host:port -> extract host, convert to wss://host/nvst/ + // - wss://... -> use as-is + // - /path -> wss://serverIp:443/path + // - fallback -> wss://serverIp:443/nvst/ + const { signalingUrl, signalingHost } = buildSignalingUrl(resourcePath, serverIp); + + // Use the resolved signaling host (which may differ from serverIp if extracted from rtsps:// URL) + const effectiveHost = signalingHost ?? serverIp; + const signalingServer = effectiveHost.includes(":") + ? effectiveHost + : `${effectiveHost}:443`; + + return { + serverIp, + signalingServer, + signalingUrl, + mediaConnectionInfo: resolveMediaConnectionInfo(connections, serverIp), + }; +} + +/** + * Resolve the media connection endpoint (IP + port) from the session's connectionInfo array. + * Matches Rust's media_connection_info() priority chain: + * 1. usage=2 (Primary media path, UDP) + * 2. usage=17 (Alternative media path) + * 3. usage=14 with highest port (Alliance fallback — distinguishes media port from signaling port) + * 4. Fallback: use serverIp with the highest port from any usage=14 entry + * + * For each entry, IP is extracted from: + * a. The .ip field directly + * b. The hostname in .resourcePath (e.g. rtsps://80-250-97-40.server.net:48322) + * c. Fallback to serverIp (only for usage=14 Alliance fallback) + */ +function resolveMediaConnectionInfo( + connections: Array<{ ip?: string; port: number; usage: number; protocol?: number; resourcePath?: string }>, + serverIp: string, +): { ip: string; port: number } | undefined { + // Helper: extract IP from a connection entry + const extractIp = (conn: { ip?: string; resourcePath?: string }): string | null => { + // Try direct IP field + const rawIp = conn.ip; + const directIp = Array.isArray(rawIp) ? rawIp[0] : rawIp; + if (directIp && directIp.length > 0) return directIp; + + // Try hostname from resourcePath + if (conn.resourcePath) { + const host = extractHostFromUrl(conn.resourcePath); + if (host) return host; + } + + return null; + }; + + // Helper: extract port from a connection entry (fallback to resourcePath URL port) + const extractPort = (conn: { port: number; resourcePath?: string }): number => { + if (conn.port > 0) return conn.port; + + // Try extracting port from resourcePath URL + if (conn.resourcePath) { + try { + const url = new URL(conn.resourcePath.replace("rtsps://", "https://").replace("rtsp://", "http://")); + const portStr = url.port; + if (portStr) return parseInt(portStr, 10); + } catch { + // Ignore + } + } + + return 0; + }; + + // Priority 1: usage=2 (Primary media path, UDP) + const primary = connections.find((c) => c.usage === 2); + if (primary) { + const ip = extractIp(primary); + const port = extractPort(primary); + console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=2 candidate: ip=${ip}, port=${port}`); + if (ip && port > 0) return { ip, port }; + } + + // Priority 2: usage=17 (Alternative media path) + const alt = connections.find((c) => c.usage === 17); + if (alt) { + const ip = extractIp(alt); + const port = extractPort(alt); + console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=17 candidate: ip=${ip}, port=${port}`); + if (ip && port > 0) return { ip, port }; + } + + // Priority 3: usage=14 with highest port (Alliance fallback) + const alliance = connections + .filter((c) => c.usage === 14) + .sort((a, b) => b.port - a.port); + + for (const conn of alliance) { + const ip = extractIp(conn) ?? serverIp; + const port = extractPort(conn); + console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=14 candidate: ip=${ip}, port=${port} (serverIp fallback=${serverIp})`); + if (ip && port > 0) return { ip, port }; + } + + console.log("[CloudMatch] resolveMediaConnectionInfo: NO valid media connection info found"); + return undefined; +} + +/** + * Build signaling WSS URL from the resourcePath, matching Rust implementation. + * Returns the URL and optionally the extracted host (if different from serverIp). + */ +function buildSignalingUrl( + raw: string, + serverIp: string, +): { signalingUrl: string; signalingHost: string | null } { + if (raw.startsWith("rtsps://") || raw.startsWith("rtsp://")) { + // Extract hostname from RTSP URL, convert to wss:// + const withoutScheme = raw.startsWith("rtsps://") + ? raw.slice("rtsps://".length) + : raw.slice("rtsp://".length); + const host = withoutScheme.split(":")[0]?.split("/")[0]; + if (host && host.length > 0 && !host.startsWith(".")) { + return { + signalingUrl: `wss://${host}/nvst/`, + signalingHost: host, + }; + } + return { + signalingUrl: `wss://${serverIp}:443/nvst/`, + signalingHost: null, + }; + } + + if (raw.startsWith("wss://")) { + // Already a full WSS URL, use as-is; extract host + const withoutScheme = raw.slice("wss://".length); + const host = withoutScheme.split("/")[0] ?? null; + return { signalingUrl: raw, signalingHost: host }; + } + + if (raw.startsWith("/")) { + // Relative path + return { + signalingUrl: `wss://${serverIp}:443${raw}`, + signalingHost: null, + }; + } + + // Fallback + return { + signalingUrl: `wss://${serverIp}:443/nvst/`, + signalingHost: null, + }; +} + +function requestHeaders(token: string): Record { + const clientId = crypto.randomUUID(); + const deviceId = crypto.randomUUID(); + + return { + "User-Agent": GFN_USER_AGENT, + Authorization: `GFNJWT ${token}`, + "Content-Type": "application/json", + Origin: "https://play.geforcenow.com", + Referer: "https://play.geforcenow.com/", + "nv-browser-type": "CHROME", + "nv-client-id": clientId, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-device-make": "UNKNOWN", + "nv-device-model": "UNKNOWN", + "nv-device-os": process.platform === "win32" ? "WINDOWS" : process.platform === "darwin" ? "MACOS" : "LINUX", + "nv-device-type": "DESKTOP", + "x-device-id": deviceId, + }; +} + +function parseResolution(input: string): { width: number; height: number } { + const [rawWidth, rawHeight] = input.split("x"); + const width = Number.parseInt(rawWidth ?? "", 10); + const height = Number.parseInt(rawHeight ?? "", 10); + + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return { width: 1920, height: 1080 }; + } + + return { width, height }; +} + +function timezoneOffsetMs(): number { + return -new Date().getTimezoneOffset() * 60 * 1000; +} + +function buildSessionRequestBody(input: SessionCreateRequest): CloudMatchRequest { + const { width, height } = parseResolution(input.settings.resolution); + const cq = input.settings.colorQuality; + // IMPORTANT: hdrEnabled is a SEPARATE toggle from color quality. + // The Rust reference (cloudmatch.rs) uses settings.hdr_enabled independently. + // 10-bit color depth does NOT mean HDR — you can have 10-bit SDR. + // Conflating them caused the server to set up an HDR pipeline, which + // dynamically downscaled resolution to ~540p. + const hdrEnabled = false; // No HDR toggle implemented yet; hardcode off like claim body + const bitDepth = colorQualityBitDepth(cq); + const chromaFormat = colorQualityChromaFormat(cq); + const accountLinked = input.accountLinked ?? true; + + return { + sessionRequestData: { + appId: input.appId, + internalTitle: input.internalTitle || null, + availableSupportedControllers: [], + networkTestSessionId: null, + parentSessionId: null, + clientIdentification: "GFN-PC", + deviceHashId: crypto.randomUUID(), + clientVersion: "30.0", + sdkVersion: "1.0", + streamerVersion: 1, + clientPlatformName: "windows", + clientRequestMonitorSettings: [ + { + widthInPixels: width, + heightInPixels: height, + framesPerSecond: input.settings.fps, + sdrHdrMode: hdrEnabled ? 1 : 0, + displayData: { + desiredContentMaxLuminance: hdrEnabled ? 1000 : 0, + desiredContentMinLuminance: 0, + desiredContentMaxFrameAverageLuminance: hdrEnabled ? 500 : 0, + }, + dpi: 100, + }, + ], + useOps: true, + audioMode: 2, + metaData: [ + { key: "SubSessionId", value: crypto.randomUUID() }, + { key: "wssignaling", value: "1" }, + { key: "GSStreamerType", value: "WebRTC" }, + { key: "networkType", value: "Unknown" }, + { key: "ClientImeSupport", value: "0" }, + { + key: "clientPhysicalResolution", + value: JSON.stringify({ horizontalPixels: width, verticalPixels: height }), + }, + { key: "surroundAudioInfo", value: "2" }, + ], + sdrHdrMode: hdrEnabled ? 1 : 0, + clientDisplayHdrCapabilities: hdrEnabled + ? { + version: 1, + hdrEdrSupportedFlagsInUint32: 1, + staticMetadataDescriptorId: 0, + } + : null, + surroundAudioInfo: 0, + remoteControllersBitmap: 0, + clientTimezoneOffset: timezoneOffsetMs(), + enhancedStreamMode: 1, + appLaunchMode: 1, + secureRTSPSupported: false, + partnerCustomData: "", + accountLinked, + enablePersistingInGameSettings: true, + userAge: 26, + requestedStreamingFeatures: { + reflex: input.settings.fps >= 120, + bitDepth, + cloudGsync: false, + enabledL4S: false, + mouseMovementFlags: 0, + trueHdr: hdrEnabled, + supportedHidDevices: 0, + profile: 0, + fallbackToLogicalResolution: false, + hidDevices: null, + chromaFormat, + prefilterMode: 0, + prefilterSharpness: 0, + prefilterNoiseReduction: 0, + hudStreamingMode: 0, + sdrColorSpace: 2, + hdrColorSpace: hdrEnabled ? 4 : 0, + }, + }, + }; +} + +function cloudmatchUrl(zone: string): string { + return `https://${zone}.cloudmatchbeta.nvidiagrid.net`; +} + +function resolveStreamingBaseUrl(zone: string, provided?: string): string { + if (provided && provided.trim()) { + const trimmed = provided.trim(); + return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed; + } + return cloudmatchUrl(zone); +} + +function shouldUseServerIp(baseUrl: string): boolean { + return baseUrl.includes("cloudmatchbeta.nvidiagrid.net"); +} + +function resolvePollStopBase(zone: string, provided?: string, serverIp?: string): string { + const base = resolveStreamingBaseUrl(zone, provided); + // Only use serverIp if it's a real server IP (not a zone hostname). + // The Rust version checks: if we're NOT an alliance partner AND we have a server_ip, use it. + // But if the "serverIp" is actually the zone hostname (from an early poll when connectionInfo + // was empty), using it is circular and doesn't help. + if (serverIp && shouldUseServerIp(base) && !isZoneHostname(serverIp)) { + return `https://${serverIp}`; + } + return base; +} + +function toSessionInfo(zone: string, streamingBaseUrl: string, payload: CloudMatchResponse): SessionInfo { + if (payload.requestStatus.statusCode !== 1) { + // Use SessionError for parsing error responses + const errorJson = JSON.stringify(payload); + throw SessionError.fromResponse(200, errorJson); + } + + const signaling = resolveSignaling(payload); + + // Debug logging to trace signaling resolution + const connections = payload.session.connectionInfo ?? []; + console.log( + `[CloudMatch] toSessionInfo: status=${payload.session.status}, ` + + `connectionInfo=${connections.length} entries, ` + + `serverIp=${signaling.serverIp}, ` + + `signalingServer=${signaling.signalingServer}, ` + + `signalingUrl=${signaling.signalingUrl}`, + ); + for (const conn of connections) { + console.log( + `[CloudMatch] conn: usage=${conn.usage} ip=${conn.ip ?? "null"} port=${conn.port} ` + + `resourcePath=${conn.resourcePath ?? "null"}`, + ); + } + + return { + sessionId: payload.session.sessionId, + status: payload.session.status, + zone, + streamingBaseUrl, + serverIp: signaling.serverIp, + signalingServer: signaling.signalingServer, + signalingUrl: signaling.signalingUrl, + gpuType: payload.session.gpuType, + iceServers: normalizeIceServers(payload), + mediaConnectionInfo: signaling.mediaConnectionInfo, + }; +} + +export async function createSession(input: SessionCreateRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session creation"); + } + + if (!/^\d+$/.test(input.appId)) { + throw new Error(`Invalid launch appId '${input.appId}' (must be numeric)`); + } + + const body = buildSessionRequestBody(input); + + const base = resolveStreamingBaseUrl(input.zone, input.streamingBaseUrl); + const url = `${base}/v2/session?keyboardLayout=en-US&languageCode=en_US`; + const response = await fetch(url, { + method: "POST", + headers: requestHeaders(input.token), + body: JSON.stringify(body), + }); + + const text = await response.text(); + if (!response.ok) { + // Use SessionError to parse and throw detailed error + throw SessionError.fromResponse(response.status, text); + } + + const payload = JSON.parse(text) as CloudMatchResponse; + return toSessionInfo(input.zone, base, payload); +} + +export async function pollSession(input: SessionPollRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session polling"); + } + + const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); + const url = `${base}/v2/session/${input.sessionId}`; + const headers = requestHeaders(input.token); + const response = await fetch(url, { + method: "GET", + headers, + }); + + const text = await response.text(); + if (!response.ok) { + throw SessionError.fromResponse(response.status, text); + } + + const payload = JSON.parse(text) as CloudMatchResponse; + + // Match Rust behavior: if the poll was routed through the zone load balancer + // and the response now contains a real server IP in connectionInfo, re-poll + // directly via the real server IP. This ensures the signaling data and + // connection info are correct (the zone LB may return different data than + // a direct server poll). + const realServerIp = streamingServerIp(payload); + const polledViaZone = isZoneHostname(new URL(base).hostname); + const realIpDiffers = + realServerIp && + realServerIp.length > 0 && + !isZoneHostname(realServerIp) && + realServerIp !== input.serverIp; + + if (polledViaZone && realIpDiffers && (payload.session.status === 2 || payload.session.status === 3)) { + // Session is ready and we now know the real server IP — re-poll directly + console.log( + `[CloudMatch] Session ready: re-polling via real server IP ${realServerIp} (was: ${new URL(base).hostname})`, + ); + const directBase = `https://${realServerIp}`; + const directUrl = `${directBase}/v2/session/${input.sessionId}`; + try { + const directResponse = await fetch(directUrl, { + method: "GET", + headers, + }); + if (directResponse.ok) { + const directText = await directResponse.text(); + const directPayload = JSON.parse(directText) as CloudMatchResponse; + if (directPayload.requestStatus.statusCode === 1) { + console.log("[CloudMatch] Direct re-poll succeeded, using direct response for signaling info"); + return toSessionInfo(input.zone, directBase, directPayload); + } + } + } catch (e) { + // Direct poll failed — fall through to use the original zone LB response + console.warn("[CloudMatch] Direct re-poll failed, using zone LB response:", e); + } + } + + return toSessionInfo(input.zone, base, payload); +} + +export async function stopSession(input: SessionStopRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session stop"); + } + + const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); + const url = `${base}/v2/session/${input.sessionId}`; + const response = await fetch(url, { + method: "DELETE", + headers: requestHeaders(input.token), + }); + + if (!response.ok) { + const text = await response.text(); + // Use SessionError to parse and throw detailed error + throw SessionError.fromResponse(response.status, text); + } +} + +/** + * Get list of active sessions (status 2 or 3) + * Returns sessions that are Ready or Streaming + */ +export async function getActiveSessions( + token: string, + streamingBaseUrl: string, +): Promise { + if (!token) { + throw new Error("Missing token for getting active sessions"); + } + + const deviceId = crypto.randomUUID(); + const clientId = crypto.randomUUID(); + + const base = streamingBaseUrl.trim().endsWith("/") + ? streamingBaseUrl.trim().slice(0, -1) + : streamingBaseUrl.trim(); + const url = `${base}/v2/session`; + + const headers: Record = { + "User-Agent": GFN_USER_AGENT, + Authorization: `GFNJWT ${token}`, + "Content-Type": "application/json", + "nv-client-id": clientId, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-device-os": process.platform === "win32" ? "WINDOWS" : process.platform === "darwin" ? "MACOS" : "LINUX", + "nv-device-type": "DESKTOP", + "x-device-id": deviceId, + }; + + const response = await fetch(url, { + method: "GET", + headers, + }); + + const text = await response.text(); + + if (!response.ok) { + // Return empty list on failure (matching Rust behavior) + console.warn(`Get sessions failed: ${response.status} - ${text.slice(0, 200)}`); + return []; + } + + let sessionsResponse: GetSessionsResponse; + try { + sessionsResponse = JSON.parse(text) as GetSessionsResponse; + } catch { + return []; + } + + if (sessionsResponse.requestStatus.statusCode !== 1) { + console.warn(`Get sessions API error: ${sessionsResponse.requestStatus.statusDescription}`); + return []; + } + + // Filter active sessions (status 2 = Ready, status 3 = Streaming) + const activeSessions: ActiveSessionInfo[] = sessionsResponse.sessions + .filter((s) => s.status === 2 || s.status === 3) + .map((s) => { + // Extract appId from sessionRequestData + const appId = s.sessionRequestData?.appId ? Number(s.sessionRequestData.appId) : 0; + + // Get server IP from sessionControlInfo + const serverIp = s.sessionControlInfo?.ip; + + // Build signaling URL from connection info + const connInfo = s.connectionInfo?.find((conn) => conn.usage === 14 && conn.ip); + const connIp = connInfo?.ip; + const signalingUrl = Array.isArray(connIp) + ? connIp.map((ip: string) => `wss://${ip}:443/nvst/`) + : typeof connIp === "string" + ? [`wss://${connIp}:443/nvst/`] + : Array.isArray(serverIp) + ? serverIp.map((ip: string) => `wss://${ip}:443/nvst/`) + : typeof serverIp === "string" + ? [`wss://${serverIp}:443/nvst/`] + : undefined; + + // Extract resolution and fps from monitor settings + const monitorSettings = s.monitorSettings?.[0]; + const resolution = monitorSettings + ? `${monitorSettings.widthInPixels ?? 0}x${monitorSettings.heightInPixels ?? 0}` + : undefined; + const fps = monitorSettings?.framesPerSecond ?? undefined; + + return { + sessionId: s.sessionId, + appId, + gpuType: s.gpuType, + status: s.status, + serverIp, + signalingUrl: Array.isArray(signalingUrl) ? signalingUrl[0] : signalingUrl, + resolution, + fps, + }; + }); + + return activeSessions; +} + +/** + * Build claim/resume request payload + */ +function buildClaimRequestBody(sessionId: string, appId: string, settings: StreamSettings): unknown { + const { width, height } = parseResolution(settings.resolution); + const cq = settings.colorQuality; + const chromaFormat = colorQualityChromaFormat(cq); + // Claim/resume uses SDR mode (matching Rust: hdr_enabled defaults false for claims). + // HDR is only negotiated on the initial session create. + const hdrEnabled = false; + const deviceId = crypto.randomUUID(); + const subSessionId = crypto.randomUUID(); + const timezoneMs = timezoneOffsetMs(); + + // Build HDR capabilities if enabled + const hdrCapabilities = hdrEnabled + ? { + version: 1, + hdrEdrSupportedFlagsInUint32: 3, // 1=HDR10, 2=EDR, 3=both + staticMetadataDescriptorId: 0, + displayData: { + maxLuminance: 1000, + minLuminance: 0.01, + maxFrameAverageLuminance: 500, + }, + } + : null; + + return { + action: 2, + data: "RESUME", + sessionRequestData: { + audioMode: 2, + remoteControllersBitmap: 0, + sdrHdrMode: hdrEnabled ? 1 : 0, + networkTestSessionId: null, + availableSupportedControllers: [], + clientVersion: "30.0", + deviceHashId: deviceId, + internalTitle: null, + clientPlatformName: "windows", + metaData: [ + { key: "SubSessionId", value: subSessionId }, + { key: "wssignaling", value: "1" }, + { key: "GSStreamerType", value: "WebRTC" }, + { key: "networkType", value: "Unknown" }, + { key: "ClientImeSupport", value: "0" }, + { + key: "clientPhysicalResolution", + value: JSON.stringify({ horizontalPixels: width, verticalPixels: height }), + }, + { key: "surroundAudioInfo", value: "2" }, + ], + surroundAudioInfo: 0, + clientTimezoneOffset: timezoneMs, + clientIdentification: "GFN-PC", + parentSessionId: null, + appId, + streamerVersion: 1, + clientRequestMonitorSettings: [ + { + widthInPixels: width, + heightInPixels: height, + framesPerSecond: settings.fps, + sdrHdrMode: hdrEnabled ? 1 : 0, + displayData: { + desiredContentMaxLuminance: hdrEnabled ? 1000 : 0, + desiredContentMinLuminance: 0, + desiredContentMaxFrameAverageLuminance: hdrEnabled ? 500 : 0, + }, + dpi: 0, + }, + ], + appLaunchMode: 1, + sdkVersion: "1.0", + enhancedStreamMode: 1, + useOps: true, + clientDisplayHdrCapabilities: hdrCapabilities, + accountLinked: true, + partnerCustomData: "", + enablePersistingInGameSettings: true, + secureRTSPSupported: false, + userAge: 26, + requestedStreamingFeatures: { + reflex: settings.fps >= 120, + bitDepth: 0, + cloudGsync: false, + enabledL4S: false, + profile: 0, + fallbackToLogicalResolution: false, + chromaFormat, + prefilterMode: 0, + hudStreamingMode: 0, + }, + }, + metaData: [], + }; +} + +/** + * Claim/Resume an existing session + * Required before connecting to an existing session + */ +export async function claimSession(input: SessionClaimRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session claim"); + } + + const deviceId = crypto.randomUUID(); + const clientId = crypto.randomUUID(); + + const claimUrl = `https://${input.serverIp}/v2/session/${input.sessionId}?keyboardLayout=en-US&languageCode=en_US`; + + // Provide default values for optional parameters + const appId = input.appId ?? "0"; + const settings = input.settings ?? { + resolution: "1920x1080", + fps: 60, + maxBitrateMbps: 75, + codec: "H264", + colorQuality: "8bit_420", + }; + + const payload = buildClaimRequestBody(input.sessionId, appId, settings); + + const headers: Record = { + "User-Agent": GFN_USER_AGENT, + Authorization: `GFNJWT ${input.token}`, + "Content-Type": "application/json", + Origin: "https://play.geforcenow.com", + Referer: "https://play.geforcenow.com/", + "nv-client-id": clientId, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-device-os": process.platform === "win32" ? "WINDOWS" : process.platform === "darwin" ? "MACOS" : "LINUX", + "nv-device-type": "DESKTOP", + "x-device-id": deviceId, + }; + + // Send claim request + const response = await fetch(claimUrl, { + method: "PUT", + headers, + body: JSON.stringify(payload), + }); + + const text = await response.text(); + + if (!response.ok) { + throw SessionError.fromResponse(response.status, text); + } + + const apiResponse = JSON.parse(text) as CloudMatchResponse; + + if (apiResponse.requestStatus.statusCode !== 1) { + throw SessionError.fromResponse(200, text); + } + + // Poll until session is ready (status 2 or 3) + const getUrl = `https://${input.serverIp}/v2/session/${input.sessionId}`; + const maxAttempts = 60; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (attempt > 1) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + // Build headers without Origin/Referer for polling + const pollHeaders: Record = { ...headers }; + delete pollHeaders["Origin"]; + delete pollHeaders["Referer"]; + + const pollResponse = await fetch(getUrl, { + method: "GET", + headers: pollHeaders, + }); + + if (!pollResponse.ok) { + continue; + } + + const pollText = await pollResponse.text(); + let pollApiResponse: CloudMatchResponse; + + try { + pollApiResponse = JSON.parse(pollText) as CloudMatchResponse; + } catch { + continue; + } + + const sessionData = pollApiResponse.session; + + if (sessionData.status === 2 || sessionData.status === 3) { + // Session is ready + const signaling = resolveSignaling(pollApiResponse); + + return { + sessionId: sessionData.sessionId, + status: sessionData.status, + zone: "", // Zone not applicable for claimed sessions + streamingBaseUrl: `https://${input.serverIp}`, + serverIp: signaling.serverIp, + signalingServer: signaling.signalingServer, + signalingUrl: signaling.signalingUrl, + gpuType: sessionData.gpuType, + iceServers: normalizeIceServers(pollApiResponse), + mediaConnectionInfo: signaling.mediaConnectionInfo, + }; + } + + // If status is not "cleaning up" (6), break early + if (sessionData.status !== 6) { + break; + } + } + + throw new Error("Session did not become ready after claiming"); +} diff --git a/opennow-stable/src/main/gfn/errorCodes.ts b/opennow-stable/src/main/gfn/errorCodes.ts new file mode 100644 index 0000000..3c08d48 --- /dev/null +++ b/opennow-stable/src/main/gfn/errorCodes.ts @@ -0,0 +1,978 @@ +/** + * GFN CloudMatch Error Codes + * + * Error code mappings extracted from the official GFN web client. + * These provide user-friendly error messages for session failures. + */ + +/** GFN Session Error Codes from official client */ +export enum GfnErrorCode { + // Success codes + Success = 15859712, + + // Client-side errors (3237085xxx - 3237093xxx) + InvalidOperation = 3237085186, + NetworkError = 3237089282, + GetActiveSessionServerError = 3237089283, + AuthTokenNotUpdated = 3237093377, + SessionFinishedState = 3237093378, + ResponseParseFailure = 3237093379, + InvalidServerResponse = 3237093381, + PutOrPostInProgress = 3237093382, + GridServerNotInitialized = 3237093383, + DOMExceptionInSessionControl = 3237093384, + InvalidAdStateTransition = 3237093386, + AuthTokenUpdateTimeout = 3237093387, + + // Server error codes (base 3237093632 + statusCode) + SessionServerErrorBegin = 3237093632, + RequestForbidden = 3237093634, // statusCode 2 + ServerInternalTimeout = 3237093635, // statusCode 3 + ServerInternalError = 3237093636, // statusCode 4 + ServerInvalidRequest = 3237093637, // statusCode 5 + ServerInvalidRequestVersion = 3237093638, // statusCode 6 + SessionListLimitExceeded = 3237093639, // statusCode 7 + InvalidRequestDataMalformed = 3237093640, // statusCode 8 + InvalidRequestDataMissing = 3237093641, // statusCode 9 + RequestLimitExceeded = 3237093642, // statusCode 10 + SessionLimitExceeded = 3237093643, // statusCode 11 + InvalidRequestVersionOutOfDate = 3237093644, // statusCode 12 + SessionEntitledTimeExceeded = 3237093645, // statusCode 13 + AuthFailure = 3237093646, // statusCode 14 + InvalidAuthenticationMalformed = 3237093647, // statusCode 15 + InvalidAuthenticationExpired = 3237093648, // statusCode 16 + InvalidAuthenticationNotFound = 3237093649, // statusCode 17 + EntitlementFailure = 3237093650, // statusCode 18 + InvalidAppIdNotAvailable = 3237093651, // statusCode 19 + InvalidAppIdNotFound = 3237093652, // statusCode 20 + InvalidSessionIdMalformed = 3237093653, // statusCode 21 + InvalidSessionIdNotFound = 3237093654, // statusCode 22 + EulaUnAccepted = 3237093655, // statusCode 23 + MaintenanceStatus = 3237093656, // statusCode 24 + ServiceUnAvailable = 3237093657, // statusCode 25 + SteamGuardRequired = 3237093658, // statusCode 26 + SteamLoginRequired = 3237093659, // statusCode 27 + SteamGuardInvalid = 3237093660, // statusCode 28 + SteamProfilePrivate = 3237093661, // statusCode 29 + InvalidCountryCode = 3237093662, // statusCode 30 + InvalidLanguageCode = 3237093663, // statusCode 31 + MissingCountryCode = 3237093664, // statusCode 32 + MissingLanguageCode = 3237093665, // statusCode 33 + SessionNotPaused = 3237093666, // statusCode 34 + EmailNotVerified = 3237093667, // statusCode 35 + InvalidAuthenticationUnsupportedProtocol = 3237093668, // statusCode 36 + InvalidAuthenticationUnknownToken = 3237093669, // statusCode 37 + InvalidAuthenticationCredentials = 3237093670, // statusCode 38 + SessionNotPlaying = 3237093671, // statusCode 39 + InvalidServiceResponse = 3237093672, // statusCode 40 + AppPatching = 3237093673, // statusCode 41 + GameNotFound = 3237093674, // statusCode 42 + NotEnoughCredits = 3237093675, // statusCode 43 + InvitationOnlyRegistration = 3237093676, // statusCode 44 + RegionNotSupportedForRegistration = 3237093677, // statusCode 45 + SessionTerminatedByAnotherClient = 3237093678, // statusCode 46 + DeviceIdAlreadyUsed = 3237093679, // statusCode 47 + ServiceNotExist = 3237093680, // statusCode 48 + SessionExpired = 3237093681, // statusCode 49 + SessionLimitPerDeviceReached = 3237093682, // statusCode 50 + ForwardingZoneOutOfCapacity = 3237093683, // statusCode 51 + RegionNotSupportedIndefinitely = 3237093684, // statusCode 52 + RegionBanned = 3237093685, // statusCode 53 + RegionOnHoldForFree = 3237093686, // statusCode 54 + RegionOnHoldForPaid = 3237093687, // statusCode 55 + AppMaintenanceStatus = 3237093688, // statusCode 56 + ResourcePoolNotConfigured = 3237093689, // statusCode 57 + InsufficientVmCapacity = 3237093690, // statusCode 58 + InsufficientRouteCapacity = 3237093691, // statusCode 59 + InsufficientScratchSpaceCapacity = 3237093692, // statusCode 60 + RequiredSeatInstanceTypeNotSupported = 3237093693, // statusCode 61 + ServerSessionQueueLengthExceeded = 3237093694, // statusCode 62 + RegionNotSupportedForStreaming = 3237093695, // statusCode 63 + SessionForwardRequestAllocationTimeExpired = 3237093696, // statusCode 64 + SessionForwardGameBinariesNotAvailable = 3237093697, // statusCode 65 + GameBinariesNotAvailableInRegion = 3237093698, // statusCode 66 + UekRetrievalFailed = 3237093699, // statusCode 67 + EntitlementFailureForResource = 3237093700, // statusCode 68 + SessionInQueueAbandoned = 3237093701, // statusCode 69 + MemberTerminated = 3237093702, // statusCode 70 + SessionRemovedFromQueueMaintenance = 3237093703, // statusCode 71 + ZoneMaintenanceStatus = 3237093704, // statusCode 72 + GuestModeCampaignDisabled = 3237093705, // statusCode 73 + RegionNotSupportedAnonymousAccess = 3237093706, // statusCode 74 + InstanceTypeNotSupportedInSingleRegion = 3237093707, // statusCode 75 + InvalidZoneForQueuedSession = 3237093710, // statusCode 78 + SessionWaitingAdsTimeExpired = 3237093711, // statusCode 79 + UserCancelledWatchingAds = 3237093712, // statusCode 80 + StreamingNotAllowedInLimitedMode = 3237093713, // statusCode 81 + ForwardRequestJPMFailed = 3237093714, // statusCode 82 + MaxSessionNumberLimitExceeded = 3237093715, // statusCode 83 + GuestModePartnerCapacityDisabled = 3237093716, // statusCode 84 + SessionRejectedNoCapacity = 3237093717, // statusCode 85 + SessionInsufficientPlayabilityLevel = 3237093718, // statusCode 86 + ForwardRequestLOFNFailed = 3237093719, // statusCode 87 + InvalidTransportRequest = 3237093720, // statusCode 88 + UserStorageNotAvailable = 3237093721, // statusCode 89 + GfnStorageNotAvailable = 3237093722, // statusCode 90 + SessionServerErrorEnd = 3237093887, + + // Session setup cancelled + SessionSetupCancelled = 15867905, + SessionSetupCancelledDuringQueuing = 15867906, + RequestCancelled = 15867907, + SystemSleepDuringSessionSetup = 15867909, + NoInternetDuringSessionSetup = 15868417, + + // Network errors (3237101xxx) + SocketError = 3237101580, + AddressResolveFailed = 3237101581, + ConnectFailed = 3237101582, + SslError = 3237101583, + ConnectionTimeout = 3237101584, + DataReceiveTimeout = 3237101585, + PeerNoResponse = 3237101586, + UnexpectedHttpRedirect = 3237101587, + DataSendFailure = 3237101588, + DataReceiveFailure = 3237101589, + CertificateRejected = 3237101590, + DataNotAllowed = 3237101591, + NetworkErrorUnknown = 3237101592, +} + +/** Error message entry with title and description */ +interface ErrorMessageEntry { + title: string; + description: string; +} + +/** User-friendly error messages map */ +export const ERROR_MESSAGES: Map = new Map([ + // Success + [15859712, { title: "Success", description: "Session started successfully." }], + + // Client errors + [ + 3237085186, + { + title: "Invalid Operation", + description: "The requested operation is not valid at this time.", + }, + ], + [ + 3237089282, + { + title: "Network Error", + description: "A network error occurred. Please check your internet connection.", + }, + ], + [ + 3237093377, + { + title: "Authentication Required", + description: "Your session has expired. Please log in again.", + }, + ], + [ + 3237093379, + { + title: "Server Response Error", + description: "Failed to parse server response. Please try again.", + }, + ], + [ + 3237093381, + { + title: "Invalid Server Response", + description: "The server returned an invalid response.", + }, + ], + [ + 3237093384, + { + title: "Session Error", + description: "An error occurred during session setup.", + }, + ], + [ + 3237093387, + { + title: "Authentication Timeout", + description: "Authentication token update timed out. Please log in again.", + }, + ], + + // Server errors + [ + 3237093634, + { + title: "Access Forbidden", + description: "Access to this service is forbidden.", + }, + ], + [ + 3237093635, + { + title: "Server Timeout", + description: "The server timed out. Please try again.", + }, + ], + [ + 3237093636, + { + title: "Server Error", + description: "An internal server error occurred. Please try again later.", + }, + ], + [ + 3237093637, + { + title: "Invalid Request", + description: "The request was invalid.", + }, + ], + [ + 3237093639, + { + title: "Too Many Sessions", + description: "You have too many active sessions. Please close some sessions and try again.", + }, + ], + [ + 3237093643, + { + title: "Session Limit Exceeded", + description: "You have reached your session limit. Another session may already be running on your account.", + }, + ], + [ + 3237093645, + { + title: "Session Time Exceeded", + description: "Your session time has been exceeded.", + }, + ], + [ + 3237093646, + { + title: "Authentication Failed", + description: "Authentication failed. Please log in again.", + }, + ], + [ + 3237093648, + { + title: "Session Expired", + description: "Your authentication has expired. Please log in again.", + }, + ], + [ + 3237093650, + { + title: "Entitlement Error", + description: "You don't have access to this game or service.", + }, + ], + [ + 3237093651, + { + title: "Game Not Available", + description: "This game is not currently available.", + }, + ], + [ + 3237093652, + { + title: "Game Not Found", + description: "This game was not found in the library.", + }, + ], + [ + 3237093655, + { + title: "EULA Required", + description: "You must accept the End User License Agreement to continue.", + }, + ], + [ + 3237093656, + { + title: "Under Maintenance", + description: "GeForce NOW is currently under maintenance. Please try again later.", + }, + ], + [ + 3237093657, + { + title: "Service Unavailable", + description: "The service is temporarily unavailable. Please try again later.", + }, + ], + [ + 3237093658, + { + title: "Steam Guard Required", + description: "Steam Guard authentication is required. Please complete Steam Guard verification.", + }, + ], + [ + 3237093659, + { + title: "Steam Login Required", + description: "You need to link your Steam account to play this game.", + }, + ], + [ + 3237093660, + { + title: "Steam Guard Invalid", + description: "Steam Guard code is invalid. Please try again.", + }, + ], + [ + 3237093661, + { + title: "Steam Profile Private", + description: "Your Steam profile is private. Please make it public or friends-only.", + }, + ], + [ + 3237093667, + { + title: "Email Not Verified", + description: "Please verify your email address to continue.", + }, + ], + [ + 3237093673, + { + title: "Game Updating", + description: "This game is currently being updated. Please try again later.", + }, + ], + [ + 3237093674, + { + title: "Game Not Found", + description: "This game was not found.", + }, + ], + [ + 3237093675, + { + title: "Insufficient Credits", + description: "You don't have enough credits for this session.", + }, + ], + [ + 3237093678, + { + title: "Session Taken Over", + description: "Your session was taken over by another device.", + }, + ], + [ + 3237093681, + { + title: "Session Expired", + description: "Your session has expired.", + }, + ], + [ + 3237093682, + { + title: "Device Limit Reached", + description: "You have reached the session limit for this device.", + }, + ], + [ + 3237093683, + { + title: "Region At Capacity", + description: "Your region is currently at capacity. Please try again later.", + }, + ], + [ + 3237093684, + { + title: "Region Not Supported", + description: "GeForce NOW is not available in your region.", + }, + ], + [ + 3237093685, + { + title: "Region Banned", + description: "GeForce NOW is not available in your region.", + }, + ], + [ + 3237093686, + { + title: "Free Tier On Hold", + description: "Free tier is temporarily unavailable in your region.", + }, + ], + [ + 3237093687, + { + title: "Paid Tier On Hold", + description: "Paid tier is temporarily unavailable in your region.", + }, + ], + [ + 3237093688, + { + title: "Game Maintenance", + description: "This game is currently under maintenance.", + }, + ], + [ + 3237093690, + { + title: "No Capacity", + description: "No gaming rigs are available right now. Please try again later or join the queue.", + }, + ], + [ + 3237093694, + { + title: "Queue Full", + description: "The queue is currently full. Please try again later.", + }, + ], + [ + 3237093695, + { + title: "Region Not Supported", + description: "Streaming is not supported in your region.", + }, + ], + [ + 3237093698, + { + title: "Game Not Available", + description: "This game is not available in your region.", + }, + ], + [ + 3237093701, + { + title: "Queue Abandoned", + description: "Your session in queue was abandoned.", + }, + ], + [ + 3237093702, + { + title: "Account Terminated", + description: "Your account has been terminated.", + }, + ], + [ + 3237093703, + { + title: "Queue Maintenance", + description: "The queue was cleared due to maintenance.", + }, + ], + [ + 3237093704, + { + title: "Zone Maintenance", + description: "This server zone is under maintenance.", + }, + ], + [ + 3237093711, + { + title: "Ads Timeout", + description: "Session expired while waiting for ads. Free tier users must watch ads to play. Please start a new session.", + }, + ], + [ + 3237093712, + { + title: "Ads Cancelled", + description: "Session cancelled because ads were skipped. Free tier users must watch ads to play.", + }, + ], + [ + 3237093713, + { + title: "Limited Mode", + description: "Streaming is not allowed in limited mode.", + }, + ], + [ + 3237093715, + { + title: "Session Limit", + description: "Maximum number of sessions reached.", + }, + ], + [ + 3237093717, + { + title: "No Capacity", + description: "No gaming rigs are available. Please try again later.", + }, + ], + [ + 3237093718, + { + title: "Playability Level Issue", + description: "Your account's playability level is insufficient. This may mean another session is already running, or there's a subscription issue.", + }, + ], + [ + 3237093721, + { + title: "Storage Unavailable", + description: "User storage is not available.", + }, + ], + [ + 3237093722, + { + title: "Storage Error", + description: "GFN storage is not available.", + }, + ], + + // Cancellation + [ + 15867905, + { + title: "Session Cancelled", + description: "Session setup was cancelled.", + }, + ], + [ + 15867906, + { + title: "Queue Cancelled", + description: "You left the queue.", + }, + ], + [ + 15867907, + { + title: "Request Cancelled", + description: "The request was cancelled.", + }, + ], + [ + 15867909, + { + title: "System Sleep", + description: "Session setup was interrupted by system sleep.", + }, + ], + [ + 15868417, + { + title: "No Internet", + description: "No internet connection during session setup.", + }, + ], + + // Network errors + [ + 3237101580, + { + title: "Socket Error", + description: "A socket error occurred. Please check your network.", + }, + ], + [ + 3237101581, + { + title: "DNS Error", + description: "Failed to resolve server address. Please check your network.", + }, + ], + [ + 3237101582, + { + title: "Connection Failed", + description: "Failed to connect to the server. Please check your network.", + }, + ], + [ + 3237101583, + { + title: "SSL Error", + description: "A secure connection error occurred.", + }, + ], + [ + 3237101584, + { + title: "Connection Timeout", + description: "Connection timed out. Please check your network.", + }, + ], + [ + 3237101585, + { + title: "Receive Timeout", + description: "Data receive timed out. Please check your network.", + }, + ], + [ + 3237101586, + { + title: "No Response", + description: "Server not responding. Please try again.", + }, + ], + [ + 3237101590, + { + title: "Certificate Error", + description: "Server certificate was rejected.", + }, + ], +]); + +/** Parsed error information from CloudMatch response */ +export interface SessionErrorInfo { + /** HTTP status code (e.g., 403) */ + httpStatus: number; + /** CloudMatch status code from requestStatus.statusCode */ + statusCode: number; + /** Status description from requestStatus.statusDescription */ + statusDescription?: string; + /** Unified error code from requestStatus.unifiedErrorCode */ + unifiedErrorCode?: number; + /** Session error code from session.errorCode */ + sessionErrorCode?: number; + /** Computed GFN error code */ + gfnErrorCode: number; + /** User-friendly title */ + title: string; + /** User-friendly description */ + description: string; +} + +/** CloudMatch error response structure */ +interface CloudMatchErrorResponse { + requestStatus?: { + statusCode?: number; + statusDescription?: string; + unifiedErrorCode?: number; + }; + session?: { + sessionId?: string; + errorCode?: number; + }; +} + +/** Session error class for parsing and handling CloudMatch errors */ +export class SessionError extends Error { + /** HTTP status code */ + public readonly httpStatus: number; + /** CloudMatch status code from requestStatus.statusCode */ + public readonly statusCode: number; + /** Status description from requestStatus.statusDescription */ + public readonly statusDescription?: string; + /** Unified error code from requestStatus.unifiedErrorCode */ + public readonly unifiedErrorCode?: number; + /** Session error code from session.errorCode */ + public readonly sessionErrorCode?: number; + /** Computed GFN error code */ + public readonly gfnErrorCode: number; + /** User-friendly title */ + public readonly title: string; + + constructor(info: SessionErrorInfo) { + super(info.description); + this.name = "SessionError"; + this.httpStatus = info.httpStatus; + this.statusCode = info.statusCode; + this.statusDescription = info.statusDescription; + this.unifiedErrorCode = info.unifiedErrorCode; + this.sessionErrorCode = info.sessionErrorCode; + this.gfnErrorCode = info.gfnErrorCode; + this.title = info.title; + } + + /** Get error type as a string (e.g., "SessionLimitExceeded") */ + get errorType(): string { + // Try to find the enum name from the error code + const entry = Object.entries(GfnErrorCode).find(([, value]) => value === this.gfnErrorCode); + if (entry) { + return entry[0]; + } + // Fallback to status code based naming + if (this.statusCode > 0) { + return `StatusCode${this.statusCode}`; + } + return "UnknownError"; + } + + /** Get user-friendly error message */ + get errorDescription(): string { + return this.message; + } + + /** + * Parse error from CloudMatch response JSON + */ + static fromResponse(httpStatus: number, responseBody: string): SessionError { + let json: CloudMatchErrorResponse = {}; + + try { + json = JSON.parse(responseBody) as CloudMatchErrorResponse; + } catch { + // Parsing failed, use empty object + } + + // Extract fields + const statusCode = json.requestStatus?.statusCode ?? 0; + const statusDescription = json.requestStatus?.statusDescription; + const unifiedErrorCode = json.requestStatus?.unifiedErrorCode; + const sessionErrorCode = json.session?.errorCode; + + // Compute GFN error code using official client logic + const gfnErrorCode = SessionError.computeErrorCode(statusCode, unifiedErrorCode); + + // Get user-friendly message + const { title, description } = SessionError.getErrorMessage( + gfnErrorCode, + statusDescription, + httpStatus, + ); + + return new SessionError({ + httpStatus, + statusCode, + statusDescription, + unifiedErrorCode, + sessionErrorCode, + gfnErrorCode, + title, + description, + }); + } + + /** + * Compute GFN error code from CloudMatch response (matching official client logic) + */ + private static computeErrorCode(statusCode: number, unifiedErrorCode?: number): number { + // Base error code + let errorCode: number = 3237093632; // SessionServerErrorBegin + + // Convert statusCode to error code + if (statusCode === 1) { + errorCode = 15859712; // Success + } else if (statusCode > 0 && statusCode < 255) { + errorCode = 3237093632 + statusCode; + } + + // Use unifiedErrorCode if available and error_code is generic + if (unifiedErrorCode !== undefined) { + switch (errorCode) { + case 3237093632: // SessionServerErrorBegin + case 3237093636: // ServerInternalError + case 3237093381: // InvalidServerResponse + errorCode = unifiedErrorCode; + break; + } + } + + return errorCode; + } + + /** + * Get user-friendly error message + */ + private static getErrorMessage( + errorCode: number, + statusDescription: string | undefined, + httpStatus: number, + ): { title: string; description: string } { + // Check for known error code + const knownError = ERROR_MESSAGES.get(errorCode); + if (knownError) { + return knownError; + } + + // Parse status description for known patterns + if (statusDescription) { + const descUpper = statusDescription.toUpperCase(); + + if (descUpper.includes("INSUFFICIENT_PLAYABILITY")) { + return { + title: "Session Already Active", + description: + "Another session is already running on your account. Please close it first or wait for it to timeout.", + }; + } + + if (descUpper.includes("SESSION_LIMIT")) { + return { + title: "Session Limit Exceeded", + description: "You have reached your maximum number of concurrent sessions.", + }; + } + + if (descUpper.includes("MAINTENANCE")) { + return { + title: "Under Maintenance", + description: "The service is currently under maintenance. Please try again later.", + }; + } + + if (descUpper.includes("CAPACITY") || descUpper.includes("QUEUE")) { + return { + title: "No Capacity Available", + description: "All gaming rigs are currently in use. Please try again later.", + }; + } + + if (descUpper.includes("AUTH") || descUpper.includes("TOKEN")) { + return { + title: "Authentication Error", + description: "Please log in again.", + }; + } + + if (descUpper.includes("ENTITLEMENT")) { + return { + title: "Access Denied", + description: "You don't have access to this game or service.", + }; + } + } + + // Fallback based on HTTP status + switch (httpStatus) { + case 401: + return { + title: "Unauthorized", + description: "Please log in again.", + }; + case 403: + return { + title: "Access Denied", + description: "Access to this resource was denied.", + }; + case 404: + return { + title: "Not Found", + description: "The requested resource was not found.", + }; + case 429: + return { + title: "Too Many Requests", + description: "Please wait a moment and try again.", + }; + } + + if (httpStatus >= 500 && httpStatus < 600) { + return { + title: "Server Error", + description: "A server error occurred. Please try again later.", + }; + } + + return { + title: "Error", + description: `An error occurred (HTTP ${httpStatus}).`, + }; + } + + /** + * Check if this error indicates another session is running + */ + isSessionConflict(): boolean { + const sessionConflictCodes = [ + GfnErrorCode.SessionLimitExceeded, // 3237093643 + GfnErrorCode.SessionLimitPerDeviceReached, // 3237093682 + GfnErrorCode.MaxSessionNumberLimitExceeded, // 3237093715 + GfnErrorCode.SessionInsufficientPlayabilityLevel, // 3237093718 + ]; + + if (sessionConflictCodes.includes(this.gfnErrorCode)) { + return true; + } + + if (this.statusDescription?.toUpperCase().includes("INSUFFICIENT_PLAYABILITY")) { + return true; + } + + return false; + } + + /** + * Check if this is a temporary error that might resolve with retry + */ + isRetryable(): boolean { + const retryableCodes = [ + GfnErrorCode.NetworkError, // 3237089282 + GfnErrorCode.ServerInternalTimeout, // 3237093635 + GfnErrorCode.ServerInternalError, // 3237093636 + GfnErrorCode.ForwardingZoneOutOfCapacity, // 3237093683 + GfnErrorCode.InsufficientVmCapacity, // 3237093690 + GfnErrorCode.SessionRejectedNoCapacity, // 3237093717 + GfnErrorCode.ConnectionTimeout, // 3237101584 + GfnErrorCode.DataReceiveTimeout, // 3237101585 + GfnErrorCode.PeerNoResponse, // 3237101586 + ]; + + return retryableCodes.includes(this.gfnErrorCode); + } + + /** + * Check if user needs to log in again + */ + needsReauth(): boolean { + const reauthCodes = [ + GfnErrorCode.AuthTokenNotUpdated, // 3237093377 + GfnErrorCode.AuthTokenUpdateTimeout, // 3237093387 + GfnErrorCode.AuthFailure, // 3237093646 + GfnErrorCode.InvalidAuthenticationMalformed, // 3237093647 + GfnErrorCode.InvalidAuthenticationExpired, // 3237093648 + GfnErrorCode.InvalidAuthenticationNotFound, // 3237093649 + GfnErrorCode.InvalidAuthenticationUnsupportedProtocol, // 3237093668 + GfnErrorCode.InvalidAuthenticationUnknownToken, // 3237093669 + GfnErrorCode.InvalidAuthenticationCredentials, // 3237093670 + ]; + + if (reauthCodes.includes(this.gfnErrorCode)) { + return true; + } + + if (this.httpStatus === 401) { + return true; + } + + return false; + } + + /** + * Convert to a plain object for serialization + */ + toJSON(): SessionErrorInfo { + return { + httpStatus: this.httpStatus, + statusCode: this.statusCode, + statusDescription: this.statusDescription, + unifiedErrorCode: this.unifiedErrorCode, + sessionErrorCode: this.sessionErrorCode, + gfnErrorCode: this.gfnErrorCode, + title: this.title, + description: this.message, + }; + } +} + +/** Helper function to check if an error is a SessionError */ +export function isSessionError(error: unknown): error is SessionError { + return error instanceof SessionError; +} + +/** Helper function to parse error from CloudMatch response */ +export function parseCloudMatchError(httpStatus: number, responseBody: string): SessionError { + return SessionError.fromResponse(httpStatus, responseBody); +} diff --git a/opennow-stable/src/main/gfn/games.ts b/opennow-stable/src/main/gfn/games.ts new file mode 100644 index 0000000..4a75322 --- /dev/null +++ b/opennow-stable/src/main/gfn/games.ts @@ -0,0 +1,367 @@ +import type { GameInfo, GameVariant } from "@shared/gfn"; + +const GRAPHQL_URL = "https://games.geforce.com/graphql"; +const PANELS_QUERY_HASH = "f8e26265a5db5c20e1334a6872cf04b6e3970507697f6ae55a6ddefa5420daf0"; +const APP_METADATA_QUERY_HASH = "39187e85b6dcf60b7279a5f233288b0a8b69a8b1dbcfb5b25555afdcb988f0d7"; +const DEFAULT_LOCALE = "en_US"; +const LCARS_CLIENT_ID = "ec7e38d4-03af-4b58-b131-cfb0495903ab"; +const GFN_CLIENT_VERSION = "2.0.80.173"; + +const GFN_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 NVIDIACEFClient/HEAD/debb5919f6 GFN-PC/2.0.80.173"; + +interface GraphQlResponse { + data?: { + panels: Array<{ + name: string; + sections: Array<{ + items: Array<{ + __typename: string; + app?: AppData; + }>; + }>; + }>; + }; + errors?: Array<{ message: string }>; +} + +interface AppMetaDataResponse { + data?: { + apps: { + items: AppData[]; + }; + }; + errors?: Array<{ message: string }>; +} + +interface AppData { + id: string; + title: string; + description?: string; + longDescription?: string; + images?: { + GAME_BOX_ART?: string; + TV_BANNER?: string; + HERO_IMAGE?: string; + }; + variants?: Array<{ + id: string; + appStore: string; + supportedControls?: string[]; + gfn?: { + library?: { + selected?: boolean; + }; + }; + }>; + gfn?: { + playType?: string; + minimumMembershipTierLabel?: string; + }; +} + +interface ServerInfoResponse { + requestStatus?: { + serverId?: string; + }; +} + +interface RawPublicGame { + id?: string | number; + title?: string; + steamUrl?: string; + status?: string; +} + +function optimizeImage(url: string): string { + if (url.includes("img.nvidiagrid.net")) { + return `${url};f=webp;w=272`; + } + return url; +} + +function isNumericId(value: string | undefined): value is string { + if (!value) { + return false; + } + return /^\d+$/.test(value); +} + +function randomHuId(): string { + return `${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`; +} + +async function getVpcId(token: string, providerStreamingBaseUrl?: string): Promise { + const base = providerStreamingBaseUrl?.trim() || "https://prod.cloudmatchbeta.nvidiagrid.net/"; + const normalizedBase = base.endsWith("/") ? base : `${base}/`; + + const response = await fetch(`${normalizedBase}v2/serverInfo`, { + headers: { + Accept: "application/json", + Authorization: `GFNJWT ${token}`, + "nv-client-id": LCARS_CLIENT_ID, + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + "User-Agent": GFN_USER_AGENT, + }, + }); + + if (!response.ok) { + return "GFN-PC"; + } + + const payload = (await response.json()) as ServerInfoResponse; + return payload.requestStatus?.serverId ?? "GFN-PC"; +} + +function appToGame(app: AppData): GameInfo { + const variants: GameVariant[] = + app.variants?.map((variant) => ({ + id: variant.id, + store: variant.appStore, + supportedControls: variant.supportedControls ?? [], + })) ?? []; + + const selectedVariantIndex = + app.variants?.findIndex((variant) => variant.gfn?.library?.selected === true) ?? 0; + + const safeIndex = Math.max(0, selectedVariantIndex); + const selectedVariant = variants[safeIndex]; + const selectedVariantId = selectedVariant?.id; + const fallbackNumericVariantId = variants.find((variant) => isNumericId(variant.id))?.id; + const launchAppId = isNumericId(selectedVariantId) + ? selectedVariantId + : fallbackNumericVariantId ?? (isNumericId(app.id) ? app.id : undefined); + + const id = `${app.id}:${selectedVariantId ?? "default"}`; + const imageUrl = + app.images?.GAME_BOX_ART ?? app.images?.TV_BANNER ?? app.images?.HERO_IMAGE ?? undefined; + + return { + id, + uuid: app.id, + launchAppId, + title: app.title, + description: app.description ?? app.longDescription, + imageUrl: imageUrl ? optimizeImage(imageUrl) : undefined, + playType: app.gfn?.playType, + membershipTierLabel: app.gfn?.minimumMembershipTierLabel, + selectedVariantIndex: Math.max(0, selectedVariantIndex), + variants, + }; +} + +async function fetchAppMetaData( + token: string, + appIdOrUuid: string, + vpcId: string, +): Promise { + const variables = JSON.stringify({ + vpcId, + locale: DEFAULT_LOCALE, + appIds: [appIdOrUuid], + }); + + const extensions = JSON.stringify({ + persistedQuery: { + sha256Hash: APP_METADATA_QUERY_HASH, + }, + }); + + const params = new URLSearchParams({ + requestType: "appMetaData", + extensions, + huId: randomHuId(), + variables, + }); + + const response = await fetch(`${GRAPHQL_URL}?${params.toString()}`, { + headers: { + Accept: "application/json, text/plain, */*", + "Content-Type": "application/graphql", + Origin: "https://play.geforcenow.com", + Referer: "https://play.geforcenow.com/", + Authorization: `GFNJWT ${token}`, + "nv-client-id": LCARS_CLIENT_ID, + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + "nv-device-make": "UNKNOWN", + "nv-device-model": "UNKNOWN", + "nv-browser-type": "CHROME", + "User-Agent": GFN_USER_AGENT, + }, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`App metadata failed (${response.status}): ${text.slice(0, 400)}`); + } + + return (await response.json()) as AppMetaDataResponse; +} + +async function fetchPanels( + token: string, + panelNames: string[], + vpcId: string, +): Promise { + const variables = JSON.stringify({ + vpcId, + locale: DEFAULT_LOCALE, + panelNames, + }); + + const extensions = JSON.stringify({ + persistedQuery: { + sha256Hash: PANELS_QUERY_HASH, + }, + }); + + const requestType = panelNames.includes("LIBRARY") ? "panels/Library" : "panels/MainV2"; + const params = new URLSearchParams({ + requestType, + extensions, + huId: randomHuId(), + variables, + }); + + const response = await fetch(`${GRAPHQL_URL}?${params.toString()}`, { + headers: { + Accept: "application/json, text/plain, */*", + "Content-Type": "application/graphql", + Origin: "https://play.geforcenow.com", + Referer: "https://play.geforcenow.com/", + Authorization: `GFNJWT ${token}`, + "nv-client-id": LCARS_CLIENT_ID, + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + "nv-device-make": "UNKNOWN", + "nv-device-model": "UNKNOWN", + "nv-browser-type": "CHROME", + "User-Agent": GFN_USER_AGENT, + }, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Games GraphQL failed (${response.status}): ${text.slice(0, 400)}`); + } + + return (await response.json()) as GraphQlResponse; +} + +function flattenPanels(payload: GraphQlResponse): GameInfo[] { + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const games: GameInfo[] = []; + + for (const panel of payload.data?.panels ?? []) { + for (const section of panel.sections ?? []) { + for (const item of section.items ?? []) { + if (item.__typename === "GameItem" && item.app) { + games.push(appToGame(item.app)); + } + } + } + } + + return games; +} + +export async function fetchMainGames(token: string, providerStreamingBaseUrl?: string): Promise { + const vpcId = await getVpcId(token, providerStreamingBaseUrl); + const payload = await fetchPanels(token, ["MAIN"], vpcId); + return flattenPanels(payload); +} + +export async function fetchLibraryGames( + token: string, + providerStreamingBaseUrl?: string, +): Promise { + const vpcId = await getVpcId(token, providerStreamingBaseUrl); + const payload = await fetchPanels(token, ["LIBRARY"], vpcId); + return flattenPanels(payload); +} + +export async function fetchPublicGames(): Promise { + const response = await fetch( + "https://static.nvidiagrid.net/supported-public-game-list/locales/gfnpc-en-US.json", + { + headers: { + "User-Agent": GFN_USER_AGENT, + }, + }, + ); + + if (!response.ok) { + throw new Error(`Public games fetch failed (${response.status})`); + } + + const payload = (await response.json()) as RawPublicGame[]; + return payload + .filter((item) => item.status === "AVAILABLE" && item.title) + .map((item) => { + const id = String(item.id ?? item.title ?? "unknown"); + const steamAppId = item.steamUrl?.split("/app/")[1]?.split("/")[0]; + const imageUrl = steamAppId + ? `https://cdn.cloudflare.steamstatic.com/steam/apps/${steamAppId}/library_600x900.jpg` + : undefined; + + return { + id, + uuid: id, + launchAppId: isNumericId(id) ? id : undefined, + title: item.title ?? id, + selectedVariantIndex: 0, + variants: [{ id, store: "Unknown", supportedControls: [] }], + imageUrl, + } as GameInfo; + }); +} + +export async function resolveLaunchAppId( + token: string, + appIdOrUuid: string, + providerStreamingBaseUrl?: string, +): Promise { + if (isNumericId(appIdOrUuid)) { + return appIdOrUuid; + } + + const vpcId = await getVpcId(token, providerStreamingBaseUrl); + const payload = await fetchAppMetaData(token, appIdOrUuid, vpcId); + + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const app = payload.data?.apps.items?.[0]; + if (!app) { + return null; + } + + const variants = app.variants ?? []; + const selected = variants.find((variant) => variant.gfn?.library?.selected === true); + + if (isNumericId(selected?.id)) { + return selected.id; + } + + const firstNumeric = variants.find((variant) => isNumericId(variant.id)); + if (firstNumeric) { + return firstNumeric.id; + } + + return isNumericId(app.id) ? app.id : null; +} diff --git a/opennow-stable/src/main/gfn/signaling.ts b/opennow-stable/src/main/gfn/signaling.ts new file mode 100644 index 0000000..c15e2c4 --- /dev/null +++ b/opennow-stable/src/main/gfn/signaling.ts @@ -0,0 +1,281 @@ +import { randomBytes } from "node:crypto"; + +import WebSocket from "ws"; + +import type { + IceCandidatePayload, + MainToRendererSignalingEvent, + SendAnswerRequest, +} from "@shared/gfn"; + +const USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36"; + +interface SignalingMessage { + ackid?: number; + ack?: number; + hb?: number; + peer_info?: { + id: number; + }; + peer_msg?: { + from: number; + to: number; + msg: string; + }; +} + +export class GfnSignalingClient { + private ws: WebSocket | null = null; + private peerId = 2; + private peerName = `peer-${Math.floor(Math.random() * 10_000_000_000)}`; + private ackCounter = 0; + private heartbeatTimer: NodeJS.Timeout | null = null; + private listeners = new Set<(event: MainToRendererSignalingEvent) => void>(); + + constructor( + private readonly signalingServer: string, + private readonly sessionId: string, + private readonly signalingUrl?: string, + ) {} + + private buildSignInUrl(): string { + // Match Rust behavior: extract host:port from signalingUrl if available, + // since the signalingUrl contains the real server address (which may differ + // from signalingServer when the resource path was an rtsps:// URL) + let serverWithPort: string; + + if (this.signalingUrl) { + // Extract host:port from wss://host:port/path + const withoutScheme = this.signalingUrl.replace(/^wss?:\/\//, ""); + const hostPort = withoutScheme.split("/")[0]; + serverWithPort = hostPort && hostPort.length > 0 + ? (hostPort.includes(":") ? hostPort : `${hostPort}:443`) + : (this.signalingServer.includes(":") ? this.signalingServer : `${this.signalingServer}:443`); + } else { + serverWithPort = this.signalingServer.includes(":") + ? this.signalingServer + : `${this.signalingServer}:443`; + } + + const url = `wss://${serverWithPort}/nvst/sign_in?peer_id=${this.peerName}&version=2`; + console.log("[Signaling] URL:", url, "(server:", this.signalingServer, ", signalingUrl:", this.signalingUrl, ")"); + return url; + } + + onEvent(listener: (event: MainToRendererSignalingEvent) => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + private emit(event: MainToRendererSignalingEvent): void { + for (const listener of this.listeners) { + listener(event); + } + } + + private nextAckId(): number { + this.ackCounter += 1; + return this.ackCounter; + } + + private sendJson(payload: unknown): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + return; + } + this.ws.send(JSON.stringify(payload)); + } + + private setupHeartbeat(): void { + this.clearHeartbeat(); + this.heartbeatTimer = setInterval(() => { + this.sendJson({ hb: 1 }); + }, 5000); + } + + private clearHeartbeat(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + } + + private sendPeerInfo(): void { + this.sendJson({ + ackid: this.nextAckId(), + peer_info: { + browser: "Chrome", + browserVersion: "131", + connected: true, + id: this.peerId, + name: this.peerName, + peerRole: 0, + resolution: "1920x1080", + version: 2, + }, + }); + } + + async connect(): Promise { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + return; + } + + const url = this.buildSignInUrl(); + const protocol = `x-nv-sessionid.${this.sessionId}`; + + console.log("[Signaling] Connecting to:", url); + console.log("[Signaling] Session ID:", this.sessionId); + console.log("[Signaling] Protocol:", protocol); + + await new Promise((resolve, reject) => { + // Extract host:port for the Host header (matching Rust behavior) + const urlHost = url.replace(/^wss?:\/\//, "").split("/")[0]; + + const ws = new WebSocket(url, protocol, { + rejectUnauthorized: false, + headers: { + Host: urlHost, + Origin: "https://play.geforcenow.com", + "User-Agent": USER_AGENT, + "Sec-WebSocket-Key": randomBytes(16).toString("base64"), + }, + }); + + this.ws = ws; + + ws.once("error", (error) => { + this.emit({ type: "error", message: `Signaling connect failed: ${String(error)}` }); + reject(error); + }); + + ws.once("open", () => { + this.sendPeerInfo(); + this.setupHeartbeat(); + this.emit({ type: "connected" }); + resolve(); + }); + + ws.on("message", (raw) => { + const text = typeof raw === "string" ? raw : raw.toString("utf8"); + this.handleMessage(text); + }); + + ws.on("close", (_code, reason) => { + this.clearHeartbeat(); + const reasonText = typeof reason === "string" ? reason : reason.toString("utf8"); + this.emit({ type: "disconnected", reason: reasonText || "socket closed" }); + }); + }); + } + + private handleMessage(text: string): void { + let parsed: SignalingMessage; + try { + parsed = JSON.parse(text) as SignalingMessage; + } catch { + this.emit({ type: "log", message: `Ignoring non-JSON signaling packet: ${text.slice(0, 120)}` }); + return; + } + + if (typeof parsed.ackid === "number") { + const shouldAck = parsed.peer_info?.id !== this.peerId; + if (shouldAck) { + this.sendJson({ ack: parsed.ackid }); + } + } + + if (parsed.hb) { + this.sendJson({ hb: 1 }); + return; + } + + if (!parsed.peer_msg?.msg) { + return; + } + + let peerPayload: Record; + try { + peerPayload = JSON.parse(parsed.peer_msg.msg) as Record; + } catch { + this.emit({ type: "log", message: "Received non-JSON peer payload" }); + return; + } + + if (peerPayload.type === "offer" && typeof peerPayload.sdp === "string") { + console.log(`[Signaling] Received OFFER SDP (${peerPayload.sdp.length} chars), first 500 chars:`); + console.log(peerPayload.sdp.slice(0, 500)); + this.emit({ type: "offer", sdp: peerPayload.sdp }); + return; + } + + if (typeof peerPayload.candidate === "string") { + console.log(`[Signaling] Received remote ICE candidate: ${peerPayload.candidate}`); + this.emit({ + type: "remote-ice", + candidate: { + candidate: peerPayload.candidate, + sdpMid: + typeof peerPayload.sdpMid === "string" || peerPayload.sdpMid === null + ? peerPayload.sdpMid + : undefined, + sdpMLineIndex: + typeof peerPayload.sdpMLineIndex === "number" || peerPayload.sdpMLineIndex === null + ? peerPayload.sdpMLineIndex + : undefined, + }, + }); + return; + } + + // Log any unhandled peer message types for debugging + console.log("[Signaling] Unhandled peer message keys:", Object.keys(peerPayload)); + } + + async sendAnswer(payload: SendAnswerRequest): Promise { + console.log(`[Signaling] Sending ANSWER SDP (${payload.sdp.length} chars), first 500 chars:`); + console.log(payload.sdp.slice(0, 500)); + if (payload.nvstSdp) { + console.log(`[Signaling] Sending nvstSdp (${payload.nvstSdp.length} chars):`); + console.log(payload.nvstSdp); + } + const answer = { + type: "answer", + sdp: payload.sdp, + ...(payload.nvstSdp ? { nvstSdp: payload.nvstSdp } : {}), + }; + + this.sendJson({ + peer_msg: { + from: this.peerId, + to: 1, + msg: JSON.stringify(answer), + }, + ackid: this.nextAckId(), + }); + } + + async sendIceCandidate(candidate: IceCandidatePayload): Promise { + console.log(`[Signaling] Sending local ICE candidate: ${candidate.candidate} (sdpMid=${candidate.sdpMid})`); + this.sendJson({ + peer_msg: { + from: this.peerId, + to: 1, + msg: JSON.stringify({ + candidate: candidate.candidate, + sdpMid: candidate.sdpMid, + sdpMLineIndex: candidate.sdpMLineIndex, + }), + }, + ackid: this.nextAckId(), + }); + } + + disconnect(): void { + this.clearHeartbeat(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } +} diff --git a/opennow-stable/src/main/gfn/subscription.ts b/opennow-stable/src/main/gfn/subscription.ts new file mode 100644 index 0000000..601eb87 --- /dev/null +++ b/opennow-stable/src/main/gfn/subscription.ts @@ -0,0 +1,304 @@ +/** + * MES (Membership/Subscription) API integration for GeForce NOW + * Handles fetching subscription info from the MES API endpoint. + */ + +import type { + SubscriptionInfo, + EntitledResolution, + StorageAddon, + StreamRegion, +} from "@shared/gfn"; + +/** MES API endpoint URL */ +const MES_URL = "https://mes.geforcenow.com/v4/subscriptions"; + +/** LCARS Client ID */ +const LCARS_CLIENT_ID = "ec7e38d4-03af-4b58-b131-cfb0495903ab"; + +/** GFN client version */ +const GFN_CLIENT_VERSION = "2.0.80.173"; + +interface SubscriptionResponse { + firstEntitlementStartDateTime?: string; + type?: string; + membershipTier?: string; + allottedTimeInMinutes?: number; + purchasedTimeInMinutes?: number; + rolledOverTimeInMinutes?: number; + remainingTimeInMinutes?: number; + totalTimeInMinutes?: number; + notifications?: { + notifyUserWhenTimeRemainingInMinutes?: number; + notifyUserOnSessionWhenRemainingTimeInMinutes?: number; + }; + currentSpanStartDateTime?: string; + currentSpanEndDateTime?: string; + currentSubscriptionState?: { + state?: string; + isGamePlayAllowed?: boolean; + }; + subType?: string; + addons?: SubscriptionAddonResponse[]; + features?: SubscriptionFeatures; +} + +interface SubscriptionFeatures { + resolutions?: SubscriptionResolution[]; +} + +interface SubscriptionResolution { + heightInPixels: number; + widthInPixels: number; + framesPerSecond: number; + isEntitled: boolean; +} + +interface SubscriptionAddonResponse { + type?: string; + subType?: string; + status?: string; + attributes?: AddonAttribute[]; +} + +interface AddonAttribute { + key?: string; + textValue?: string; +} + +function parseMinutes(value: unknown): number | undefined { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + return undefined; +} + +function parseNumberText(value: unknown): number | undefined { + if (typeof value !== "string" || value.trim().length === 0) { + return undefined; + } + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return undefined; + } + return parsed; +} + +function parseIsoDate(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +/** + * Fetch subscription info from MES API + * @param token - The authentication token + * @param userId - The user ID + * @param vpcId - The VPC ID (defaults to a common European VPC if not provided) + * @returns The subscription info + */ +export async function fetchSubscription( + token: string, + userId: string, + vpcId = "NP-AMS-08", +): Promise { + const url = new URL(MES_URL); + url.searchParams.append("serviceName", "gfn_pc"); + url.searchParams.append("languageCode", "en_US"); + url.searchParams.append("vpcId", vpcId); + url.searchParams.append("userId", userId); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `GFNJWT ${token}`, + Accept: "application/json", + "nv-client-id": LCARS_CLIENT_ID, + "nv-client-type": "NATIVE", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-client-streamer": "NVIDIA-CLASSIC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + }, + }); + + if (!response.ok) { + const body = await response.text(); + throw new Error(`Subscription API failed with status ${response.status}: ${body}`); + } + + const data = (await response.json()) as SubscriptionResponse; + + // Parse membership tier (defaults to FREE) + const membershipTier = data.membershipTier ?? "FREE"; + + // Convert minutes to hours. Use the additive fields as fallback if total is absent. + const allottedMinutes = parseMinutes(data.allottedTimeInMinutes) ?? 0; + const purchasedMinutes = parseMinutes(data.purchasedTimeInMinutes) ?? 0; + const rolledOverMinutes = parseMinutes(data.rolledOverTimeInMinutes) ?? 0; + const fallbackTotalMinutes = allottedMinutes + purchasedMinutes + rolledOverMinutes; + const totalMinutes = parseMinutes(data.totalTimeInMinutes) ?? fallbackTotalMinutes; + const remainingMinutes = parseMinutes(data.remainingTimeInMinutes) ?? 0; + const usedMinutes = Math.max(totalMinutes - remainingMinutes, 0); + + const allottedHours = allottedMinutes / 60; + const purchasedHours = purchasedMinutes / 60; + const rolledOverHours = rolledOverMinutes / 60; + const usedHours = usedMinutes / 60; + const remainingHours = remainingMinutes / 60; + const totalHours = totalMinutes / 60; + + // Check if unlimited subscription + const isUnlimited = data.subType === "UNLIMITED"; + + // Parse storage addon + let storageAddon: StorageAddon | undefined; + const storageAddonResponse = data.addons?.find( + (addon) => + addon.type === "STORAGE" && + addon.subType === "PERMANENT_STORAGE" && + addon.status === "OK", + ); + + if (storageAddonResponse) { + const sizeAttr = storageAddonResponse.attributes?.find( + (attr) => attr.key === "TOTAL_STORAGE_SIZE_IN_GB", + ); + const usedAttr = storageAddonResponse.attributes?.find( + (attr) => attr.key === "USED_STORAGE_SIZE_IN_GB", + ); + const regionNameAttr = storageAddonResponse.attributes?.find( + (attr) => attr.key === "STORAGE_METRO_REGION_NAME", + ); + const regionCodeAttr = storageAddonResponse.attributes?.find( + (attr) => attr.key === "STORAGE_METRO_REGION", + ); + const sizeGb = parseNumberText(sizeAttr?.textValue); + const usedGb = parseNumberText(usedAttr?.textValue); + const regionName = regionNameAttr?.textValue; + const regionCode = regionCodeAttr?.textValue; + + storageAddon = { + type: "PERMANENT_STORAGE", + sizeGb, + usedGb, + regionName, + regionCode, + }; + } + + // Parse entitled resolutions + const entitledResolutions: EntitledResolution[] = []; + if (data.features?.resolutions) { + for (const res of data.features.resolutions) { + // Include all resolutions (matching Rust implementation behavior) + entitledResolutions.push({ + width: res.widthInPixels, + height: res.heightInPixels, + fps: res.framesPerSecond, + }); + } + + // Sort by highest resolution/fps first + entitledResolutions.sort((a, b) => { + if (b.width !== a.width) return b.width - a.width; + if (b.height !== a.height) return b.height - a.height; + return b.fps - a.fps; + }); + } + + return { + membershipTier, + subscriptionType: data.type, + subscriptionSubType: data.subType, + allottedHours, + purchasedHours, + rolledOverHours, + usedHours, + remainingHours, + totalHours, + firstEntitlementStartDateTime: parseIsoDate(data.firstEntitlementStartDateTime), + serverRegionId: vpcId, + currentSpanStartDateTime: parseIsoDate(data.currentSpanStartDateTime), + currentSpanEndDateTime: parseIsoDate(data.currentSpanEndDateTime), + notifyUserWhenTimeRemainingInMinutes: parseMinutes( + data.notifications?.notifyUserWhenTimeRemainingInMinutes, + ), + notifyUserOnSessionWhenRemainingTimeInMinutes: parseMinutes( + data.notifications?.notifyUserOnSessionWhenRemainingTimeInMinutes, + ), + state: data.currentSubscriptionState?.state, + isGamePlayAllowed: data.currentSubscriptionState?.isGamePlayAllowed, + isUnlimited, + storageAddon, + entitledResolutions, + }; +} + +/** + * Fetch dynamic regions from serverInfo endpoint to get VPC ID + * @param token - Optional authentication token + * @param streamingBaseUrl - Base URL for the streaming service + * @returns Array of stream regions and the discovered VPC ID + */ +export async function fetchDynamicRegions( + token: string | undefined, + streamingBaseUrl: string, +): Promise<{ regions: StreamRegion[]; vpcId: string | null }> { + const base = streamingBaseUrl.endsWith("/") + ? streamingBaseUrl + : `${streamingBaseUrl}/`; + const url = `${base}v2/serverInfo`; + + const headers: Record = { + Accept: "application/json", + "nv-client-id": LCARS_CLIENT_ID, + "nv-client-type": "BROWSER", + "nv-client-version": GFN_CLIENT_VERSION, + "nv-client-streamer": "WEBRTC", + "nv-device-os": "WINDOWS", + "nv-device-type": "DESKTOP", + }; + + if (token) { + headers.Authorization = `GFNJWT ${token}`; + } + + let response: Response; + try { + response = await fetch(url, { headers }); + } catch { + return { regions: [], vpcId: null }; + } + + if (!response.ok) { + return { regions: [], vpcId: null }; + } + + const data = (await response.json()) as { + requestStatus?: { serverId?: string }; + metaData?: Array<{ key: string; value: string }>; + }; + + // Extract VPC ID + const vpcId = data.requestStatus?.serverId ?? null; + + // Extract regions + const regions = (data.metaData ?? []) + .filter( + (entry) => + entry.value.startsWith("https://") && + entry.key !== "gfn-regions" && + !entry.key.startsWith("gfn-"), + ) + .map((entry) => ({ + name: entry.key, + url: entry.value.endsWith("/") ? entry.value : `${entry.value}/`, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + return { regions, vpcId }; +} diff --git a/opennow-stable/src/main/gfn/types.ts b/opennow-stable/src/main/gfn/types.ts new file mode 100644 index 0000000..eb0c795 --- /dev/null +++ b/opennow-stable/src/main/gfn/types.ts @@ -0,0 +1,194 @@ +import type { SessionError, SessionErrorInfo } from "./errorCodes"; + +export interface CloudMatchRequest { + sessionRequestData: { + appId: string; + internalTitle: string | null; + availableSupportedControllers: number[]; + networkTestSessionId: string | null; + parentSessionId: string | null; + clientIdentification: string; + deviceHashId: string; + clientVersion: string; + sdkVersion: string; + streamerVersion: number; + clientPlatformName: string; + clientRequestMonitorSettings: Array<{ + widthInPixels: number; + heightInPixels: number; + framesPerSecond: number; + sdrHdrMode: number; + displayData: { + desiredContentMaxLuminance: number; + desiredContentMinLuminance: number; + desiredContentMaxFrameAverageLuminance: number; + }; + dpi: number; + }>; + useOps: boolean; + audioMode: number; + metaData: Array<{ key: string; value: string }>; + sdrHdrMode: number; + clientDisplayHdrCapabilities: { + version: number; + hdrEdrSupportedFlagsInUint32: number; + staticMetadataDescriptorId: number; + } | null; + surroundAudioInfo: number; + remoteControllersBitmap: number; + clientTimezoneOffset: number; + enhancedStreamMode: number; + appLaunchMode: number; + secureRTSPSupported: boolean; + partnerCustomData: string; + accountLinked: boolean; + enablePersistingInGameSettings: boolean; + userAge: number; + requestedStreamingFeatures: { + reflex: boolean; + bitDepth: number; + cloudGsync: boolean; + enabledL4S: boolean; + mouseMovementFlags: number; + trueHdr: boolean; + supportedHidDevices: number; + profile: number; + fallbackToLogicalResolution: boolean; + hidDevices: string | null; + chromaFormat: number; + prefilterMode: number; + prefilterSharpness: number; + prefilterNoiseReduction: number; + hudStreamingMode: number; + sdrColorSpace: number; + hdrColorSpace: number; + }; + }; +} + +export interface CloudMatchResponse { + requestStatus: { + statusCode: number; + statusDescription?: string; + unifiedErrorCode?: number; + }; + session: { + sessionId: string; + status: number; + errorCode?: number; + gpuType?: string; + connectionInfo?: Array<{ + ip?: string; + port: number; + usage: number; + protocol?: number; + resourcePath?: string; + }>; + sessionControlInfo?: { + ip?: string; + }; + iceServerConfiguration?: { + iceServers?: Array<{ + urls: string[] | string; + username?: string; + credential?: string; + }>; + }; + }; +} + +/** Session in the get sessions response */ +export interface SessionEntry { + sessionId: string; + status: number; + gpuType?: string; + sessionRequestData?: { + appId?: string; + [key: string]: unknown; + }; + sessionControlInfo?: { + ip?: string; + }; + connectionInfo?: Array<{ + ip?: string; + port: number; + usage: number; + protocol?: number; + }>; + monitorSettings?: Array<{ + widthInPixels?: number; + heightInPixels?: number; + framesPerSecond?: number; + }>; +} + +/** Response from GET /v2/session (list of sessions) */ +export interface GetSessionsResponse { + requestStatus: { + statusCode: number; + statusDescription?: string; + unifiedErrorCode?: number; + }; + sessions: SessionEntry[]; +} + +// Re-export error types for convenience +export type { SessionError, SessionErrorInfo }; + +/** Result type for CloudMatch operations that may fail with a SessionError */ +export type CloudMatchResult = + | { success: true; data: T } + | { success: false; error: SessionError }; + +/** Error response structure from CloudMatch API */ +export interface CloudMatchErrorResponse { + requestStatus: { + statusCode: number; + statusDescription?: string; + unifiedErrorCode?: number; + }; + session?: { + sessionId?: string; + errorCode?: number; + }; +} + +/** Entitled resolution from subscription features */ +export interface EntitledResolution { + width: number; + height: number; + fps: number; +} + +/** Storage addon info */ +export interface StorageAddon { + type: "PERMANENT_STORAGE"; + sizeGb?: number; + usedGb?: number; + regionName?: string; + regionCode?: string; +} + +/** Subscription info from MES API */ +export interface SubscriptionInfo { + membershipTier: string; + subscriptionType?: string; + subscriptionSubType?: string; + allottedHours: number; + purchasedHours: number; + rolledOverHours: number; + usedHours: number; + remainingHours: number; + totalHours: number; + firstEntitlementStartDateTime?: string; + serverRegionId?: string; + currentSpanStartDateTime?: string; + currentSpanEndDateTime?: string; + notifyUserWhenTimeRemainingInMinutes?: number; + notifyUserOnSessionWhenRemainingTimeInMinutes?: number; + state?: string; + isGamePlayAllowed?: boolean; + isUnlimited: boolean; + storageAddon?: StorageAddon; + entitledResolutions: EntitledResolution[]; +} diff --git a/opennow-stable/src/main/index.ts b/opennow-stable/src/main/index.ts new file mode 100644 index 0000000..f152e62 --- /dev/null +++ b/opennow-stable/src/main/index.ts @@ -0,0 +1,528 @@ +import { app, BrowserWindow, ipcMain, dialog } from "electron"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { existsSync, readFileSync } from "node:fs"; + +// Keyboard shortcuts reference (matching Rust implementation): +// F11 - Toggle fullscreen (handled in main process) +// F3 - Toggle stats overlay (handled in renderer) +// Ctrl+Shift+Q - Stop streaming (handled in renderer) +// F8 - Toggle mouse/pointer lock (handled in main process via IPC) + +import { IPC_CHANNELS } from "@shared/ipc"; +import type { + MainToRendererSignalingEvent, + AuthLoginRequest, + AuthSessionRequest, + GamesFetchRequest, + ResolveLaunchIdRequest, + RegionsFetchRequest, + SessionCreateRequest, + SessionPollRequest, + SessionStopRequest, + SessionClaimRequest, + SignalingConnectRequest, + SendAnswerRequest, + IceCandidatePayload, + Settings, + VideoAccelerationPreference, + SubscriptionFetchRequest, + SessionConflictChoice, +} from "@shared/gfn"; + +import { getSettingsManager, type SettingsManager } from "./settings"; + +import { createSession, pollSession, stopSession, getActiveSessions, claimSession } from "./gfn/cloudmatch"; +import { AuthService } from "./gfn/auth"; +import { + fetchLibraryGames, + fetchMainGames, + fetchPublicGames, + resolveLaunchAppId, +} from "./gfn/games"; +import { fetchSubscription, fetchDynamicRegions } from "./gfn/subscription"; +import { GfnSignalingClient } from "./gfn/signaling"; +import { isSessionError, SessionError, GfnErrorCode } from "./gfn/errorCodes"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Configure Chromium video and WebRTC behavior before app.whenReady(). + +interface BootstrapVideoPreferences { + decoderPreference: VideoAccelerationPreference; + encoderPreference: VideoAccelerationPreference; +} + +function isAccelerationPreference(value: unknown): value is VideoAccelerationPreference { + return value === "auto" || value === "hardware" || value === "software"; +} + +function loadBootstrapVideoPreferences(): BootstrapVideoPreferences { + const defaults: BootstrapVideoPreferences = { + decoderPreference: "auto", + encoderPreference: "auto", + }; + try { + const settingsPath = join(app.getPath("userData"), "settings.json"); + if (!existsSync(settingsPath)) { + return defaults; + } + const parsed = JSON.parse(readFileSync(settingsPath, "utf-8")) as Partial; + return { + decoderPreference: isAccelerationPreference(parsed.decoderPreference) + ? parsed.decoderPreference + : defaults.decoderPreference, + encoderPreference: isAccelerationPreference(parsed.encoderPreference) + ? parsed.encoderPreference + : defaults.encoderPreference, + }; + } catch { + return defaults; + } +} + +const bootstrapVideoPrefs = loadBootstrapVideoPreferences(); +console.log( + `[Main] Video acceleration preference: decode=${bootstrapVideoPrefs.decoderPreference}, encode=${bootstrapVideoPrefs.encoderPreference}`, +); + +// --- Platform-specific HW video decode features --- +const platformFeatures: string[] = []; + +if (process.platform === "win32") { + // Windows: D3D11 + Media Foundation path for HW decode/encode acceleration + if (bootstrapVideoPrefs.decoderPreference !== "software") { + platformFeatures.push("D3D11VideoDecoder"); + } + if ( + bootstrapVideoPrefs.decoderPreference !== "software" || + bootstrapVideoPrefs.encoderPreference !== "software" + ) { + platformFeatures.push("MediaFoundationD3D11VideoCapture"); + } +} else if (process.platform === "linux") { + // Linux: VA-API path for HW decode/encode (Intel/AMD GPUs) + if (bootstrapVideoPrefs.decoderPreference !== "software") { + platformFeatures.push("VaapiVideoDecoder"); + } + if (bootstrapVideoPrefs.encoderPreference !== "software") { + platformFeatures.push("VaapiVideoEncoder"); + } + if ( + bootstrapVideoPrefs.decoderPreference !== "software" || + bootstrapVideoPrefs.encoderPreference !== "software" + ) { + platformFeatures.push("VaapiIgnoreDriverChecks"); + } +} +// macOS: VideoToolbox handles HW acceleration natively, no extra feature flags needed + +app.commandLine.appendSwitch("enable-features", + [ + // --- AV1 support (cross-platform) --- + "Dav1dVideoDecoder", // Fast AV1 software fallback via dav1d (if no HW decoder) + // --- Additional (cross-platform) --- + "HardwareMediaKeyHandling", + // --- Platform-specific HW decode/encode --- + ...platformFeatures, + ].join(","), +); + +const disableFeatures: string[] = [ + // Prevents mDNS candidate generation — faster ICE connectivity + "WebRtcHideLocalIpsWithMdns", +]; +if (process.platform === "linux") { + // ChromeOS-only direct video decoder path interferes on regular Linux + disableFeatures.push("UseChromeOSDirectVideoDecoder"); +} +app.commandLine.appendSwitch("disable-features", disableFeatures.join(",")); + +app.commandLine.appendSwitch("force-fieldtrials", + [ + // Disable send-side pacing — we are receive-only, pacing adds latency to RTCP feedback + "WebRTC-Video-Pacing/Disabled/", + ].join("/"), +); + +if (bootstrapVideoPrefs.decoderPreference === "hardware") { + app.commandLine.appendSwitch("enable-accelerated-video-decode"); +} else if (bootstrapVideoPrefs.decoderPreference === "software") { + app.commandLine.appendSwitch("disable-accelerated-video-decode"); +} + +if (bootstrapVideoPrefs.encoderPreference === "hardware") { + app.commandLine.appendSwitch("enable-accelerated-video-encode"); +} else if (bootstrapVideoPrefs.encoderPreference === "software") { + app.commandLine.appendSwitch("disable-accelerated-video-encode"); +} + +// Ensure the GPU process doesn't blocklist our GPU for video decode +app.commandLine.appendSwitch("ignore-gpu-blocklist"); + +// --- Responsiveness flags --- +// Keep default compositor frame pacing (vsync + frame cap) to avoid runaway +// CPU usage from uncapped UI animations. +// Prevent renderer throttling when the window is backgrounded or occluded. +app.commandLine.appendSwitch("disable-renderer-backgrounding"); +app.commandLine.appendSwitch("disable-backgrounding-occluded-windows"); +// Remove getUserMedia FPS cap (not strictly needed for receive-only but avoids potential limits) +app.commandLine.appendSwitch("max-gum-fps", "999"); + +let mainWindow: BrowserWindow | null = null; +let signalingClient: GfnSignalingClient | null = null; +let signalingClientKey: string | null = null; +let authService: AuthService; +let settingsManager: SettingsManager; + +function emitToRenderer(event: MainToRendererSignalingEvent): void { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(IPC_CHANNELS.SIGNALING_EVENT, event); + } +} + +async function createMainWindow(): Promise { + const preloadMjsPath = join(__dirname, "../preload/index.mjs"); + const preloadJsPath = join(__dirname, "../preload/index.js"); + const preloadPath = existsSync(preloadMjsPath) ? preloadMjsPath : preloadJsPath; + + const settings = settingsManager.getAll(); + + mainWindow = new BrowserWindow({ + width: settings.windowWidth || 1400, + height: settings.windowHeight || 900, + minWidth: 1024, + minHeight: 680, + autoHideMenuBar: true, + backgroundColor: "#0f172a", + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + }); + + // Handle F11 fullscreen toggle — send to renderer so it uses W3C Fullscreen API + // (which enables navigator.keyboard.lock for Escape key capture) + mainWindow.webContents.on("before-input-event", (event, input) => { + if (input.key === "F11" && input.type === "keyDown") { + event.preventDefault(); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("app:toggle-fullscreen"); + } + } + }); + + if (process.platform === "win32") { + // Keep native window fullscreen in sync with HTML fullscreen so Windows treats + // stream playback like a real fullscreen window instead of only DOM fullscreen. + mainWindow.webContents.on("enter-html-full-screen", () => { + if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isFullScreen()) { + mainWindow.setFullScreen(true); + } + }); + + mainWindow.webContents.on("leave-html-full-screen", () => { + if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isFullScreen()) { + mainWindow.setFullScreen(false); + } + }); + } + + if (process.env.ELECTRON_RENDERER_URL) { + await mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL); + } else { + await mainWindow.loadFile(join(__dirname, "../../dist/index.html")); + } + + mainWindow.on("closed", () => { + mainWindow = null; + }); +} + +async function resolveJwt(token?: string): Promise { + return authService.resolveJwtToken(token); +} + +/** + * Show a dialog asking the user how to handle a session conflict + * Returns the user's choice: "resume", "new", or "cancel" + */ +async function showSessionConflictDialog(): Promise { + if (!mainWindow || mainWindow.isDestroyed()) { + return "cancel"; + } + + const result = await dialog.showMessageBox(mainWindow, { + type: "question", + buttons: ["Resume", "Start New", "Cancel"], + defaultId: 0, + cancelId: 2, + title: "Active Session Detected", + message: "You have an active session running.", + detail: "Resume it or start a new one?", + }); + + switch (result.response) { + case 0: + return "resume"; + case 1: + return "new"; + default: + return "cancel"; + } +} + +/** + * Check if an error indicates a session conflict + */ +function isSessionConflictError(error: unknown): boolean { + if (isSessionError(error)) { + return error.isSessionConflict(); + } + return false; +} + +function rethrowSerializedSessionError(error: unknown): never { + if (error instanceof SessionError) { + throw error.toJSON(); + } + throw error; +} + +function registerIpcHandlers(): void { + ipcMain.handle(IPC_CHANNELS.AUTH_GET_SESSION, async (_event, payload: AuthSessionRequest = {}) => { + return authService.ensureValidSessionWithStatus(Boolean(payload.forceRefresh)); + }); + + ipcMain.handle(IPC_CHANNELS.AUTH_GET_PROVIDERS, async () => { + return authService.getProviders(); + }); + + ipcMain.handle(IPC_CHANNELS.AUTH_GET_REGIONS, async (_event, payload: RegionsFetchRequest) => { + return authService.getRegions(payload?.token); + }); + + ipcMain.handle(IPC_CHANNELS.AUTH_LOGIN, async (_event, payload: AuthLoginRequest) => { + return authService.login(payload); + }); + + ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async () => { + await authService.logout(); + }); + + ipcMain.handle(IPC_CHANNELS.SUBSCRIPTION_FETCH, async (_event, payload: SubscriptionFetchRequest) => { + const token = await resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + const userId = payload.userId; + + // Fetch dynamic regions to get the VPC ID (handles Alliance partners correctly) + const { vpcId } = await fetchDynamicRegions(token, streamingBaseUrl); + + return fetchSubscription(token, userId, vpcId ?? undefined); + }); + + ipcMain.handle(IPC_CHANNELS.GAMES_FETCH_MAIN, async (_event, payload: GamesFetchRequest) => { + const token = await resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + return fetchMainGames(token, streamingBaseUrl); + }); + + ipcMain.handle(IPC_CHANNELS.GAMES_FETCH_LIBRARY, async (_event, payload: GamesFetchRequest) => { + const token = await resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + return fetchLibraryGames(token, streamingBaseUrl); + }); + + ipcMain.handle(IPC_CHANNELS.GAMES_FETCH_PUBLIC, async () => { + return fetchPublicGames(); + }); + + ipcMain.handle(IPC_CHANNELS.GAMES_RESOLVE_LAUNCH_ID, async (_event, payload: ResolveLaunchIdRequest) => { + const token = await resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + return resolveLaunchAppId(token, payload.appIdOrUuid, streamingBaseUrl); + }); + + ipcMain.handle(IPC_CHANNELS.CREATE_SESSION, async (_event, payload: SessionCreateRequest) => { + try { + const token = await resolveJwt(payload.token); + const streamingBaseUrl = payload.streamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + return createSession({ + ...payload, + token, + streamingBaseUrl, + }); + } catch (error) { + rethrowSerializedSessionError(error); + } + }); + + ipcMain.handle(IPC_CHANNELS.POLL_SESSION, async (_event, payload: SessionPollRequest) => { + try { + const token = await resolveJwt(payload.token); + return pollSession({ + ...payload, + token, + streamingBaseUrl: payload.streamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl, + }); + } catch (error) { + rethrowSerializedSessionError(error); + } + }); + + ipcMain.handle(IPC_CHANNELS.STOP_SESSION, async (_event, payload: SessionStopRequest) => { + try { + const token = await resolveJwt(payload.token); + return stopSession({ + ...payload, + token, + streamingBaseUrl: payload.streamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl, + }); + } catch (error) { + rethrowSerializedSessionError(error); + } + }); + + ipcMain.handle(IPC_CHANNELS.GET_ACTIVE_SESSIONS, async (_event, token?: string, streamingBaseUrl?: string) => { + const jwt = await resolveJwt(token); + const baseUrl = streamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + return getActiveSessions(jwt, baseUrl); + }); + + ipcMain.handle(IPC_CHANNELS.CLAIM_SESSION, async (_event, payload: SessionClaimRequest) => { + try { + const token = await resolveJwt(payload.token); + const streamingBaseUrl = payload.streamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; + return claimSession({ + ...payload, + token, + streamingBaseUrl, + }); + } catch (error) { + rethrowSerializedSessionError(error); + } + }); + + ipcMain.handle(IPC_CHANNELS.SESSION_CONFLICT_DIALOG, async (): Promise => { + return showSessionConflictDialog(); + }); + + ipcMain.handle( + IPC_CHANNELS.CONNECT_SIGNALING, + async (_event, payload: SignalingConnectRequest): Promise => { + const nextKey = `${payload.sessionId}|${payload.signalingServer}|${payload.signalingUrl ?? ""}`; + if (signalingClient && signalingClientKey === nextKey) { + console.log("[Signaling] Reuse existing signaling connection (duplicate connect request ignored)"); + return; + } + + if (signalingClient) { + signalingClient.disconnect(); + } + + signalingClient = new GfnSignalingClient( + payload.signalingServer, + payload.sessionId, + payload.signalingUrl, + ); + signalingClientKey = nextKey; + signalingClient.onEvent(emitToRenderer); + await signalingClient.connect(); + }, + ); + + ipcMain.handle(IPC_CHANNELS.DISCONNECT_SIGNALING, async (): Promise => { + signalingClient?.disconnect(); + signalingClient = null; + signalingClientKey = null; + }); + + ipcMain.handle(IPC_CHANNELS.SEND_ANSWER, async (_event, payload: SendAnswerRequest) => { + if (!signalingClient) { + throw new Error("Signaling is not connected"); + } + return signalingClient.sendAnswer(payload); + }); + + ipcMain.handle(IPC_CHANNELS.SEND_ICE_CANDIDATE, async (_event, payload: IceCandidatePayload) => { + if (!signalingClient) { + throw new Error("Signaling is not connected"); + } + return signalingClient.sendIceCandidate(payload); + }); + + // Toggle fullscreen via IPC (for completeness) + ipcMain.handle(IPC_CHANNELS.TOGGLE_FULLSCREEN, async () => { + if (mainWindow && !mainWindow.isDestroyed()) { + const isFullScreen = mainWindow.isFullScreen(); + mainWindow.setFullScreen(!isFullScreen); + } + }); + + // Toggle pointer lock via IPC (F8 shortcut) + ipcMain.handle(IPC_CHANNELS.TOGGLE_POINTER_LOCK, async () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("app:toggle-pointer-lock"); + } + }); + + // Settings IPC handlers + ipcMain.handle(IPC_CHANNELS.SETTINGS_GET, async (): Promise => { + return settingsManager.getAll(); + }); + + ipcMain.handle(IPC_CHANNELS.SETTINGS_SET, async (_event: Electron.IpcMainInvokeEvent, key: K, value: Settings[K]) => { + settingsManager.set(key, value); + }); + + ipcMain.handle(IPC_CHANNELS.SETTINGS_RESET, async (): Promise => { + return settingsManager.reset(); + }); + + // Save window size when it changes + mainWindow?.on("resize", () => { + if (mainWindow && !mainWindow.isDestroyed()) { + const [width, height] = mainWindow.getSize(); + settingsManager.set("windowWidth", width); + settingsManager.set("windowHeight", height); + } + }); +} + +app.whenReady().then(async () => { + authService = new AuthService(join(app.getPath("userData"), "auth-state.json")); + await authService.initialize(); + + settingsManager = getSettingsManager(); + + registerIpcHandlers(); + await createMainWindow(); + + app.on("activate", async () => { + if (BrowserWindow.getAllWindows().length === 0) { + await createMainWindow(); + } + }); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + signalingClient?.disconnect(); + signalingClient = null; + signalingClientKey = null; +}); + +// Export for use by other modules +export { showSessionConflictDialog, isSessionConflictError }; diff --git a/opennow-stable/src/main/settings.ts b/opennow-stable/src/main/settings.ts new file mode 100644 index 0000000..f038f2a --- /dev/null +++ b/opennow-stable/src/main/settings.ts @@ -0,0 +1,200 @@ +import { app } from "electron"; +import { join } from "node:path"; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import type { VideoCodec, ColorQuality, VideoAccelerationPreference } from "@shared/gfn"; + +export interface Settings { + /** Video resolution (e.g., "1920x1080") */ + resolution: string; + /** Target FPS (30, 60, 120, etc.) */ + fps: number; + /** Maximum bitrate in Mbps (200 = unlimited) */ + maxBitrateMbps: number; + /** Preferred video codec */ + codec: VideoCodec; + /** Preferred video decode acceleration mode */ + decoderPreference: VideoAccelerationPreference; + /** Preferred video encode acceleration mode */ + encoderPreference: VideoAccelerationPreference; + /** Color quality (bit depth + chroma subsampling) */ + colorQuality: ColorQuality; + /** Preferred region URL (empty = auto) */ + region: string; + /** Enable clipboard paste into stream */ + clipboardPaste: boolean; + /** Mouse sensitivity multiplier */ + mouseSensitivity: number; + /** Toggle stats overlay shortcut */ + shortcutToggleStats: string; + /** Toggle pointer lock shortcut */ + shortcutTogglePointerLock: string; + /** Stop stream shortcut */ + shortcutStopStream: string; + /** Toggle anti-AFK shortcut */ + shortcutToggleAntiAfk: string; + /** Window width */ + windowWidth: number; + /** Window height */ + windowHeight: number; +} + +const defaultStopShortcut = "Ctrl+Shift+Q"; +const defaultAntiAfkShortcut = "Ctrl+Shift+K"; +const LEGACY_STOP_SHORTCUTS = new Set(["META+SHIFT+Q", "CMD+SHIFT+Q"]); +const LEGACY_ANTI_AFK_SHORTCUTS = new Set(["META+SHIFT+F10", "CMD+SHIFT+F10", "CTRL+SHIFT+F10"]); + +const DEFAULT_SETTINGS: Settings = { + resolution: "1920x1080", + fps: 60, + maxBitrateMbps: 75, + codec: "H264", + decoderPreference: "auto", + encoderPreference: "auto", + colorQuality: "10bit_420", + region: "", + clipboardPaste: false, + mouseSensitivity: 1, + shortcutToggleStats: "F3", + shortcutTogglePointerLock: "F8", + shortcutStopStream: defaultStopShortcut, + shortcutToggleAntiAfk: defaultAntiAfkShortcut, + windowWidth: 1400, + windowHeight: 900, +}; + +export class SettingsManager { + private settings: Settings; + private readonly settingsPath: string; + + constructor() { + this.settingsPath = join(app.getPath("userData"), "settings.json"); + this.settings = this.load(); + } + + /** + * Load settings from disk or return defaults if file doesn't exist + */ + private load(): Settings { + try { + if (!existsSync(this.settingsPath)) { + return { ...DEFAULT_SETTINGS }; + } + + const content = readFileSync(this.settingsPath, "utf-8"); + const parsed = JSON.parse(content) as Partial; + + // Merge with defaults to ensure all fields exist + const merged: Settings = { + ...DEFAULT_SETTINGS, + ...parsed, + }; + + const migrated = this.migrateLegacyShortcutDefaults(merged); + if (migrated) { + writeFileSync(this.settingsPath, JSON.stringify(merged, null, 2), "utf-8"); + } + + return merged; + } catch (error) { + console.error("Failed to load settings, using defaults:", error); + return { ...DEFAULT_SETTINGS }; + } + } + + private migrateLegacyShortcutDefaults(settings: Settings): boolean { + let migrated = false; + + const normalizeShortcut = (value: string): string => value.replace(/\s+/g, "").toUpperCase(); + const stopShortcut = normalizeShortcut(settings.shortcutStopStream); + const antiAfkShortcut = normalizeShortcut(settings.shortcutToggleAntiAfk); + + if (LEGACY_STOP_SHORTCUTS.has(stopShortcut)) { + settings.shortcutStopStream = defaultStopShortcut; + migrated = true; + } + + if (LEGACY_ANTI_AFK_SHORTCUTS.has(antiAfkShortcut)) { + settings.shortcutToggleAntiAfk = defaultAntiAfkShortcut; + migrated = true; + } + + return migrated; + } + + /** + * Save current settings to disk + */ + private save(): void { + try { + const dir = join(app.getPath("userData")); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8"); + } catch (error) { + console.error("Failed to save settings:", error); + } + } + + /** + * Get all current settings + */ + getAll(): Settings { + return { ...this.settings }; + } + + /** + * Get a specific setting value + */ + get(key: K): Settings[K] { + return this.settings[key]; + } + + /** + * Update a specific setting value + */ + set(key: K, value: Settings[K]): void { + this.settings[key] = value; + this.save(); + } + + /** + * Update multiple settings at once + */ + setMultiple(updates: Partial): void { + this.settings = { + ...this.settings, + ...updates, + }; + this.save(); + } + + /** + * Reset all settings to defaults + */ + reset(): Settings { + this.settings = { ...DEFAULT_SETTINGS }; + this.save(); + return { ...this.settings }; + } + + /** + * Get the default settings + */ + getDefaults(): Settings { + return { ...DEFAULT_SETTINGS }; + } +} + +// Singleton instance +let settingsManager: SettingsManager | null = null; + +export function getSettingsManager(): SettingsManager { + if (!settingsManager) { + settingsManager = new SettingsManager(); + } + return settingsManager; +} + +export { DEFAULT_SETTINGS }; diff --git a/opennow-stable/src/preload/index.ts b/opennow-stable/src/preload/index.ts new file mode 100644 index 0000000..bd612cd --- /dev/null +++ b/opennow-stable/src/preload/index.ts @@ -0,0 +1,78 @@ +import { contextBridge, ipcRenderer } from "electron"; + +import { IPC_CHANNELS } from "@shared/ipc"; +import type { + AuthLoginRequest, + AuthSessionRequest, + GamesFetchRequest, + ResolveLaunchIdRequest, + RegionsFetchRequest, + MainToRendererSignalingEvent, + OpenNowApi, + SessionCreateRequest, + SessionPollRequest, + SessionStopRequest, + SessionClaimRequest, + SignalingConnectRequest, + SendAnswerRequest, + IceCandidatePayload, + Settings, + SubscriptionFetchRequest, +} from "@shared/gfn"; + +// Extend the OpenNowApi interface for internal preload use +type PreloadApi = OpenNowApi; + +const api: PreloadApi = { + getAuthSession: (input: AuthSessionRequest = {}) => ipcRenderer.invoke(IPC_CHANNELS.AUTH_GET_SESSION, input), + getLoginProviders: () => ipcRenderer.invoke(IPC_CHANNELS.AUTH_GET_PROVIDERS), + getRegions: (input: RegionsFetchRequest = {}) => ipcRenderer.invoke(IPC_CHANNELS.AUTH_GET_REGIONS, input), + login: (input: AuthLoginRequest) => ipcRenderer.invoke(IPC_CHANNELS.AUTH_LOGIN, input), + logout: () => ipcRenderer.invoke(IPC_CHANNELS.AUTH_LOGOUT), + fetchSubscription: (input: SubscriptionFetchRequest) => + ipcRenderer.invoke(IPC_CHANNELS.SUBSCRIPTION_FETCH, input), + fetchMainGames: (input: GamesFetchRequest) => ipcRenderer.invoke(IPC_CHANNELS.GAMES_FETCH_MAIN, input), + fetchLibraryGames: (input: GamesFetchRequest) => + ipcRenderer.invoke(IPC_CHANNELS.GAMES_FETCH_LIBRARY, input), + fetchPublicGames: () => ipcRenderer.invoke(IPC_CHANNELS.GAMES_FETCH_PUBLIC), + resolveLaunchAppId: (input: ResolveLaunchIdRequest) => + ipcRenderer.invoke(IPC_CHANNELS.GAMES_RESOLVE_LAUNCH_ID, input), + createSession: (input: SessionCreateRequest) => ipcRenderer.invoke(IPC_CHANNELS.CREATE_SESSION, input), + pollSession: (input: SessionPollRequest) => ipcRenderer.invoke(IPC_CHANNELS.POLL_SESSION, input), + stopSession: (input: SessionStopRequest) => ipcRenderer.invoke(IPC_CHANNELS.STOP_SESSION, input), + getActiveSessions: (token?: string, streamingBaseUrl?: string) => + ipcRenderer.invoke(IPC_CHANNELS.GET_ACTIVE_SESSIONS, token, streamingBaseUrl), + claimSession: (input: SessionClaimRequest) => ipcRenderer.invoke(IPC_CHANNELS.CLAIM_SESSION, input), + showSessionConflictDialog: () => ipcRenderer.invoke(IPC_CHANNELS.SESSION_CONFLICT_DIALOG), + connectSignaling: (input: SignalingConnectRequest) => + ipcRenderer.invoke(IPC_CHANNELS.CONNECT_SIGNALING, input), + disconnectSignaling: () => ipcRenderer.invoke(IPC_CHANNELS.DISCONNECT_SIGNALING), + sendAnswer: (input: SendAnswerRequest) => ipcRenderer.invoke(IPC_CHANNELS.SEND_ANSWER, input), + sendIceCandidate: (input: IceCandidatePayload) => + ipcRenderer.invoke(IPC_CHANNELS.SEND_ICE_CANDIDATE, input), + onSignalingEvent: (listener: (event: MainToRendererSignalingEvent) => void) => { + const wrapped = (_event: Electron.IpcRendererEvent, payload: MainToRendererSignalingEvent) => { + listener(payload); + }; + + ipcRenderer.on(IPC_CHANNELS.SIGNALING_EVENT, wrapped); + return () => { + ipcRenderer.off(IPC_CHANNELS.SIGNALING_EVENT, wrapped); + }; + }, + onToggleFullscreen: (listener: () => void) => { + const wrapped = () => listener(); + ipcRenderer.on("app:toggle-fullscreen", wrapped); + return () => { + ipcRenderer.off("app:toggle-fullscreen", wrapped); + }; + }, + toggleFullscreen: () => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_FULLSCREEN), + togglePointerLock: () => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_POINTER_LOCK), + getSettings: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET), + setSetting: (key: K, value: Settings[K]) => + ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SET, key, value), + resetSettings: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_RESET), +}; + +contextBridge.exposeInMainWorld("openNow", api); diff --git a/opennow-stable/src/renderer/index.html b/opennow-stable/src/renderer/index.html new file mode 100644 index 0000000..76f1763 --- /dev/null +++ b/opennow-stable/src/renderer/index.html @@ -0,0 +1,15 @@ + + + + + + OpenNOW + + + + + +
+ + + diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx new file mode 100644 index 0000000..cc4bef0 --- /dev/null +++ b/opennow-stable/src/renderer/src/App.tsx @@ -0,0 +1,1425 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { JSX } from "react"; + +import type { + ActiveSessionInfo, + AuthSession, + AuthUser, + GameInfo, + LoginProvider, + MainToRendererSignalingEvent, + SessionInfo, + Settings, + SubscriptionInfo, + StreamRegion, + VideoCodec, +} from "@shared/gfn"; + +import { + GfnWebRtcClient, + type StreamDiagnostics, + type StreamTimeWarning, +} from "./gfn/webrtcClient"; +import { formatShortcutForDisplay, isShortcutMatch, normalizeShortcut } from "./shortcuts"; + +// UI Components +import { LoginScreen } from "./components/LoginScreen"; +import { Navbar } from "./components/Navbar"; +import { HomePage } from "./components/HomePage"; +import { LibraryPage } from "./components/LibraryPage"; +import { SettingsPage } from "./components/SettingsPage"; +import { StreamLoading } from "./components/StreamLoading"; +import { StreamView } from "./components/StreamView"; + +const codecOptions: VideoCodec[] = ["H264", "H265", "AV1"]; +const resolutionOptions = ["1280x720", "1920x1080", "2560x1440", "3840x2160", "2560x1080", "3440x1440"]; +const fpsOptions = [30, 60, 120, 144, 240]; + +type GameSource = "main" | "library" | "public"; +type AppPage = "home" | "library" | "settings"; +type StreamStatus = "idle" | "queue" | "setup" | "starting" | "connecting" | "streaming"; +type StreamLoadingStatus = "queue" | "setup" | "starting" | "connecting"; +type ExitPromptState = { open: boolean; gameTitle: string }; +type StreamWarningState = { + code: StreamTimeWarning["code"]; + message: string; + tone: "warn" | "critical"; + secondsLeft?: number; +}; +type LaunchErrorState = { + stage: StreamLoadingStatus; + title: string; + description: string; + codeLabel?: string; +}; + +const isMac = navigator.platform.toLowerCase().includes("mac"); + +const DEFAULT_SHORTCUTS = { + shortcutToggleStats: "F3", + shortcutTogglePointerLock: "F8", + shortcutStopStream: "Ctrl+Shift+Q", + shortcutToggleAntiAfk: "Ctrl+Shift+K", +} as const; + +function sleep(ms: number): Promise { + return new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +function isNumericId(value: string | undefined): value is string { + if (!value) return false; + return /^\d+$/.test(value); +} + +function parseNumericId(value: string | undefined): number | null { + if (!isNumericId(value)) return null; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function defaultVariantId(game: GameInfo): string { + const fallback = game.variants[0]?.id; + const preferred = game.variants[game.selectedVariantIndex]?.id; + return preferred ?? fallback ?? game.id; +} + +function defaultDiagnostics(): StreamDiagnostics { + return { + connectionState: "closed", + inputReady: false, + connectedGamepads: 0, + resolution: "", + codec: "", + isHdr: false, + bitrateKbps: 0, + decodeFps: 0, + renderFps: 0, + packetsLost: 0, + packetsReceived: 0, + packetLossPercent: 0, + jitterMs: 0, + rttMs: 0, + framesReceived: 0, + framesDecoded: 0, + framesDropped: 0, + decodeTimeMs: 0, + renderTimeMs: 0, + jitterBufferDelayMs: 0, + inputQueueBufferedBytes: 0, + inputQueuePeakBufferedBytes: 0, + inputQueueDropCount: 0, + inputQueueMaxSchedulingDelayMs: 0, + gpuType: "", + serverRegion: "", + }; +} + +function isSessionLimitError(error: unknown): boolean { + if (error && typeof error === "object" && "gfnErrorCode" in error) { + const candidate = error.gfnErrorCode; + if (typeof candidate === "number") { + return candidate === 3237093643 || candidate === 3237093718; + } + } + if (error instanceof Error) { + const msg = error.message.toUpperCase(); + return msg.includes("SESSION LIMIT") || msg.includes("INSUFFICIENT_PLAYABILITY") || msg.includes("DUPLICATE SESSION"); + } + return false; +} + +function warningTone(code: StreamTimeWarning["code"]): "warn" | "critical" { + if (code === 3) { + return "critical"; + } + return "warn"; +} + +function warningMessage(code: StreamTimeWarning["code"]): string { + if (code === 1) return "Session time limit approaching"; + if (code === 2) return "Idle timeout approaching"; + return "Maximum session time approaching"; +} + +function toLoadingStatus(status: StreamStatus): StreamLoadingStatus { + switch (status) { + case "queue": + case "setup": + case "starting": + case "connecting": + return status; + default: + return "queue"; + } +} + +function toCodeLabel(code: number | undefined): string | undefined { + if (code === undefined) return undefined; + if (code === 3237093643) return `SessionLimitExceeded (${code})`; + if (code === 3237093718) return `SessionInsufficientPlayabilityLevel (${code})`; + return `GFN Error ${code}`; +} + +function extractLaunchErrorCode(error: unknown): number | undefined { + if (error && typeof error === "object") { + if ("gfnErrorCode" in error) { + const directCode = error.gfnErrorCode; + if (typeof directCode === "number") return directCode; + } + if ("statusCode" in error) { + const statusCode = error.statusCode; + if (typeof statusCode === "number" && statusCode > 0 && statusCode < 255) { + return 3237093632 + statusCode; + } + } + } + if (error instanceof Error) { + const match = error.message.match(/\b(3237\d{6,})\b/); + if (match) { + const code = Number(match[1]); + if (Number.isFinite(code)) return code; + } + } + return undefined; +} + +function toLaunchErrorState(error: unknown, stage: StreamLoadingStatus): LaunchErrorState { + const unknownMessage = "The game could not start. Please try again."; + + const titleFromError = + error && typeof error === "object" && "title" in error && typeof error.title === "string" + ? error.title.trim() + : ""; + const descriptionFromError = + error && typeof error === "object" && "description" in error && typeof error.description === "string" + ? error.description.trim() + : ""; + const statusDescription = + error && typeof error === "object" && "statusDescription" in error && typeof error.statusDescription === "string" + ? error.statusDescription.trim() + : ""; + const messageFromError = error instanceof Error ? error.message.trim() : ""; + const combined = `${statusDescription} ${messageFromError}`.toUpperCase(); + const code = extractLaunchErrorCode(error); + + if ( + isSessionLimitError(error) || + combined.includes("INSUFFICIENT_PLAYABILITY") || + combined.includes("SESSION_LIMIT") || + combined.includes("DUPLICATE SESSION") + ) { + return { + stage, + title: "Duplicate Session Detected", + description: "Another session is already running on your account. Close it first or wait for it to timeout, then launch again.", + codeLabel: toCodeLabel(code), + }; + } + + return { + stage, + title: titleFromError || "Launch Failed", + description: descriptionFromError || messageFromError || statusDescription || unknownMessage, + codeLabel: toCodeLabel(code), + }; +} + +export function App(): JSX.Element { + // Auth State + const [authSession, setAuthSession] = useState(null); + const [providers, setProviders] = useState([]); + const [providerIdpId, setProviderIdpId] = useState(""); + const [isLoggingIn, setIsLoggingIn] = useState(false); + const [loginError, setLoginError] = useState(null); + const [isInitializing, setIsInitializing] = useState(true); + const [startupStatusMessage, setStartupStatusMessage] = useState("Restoring saved session..."); + const [startupRefreshNotice, setStartupRefreshNotice] = useState<{ + tone: "success" | "warn"; + text: string; + } | null>(null); + + // Navigation + const [currentPage, setCurrentPage] = useState("home"); + + // Games State + const [games, setGames] = useState([]); + const [libraryGames, setLibraryGames] = useState([]); + const [source, setSource] = useState("main"); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedGameId, setSelectedGameId] = useState(""); + const [variantByGameId, setVariantByGameId] = useState>({}); + const [isLoadingGames, setIsLoadingGames] = useState(false); + + // Settings State + const [settings, setSettings] = useState({ + resolution: "1920x1080", + fps: 60, + maxBitrateMbps: 75, + codec: "H264", + decoderPreference: "auto", + encoderPreference: "auto", + colorQuality: "10bit_420", + region: "", + clipboardPaste: false, + mouseSensitivity: 1, + shortcutToggleStats: DEFAULT_SHORTCUTS.shortcutToggleStats, + shortcutTogglePointerLock: DEFAULT_SHORTCUTS.shortcutTogglePointerLock, + shortcutStopStream: DEFAULT_SHORTCUTS.shortcutStopStream, + shortcutToggleAntiAfk: DEFAULT_SHORTCUTS.shortcutToggleAntiAfk, + windowWidth: 1400, + windowHeight: 900, + }); + const [settingsLoaded, setSettingsLoaded] = useState(false); + const [regions, setRegions] = useState([]); + const [subscriptionInfo, setSubscriptionInfo] = useState(null); + + // Stream State + const [session, setSession] = useState(null); + const [streamStatus, setStreamStatus] = useState("idle"); + const [diagnostics, setDiagnostics] = useState(defaultDiagnostics()); + const [showStatsOverlay, setShowStatsOverlay] = useState(true); + const [antiAfkEnabled, setAntiAfkEnabled] = useState(false); + const [escHoldReleaseIndicator, setEscHoldReleaseIndicator] = useState<{ visible: boolean; progress: number }>({ + visible: false, + progress: 0, + }); + const [exitPrompt, setExitPrompt] = useState({ open: false, gameTitle: "Game" }); + const [streamingGame, setStreamingGame] = useState(null); + const [queuePosition, setQueuePosition] = useState(); + const [navbarActiveSession, setNavbarActiveSession] = useState(null); + const [isResumingNavbarSession, setIsResumingNavbarSession] = useState(false); + const [launchError, setLaunchError] = useState(null); + const [sessionStartedAtMs, setSessionStartedAtMs] = useState(null); + const [sessionElapsedSeconds, setSessionElapsedSeconds] = useState(0); + const [streamWarning, setStreamWarning] = useState(null); + + // Refs + const videoRef = useRef(null); + const audioRef = useRef(null); + const clientRef = useRef(null); + const sessionRef = useRef(null); + const hasInitializedRef = useRef(false); + const regionsRequestRef = useRef(0); + const launchInFlightRef = useRef(false); + const exitPromptResolverRef = useRef<((confirmed: boolean) => void) | null>(null); + + // Session ref sync + useEffect(() => { + sessionRef.current = session; + }, [session]); + + // Derived state + const selectedProvider = useMemo(() => { + return providers.find((p) => p.idpId === providerIdpId) ?? authSession?.provider ?? null; + }, [providers, providerIdpId, authSession]); + + const effectiveStreamingBaseUrl = useMemo(() => { + return selectedProvider?.streamingServiceUrl ?? ""; + }, [selectedProvider]); + + const loadSubscriptionInfo = useCallback( + async (session: AuthSession): Promise => { + const token = session.tokens.idToken ?? session.tokens.accessToken; + const subscription = await window.openNow.fetchSubscription({ + token, + providerStreamingBaseUrl: session.provider.streamingServiceUrl, + userId: session.user.userId, + }); + setSubscriptionInfo(subscription); + }, + [], + ); + + const refreshNavbarActiveSession = useCallback(async (): Promise => { + if (!authSession) { + setNavbarActiveSession(null); + return; + } + const token = authSession.tokens.idToken ?? authSession.tokens.accessToken; + if (!token || !effectiveStreamingBaseUrl) { + setNavbarActiveSession(null); + return; + } + try { + const activeSessions = await window.openNow.getActiveSessions(token, effectiveStreamingBaseUrl); + const candidate = activeSessions.find((entry) => entry.status === 3 || entry.status === 2) ?? null; + setNavbarActiveSession(candidate); + } catch (error) { + console.warn("Failed to refresh active sessions:", error); + } + }, [authSession, effectiveStreamingBaseUrl]); + + useEffect(() => { + if (!startupRefreshNotice) return; + const timer = window.setTimeout(() => setStartupRefreshNotice(null), 7000); + return () => window.clearTimeout(timer); + }, [startupRefreshNotice]); + + useEffect(() => { + if (!authSession || streamStatus !== "idle") { + return; + } + void refreshNavbarActiveSession(); + const timer = window.setInterval(() => { + void refreshNavbarActiveSession(); + }, 10000); + return () => window.clearInterval(timer); + }, [authSession, refreshNavbarActiveSession, streamStatus]); + + // Initialize app + useEffect(() => { + if (hasInitializedRef.current) return; + hasInitializedRef.current = true; + + const initialize = async () => { + try { + // Load settings first + const loadedSettings = await window.openNow.getSettings(); + setSettings(loadedSettings); + setSettingsLoaded(true); + + // Load providers and session (force refresh on startup restore) + setStartupStatusMessage("Restoring saved session and refreshing token..."); + const [providerList, sessionResult] = await Promise.all([ + window.openNow.getLoginProviders(), + window.openNow.getAuthSession({ forceRefresh: true }), + ]); + const persistedSession = sessionResult.session; + + if (sessionResult.refresh.outcome === "refreshed") { + setStartupRefreshNotice({ + tone: "success", + text: "Session restored. Token refreshed.", + }); + setStartupStatusMessage("Token refreshed. Loading your account..."); + } else if (sessionResult.refresh.outcome === "failed") { + setStartupRefreshNotice({ + tone: "warn", + text: "Token refresh failed. Using saved session token.", + }); + setStartupStatusMessage("Token refresh failed. Continuing with saved session..."); + } else if (sessionResult.refresh.outcome === "missing_refresh_token") { + setStartupStatusMessage("Saved session has no refresh token. Continuing..."); + } else if (persistedSession) { + setStartupStatusMessage("Session restored."); + } else { + setStartupStatusMessage("No saved session found."); + } + + // Update isInitializing FIRST so UI knows we're done loading + setIsInitializing(false); + setProviders(providerList); + setAuthSession(persistedSession); + + const activeProviderId = persistedSession?.provider?.idpId ?? providerList[0]?.idpId ?? ""; + setProviderIdpId(activeProviderId); + + if (persistedSession) { + // Load regions + const token = persistedSession.tokens.idToken ?? persistedSession.tokens.accessToken; + const discovered = await window.openNow.getRegions({ token }); + setRegions(discovered); + + try { + await loadSubscriptionInfo(persistedSession); + } catch (error) { + console.warn("Failed to load subscription info:", error); + setSubscriptionInfo(null); + } + + // Load games + try { + const mainGames = await window.openNow.fetchMainGames({ + token, + providerStreamingBaseUrl: persistedSession.provider.streamingServiceUrl, + }); + setGames(mainGames); + setSource("main"); + setSelectedGameId(mainGames[0]?.id ?? ""); + setVariantByGameId( + mainGames.reduce((acc, g) => { + acc[g.id] = defaultVariantId(g); + return acc; + }, {} as Record) + ); + + // Also load library + const libGames = await window.openNow.fetchLibraryGames({ + token, + providerStreamingBaseUrl: persistedSession.provider.streamingServiceUrl, + }); + setLibraryGames(libGames); + } catch { + // Fallback to public games + const publicGames = await window.openNow.fetchPublicGames(); + setGames(publicGames); + setSource("public"); + } + } else { + // Load public games for non-logged in users + const publicGames = await window.openNow.fetchPublicGames(); + setGames(publicGames); + setSource("public"); + setSubscriptionInfo(null); + } + } catch (error) { + console.error("Initialization failed:", error); + setStartupStatusMessage("Session restore failed. Please sign in again."); + // Always set isInitializing to false even on error + setIsInitializing(false); + } + }; + + void initialize(); + }, []); + + const shortcuts = useMemo(() => { + const parseWithFallback = (value: string, fallback: string) => { + const parsed = normalizeShortcut(value); + return parsed.valid ? parsed : normalizeShortcut(fallback); + }; + const toggleStats = parseWithFallback(settings.shortcutToggleStats, DEFAULT_SHORTCUTS.shortcutToggleStats); + const togglePointerLock = parseWithFallback(settings.shortcutTogglePointerLock, DEFAULT_SHORTCUTS.shortcutTogglePointerLock); + const stopStream = parseWithFallback(settings.shortcutStopStream, DEFAULT_SHORTCUTS.shortcutStopStream); + const toggleAntiAfk = parseWithFallback(settings.shortcutToggleAntiAfk, DEFAULT_SHORTCUTS.shortcutToggleAntiAfk); + return { toggleStats, togglePointerLock, stopStream, toggleAntiAfk }; + }, [ + settings.shortcutToggleStats, + settings.shortcutTogglePointerLock, + settings.shortcutStopStream, + settings.shortcutToggleAntiAfk, + ]); + + const requestEscLockedPointerCapture = useCallback(async (target: HTMLVideoElement) => { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen().catch(() => {}); + } + + const nav = navigator as any; + if (document.fullscreenElement && nav.keyboard?.lock) { + await nav.keyboard.lock([ + "Escape", "F11", "BrowserBack", "BrowserForward", "BrowserRefresh", + ]).catch(() => {}); + } + + await (target.requestPointerLock({ unadjustedMovement: true } as any) as unknown as Promise) + .catch((err: DOMException) => { + if (err.name === "NotSupportedError") { + return target.requestPointerLock(); + } + throw err; + }) + .catch(() => {}); + }, []); + + const resolveExitPrompt = useCallback((confirmed: boolean) => { + const resolver = exitPromptResolverRef.current; + exitPromptResolverRef.current = null; + setExitPrompt((prev) => (prev.open ? { ...prev, open: false } : prev)); + resolver?.(confirmed); + }, []); + + const requestExitPrompt = useCallback((gameTitle: string): Promise => { + return new Promise((resolve) => { + if (exitPromptResolverRef.current) { + // Close any previous pending prompt to avoid dangling promises. + exitPromptResolverRef.current(false); + } + exitPromptResolverRef.current = resolve; + setExitPrompt({ + open: true, + gameTitle: gameTitle || "this game", + }); + }); + }, []); + + const handleExitPromptConfirm = useCallback(() => { + resolveExitPrompt(true); + }, [resolveExitPrompt]); + + const handleExitPromptCancel = useCallback(() => { + resolveExitPrompt(false); + }, [resolveExitPrompt]); + + useEffect(() => { + return () => { + if (exitPromptResolverRef.current) { + exitPromptResolverRef.current(false); + exitPromptResolverRef.current = null; + } + }; + }, []); + + // Listen for F11 fullscreen toggle from main process (uses W3C Fullscreen API + // so navigator.keyboard.lock() can capture Escape in fullscreen) + useEffect(() => { + const unsubscribe = window.openNow.onToggleFullscreen(() => { + if (document.fullscreenElement) { + document.exitFullscreen().catch(() => {}); + } else { + document.documentElement.requestFullscreen().catch(() => {}); + } + }); + return () => unsubscribe(); + }, []); + + // Anti-AFK interval + useEffect(() => { + if (!antiAfkEnabled || streamStatus !== "streaming") return; + + const interval = window.setInterval(() => { + clientRef.current?.sendAntiAfkPulse(); + }, 240000); // 4 minutes + + return () => clearInterval(interval); + }, [antiAfkEnabled, streamStatus]); + + useEffect(() => { + if (streamStatus === "idle" || sessionStartedAtMs === null) { + setSessionElapsedSeconds(0); + return; + } + + const updateElapsed = () => { + const elapsed = Math.max(0, Math.floor((Date.now() - sessionStartedAtMs) / 1000)); + setSessionElapsedSeconds(elapsed); + }; + + updateElapsed(); + const timer = window.setInterval(updateElapsed, 1000); + return () => window.clearInterval(timer); + }, [sessionStartedAtMs, streamStatus]); + + useEffect(() => { + if (!streamWarning) return; + const warning = streamWarning; + const timer = window.setTimeout(() => { + setStreamWarning((current) => (current === warning ? null : current)); + }, 12000); + return () => window.clearTimeout(timer); + }, [streamWarning]); + + // Signaling events + useEffect(() => { + const unsubscribe = window.openNow.onSignalingEvent(async (event: MainToRendererSignalingEvent) => { + console.log(`[App] Signaling event: ${event.type}`, event.type === "offer" ? `(SDP ${event.sdp.length} chars)` : "", event.type === "remote-ice" ? event.candidate : ""); + try { + if (event.type === "offer") { + const activeSession = sessionRef.current; + if (!activeSession) { + console.warn("[App] Received offer but no active session in sessionRef!"); + return; + } + console.log("[App] Active session for offer:", JSON.stringify({ + sessionId: activeSession.sessionId, + serverIp: activeSession.serverIp, + signalingServer: activeSession.signalingServer, + mediaConnectionInfo: activeSession.mediaConnectionInfo, + iceServersCount: activeSession.iceServers?.length, + })); + + if (!clientRef.current && videoRef.current && audioRef.current) { + clientRef.current = new GfnWebRtcClient({ + videoElement: videoRef.current, + audioElement: audioRef.current, + onLog: (line: string) => console.log(`[WebRTC] ${line}`), + onStats: (stats) => setDiagnostics(stats), + onEscHoldProgress: (visible, progress) => { + setEscHoldReleaseIndicator({ visible, progress }); + }, + onTimeWarning: (warning) => { + setStreamWarning({ + code: warning.code, + message: warningMessage(warning.code), + tone: warningTone(warning.code), + secondsLeft: warning.secondsLeft, + }); + }, + }); + } + + if (clientRef.current) { + await clientRef.current.handleOffer(event.sdp, activeSession, { + codec: settings.codec, + colorQuality: settings.colorQuality, + resolution: settings.resolution, + fps: settings.fps, + maxBitrateKbps: settings.maxBitrateMbps * 1000, + }); + setLaunchError(null); + setStreamStatus("streaming"); + setSessionStartedAtMs((current) => current ?? Date.now()); + } + } else if (event.type === "remote-ice") { + await clientRef.current?.addRemoteCandidate(event.candidate); + } else if (event.type === "disconnected") { + console.warn("Signaling disconnected:", event.reason); + clientRef.current?.dispose(); + clientRef.current = null; + setStreamStatus("idle"); + setSession(null); + setStreamingGame(null); + setLaunchError(null); + setSessionStartedAtMs(null); + setSessionElapsedSeconds(0); + setStreamWarning(null); + setEscHoldReleaseIndicator({ visible: false, progress: 0 }); + setDiagnostics(defaultDiagnostics()); + launchInFlightRef.current = false; + } else if (event.type === "error") { + console.error("Signaling error:", event.message); + } + } catch (error) { + console.error("Signaling event error:", error); + } + }); + + return () => unsubscribe(); + }, [settings]); + + // Save settings when changed + const updateSetting = useCallback(async (key: K, value: Settings[K]) => { + setSettings((prev) => ({ ...prev, [key]: value })); + if (settingsLoaded) { + await window.openNow.setSetting(key, value); + } + }, [settingsLoaded]); + + // Login handler + const handleLogin = useCallback(async () => { + setIsLoggingIn(true); + setLoginError(null); + try { + const session = await window.openNow.login({ providerIdpId: providerIdpId || undefined }); + setAuthSession(session); + setProviderIdpId(session.provider.idpId); + + // Load regions + const token = session.tokens.idToken ?? session.tokens.accessToken; + const discovered = await window.openNow.getRegions({ token }); + setRegions(discovered); + + try { + await loadSubscriptionInfo(session); + } catch (error) { + console.warn("Failed to load subscription info:", error); + setSubscriptionInfo(null); + } + + // Load games + const mainGames = await window.openNow.fetchMainGames({ + token, + providerStreamingBaseUrl: session.provider.streamingServiceUrl, + }); + setGames(mainGames); + setSource("main"); + setSelectedGameId(mainGames[0]?.id ?? ""); + + // Load library + const libGames = await window.openNow.fetchLibraryGames({ + token, + providerStreamingBaseUrl: session.provider.streamingServiceUrl, + }); + setLibraryGames(libGames); + } catch (error) { + setLoginError(error instanceof Error ? error.message : "Login failed"); + } finally { + setIsLoggingIn(false); + } + }, [loadSubscriptionInfo, providerIdpId]); + + // Logout handler + const handleLogout = useCallback(async () => { + await window.openNow.logout(); + setAuthSession(null); + setGames([]); + setLibraryGames([]); + setNavbarActiveSession(null); + setIsResumingNavbarSession(false); + setLaunchError(null); + setSubscriptionInfo(null); + setCurrentPage("home"); + const publicGames = await window.openNow.fetchPublicGames(); + setGames(publicGames); + setSource("public"); + }, []); + + // Load games handler + const loadGames = useCallback(async (targetSource: GameSource) => { + setIsLoadingGames(true); + try { + const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; + const baseUrl = effectiveStreamingBaseUrl; + + let result: GameInfo[] = []; + if (targetSource === "main" && token) { + result = await window.openNow.fetchMainGames({ token, providerStreamingBaseUrl: baseUrl }); + } else if (targetSource === "library" && token) { + result = await window.openNow.fetchLibraryGames({ token, providerStreamingBaseUrl: baseUrl }); + setLibraryGames(result); + } else if (targetSource === "public") { + result = await window.openNow.fetchPublicGames(); + } + + if (targetSource !== "library") { + setGames(result); + setSource(targetSource); + setSelectedGameId(result[0]?.id ?? ""); + } + } catch (error) { + console.error("Failed to load games:", error); + } finally { + setIsLoadingGames(false); + } + }, [authSession, effectiveStreamingBaseUrl]); + + const claimAndConnectSession = useCallback(async (existingSession: ActiveSessionInfo): Promise => { + const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; + if (!token) { + throw new Error("Missing token for session resume"); + } + if (!existingSession.serverIp) { + throw new Error("Active session is missing server address. Start the game again to create a new session."); + } + + const claimed = await window.openNow.claimSession({ + token, + streamingBaseUrl: effectiveStreamingBaseUrl, + serverIp: existingSession.serverIp, + sessionId: existingSession.sessionId, + settings: { + resolution: settings.resolution, + fps: settings.fps, + maxBitrateMbps: settings.maxBitrateMbps, + codec: settings.codec, + colorQuality: settings.colorQuality, + }, + }); + + console.log("Claimed session:", { + sessionId: claimed.sessionId, + signalingServer: claimed.signalingServer, + signalingUrl: claimed.signalingUrl, + status: claimed.status, + }); + + await sleep(1000); + + setSession(claimed); + sessionRef.current = claimed; + setQueuePosition(undefined); + setStreamStatus("connecting"); + await window.openNow.connectSignaling({ + sessionId: claimed.sessionId, + signalingServer: claimed.signalingServer, + signalingUrl: claimed.signalingUrl, + }); + }, [authSession, effectiveStreamingBaseUrl, settings]); + + // Play game handler + const handlePlayGame = useCallback(async (game: GameInfo) => { + if (!selectedProvider) return; + + if (launchInFlightRef.current || streamStatus !== "idle") { + console.warn("Ignoring play request: launch already in progress or stream not idle", { + inFlight: launchInFlightRef.current, + streamStatus, + }); + return; + } + + launchInFlightRef.current = true; + let loadingStep: StreamLoadingStatus = "queue"; + const updateLoadingStep = (next: StreamLoadingStatus): void => { + loadingStep = next; + setStreamStatus(next); + }; + + setSessionStartedAtMs(Date.now()); + setSessionElapsedSeconds(0); + setStreamWarning(null); + setLaunchError(null); + setStreamingGame(game); + updateLoadingStep("queue"); + setQueuePosition(undefined); + + try { + const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; + const selectedVariantId = variantByGameId[game.id] ?? defaultVariantId(game); + + // Resolve appId + let appId: string | null = null; + if (isNumericId(selectedVariantId)) { + appId = selectedVariantId; + } else if (isNumericId(game.launchAppId)) { + appId = game.launchAppId; + } + + if (!appId && token) { + try { + const resolved = await window.openNow.resolveLaunchAppId({ + token, + providerStreamingBaseUrl: effectiveStreamingBaseUrl, + appIdOrUuid: game.uuid ?? selectedVariantId, + }); + if (resolved && isNumericId(resolved)) { + appId = resolved; + } + } catch { + // Ignore resolution errors + } + } + + if (!appId) { + throw new Error("Could not resolve numeric appId for this game"); + } + + // Check for active sessions first + if (token) { + try { + const activeSessions = await window.openNow.getActiveSessions(token, effectiveStreamingBaseUrl); + if (activeSessions.length > 0) { + const existingSession = activeSessions[0]; + await claimAndConnectSession(existingSession); + setNavbarActiveSession(null); + return; + } + } catch (error) { + console.error("Failed to claim/resume session:", error); + // Continue to create new session + } + } + + // Create new session + const newSession = await window.openNow.createSession({ + token: token || undefined, + streamingBaseUrl: effectiveStreamingBaseUrl, + appId, + internalTitle: game.title, + accountLinked: game.playType !== "INSTALL_TO_PLAY", + zone: "prod", + settings: { + resolution: settings.resolution, + fps: settings.fps, + maxBitrateMbps: settings.maxBitrateMbps, + codec: settings.codec, + colorQuality: settings.colorQuality, + }, + }); + + setSession(newSession); + + // Poll for readiness + let readyCount = 0; + for (let attempt = 1; attempt <= 30; attempt++) { + await sleep(2000); + + const polled = await window.openNow.pollSession({ + token: token || undefined, + streamingBaseUrl: newSession.streamingBaseUrl ?? effectiveStreamingBaseUrl, + serverIp: newSession.serverIp, + zone: newSession.zone, + sessionId: newSession.sessionId, + }); + + setSession(polled); + + console.log(`Poll attempt ${attempt}: status=${polled.status}, signalingUrl=${polled.signalingUrl}`); + + if (polled.status === 2 || polled.status === 3) { + readyCount++; + console.log(`Ready count: ${readyCount}/3`); + if (readyCount >= 3) break; + } + + // Update status based on session state + if (polled.status === 1) { + updateLoadingStep("setup"); + } + } + + if (readyCount < 3) { + throw new Error("Session did not become ready in time"); + } + + updateLoadingStep("connecting"); + + // Use the polled session data which has the latest signaling info + const finalSession = sessionRef.current ?? newSession; + console.log("Connecting signaling with:", { + sessionId: finalSession.sessionId, + signalingServer: finalSession.signalingServer, + signalingUrl: finalSession.signalingUrl, + status: finalSession.status, + }); + + await window.openNow.connectSignaling({ + sessionId: finalSession.sessionId, + signalingServer: finalSession.signalingServer, + signalingUrl: finalSession.signalingUrl, + }); + } catch (error) { + console.error("Launch failed:", error); + setLaunchError(toLaunchErrorState(error, loadingStep)); + await window.openNow.disconnectSignaling().catch(() => {}); + clientRef.current?.dispose(); + clientRef.current = null; + setSession(null); + setStreamStatus("idle"); + setQueuePosition(undefined); + setSessionStartedAtMs(null); + setSessionElapsedSeconds(0); + setStreamWarning(null); + setEscHoldReleaseIndicator({ visible: false, progress: 0 }); + setDiagnostics(defaultDiagnostics()); + void refreshNavbarActiveSession(); + } finally { + launchInFlightRef.current = false; + } + }, [ + authSession, + claimAndConnectSession, + effectiveStreamingBaseUrl, + refreshNavbarActiveSession, + selectedProvider, + settings, + streamStatus, + variantByGameId, + ]); + + const handleResumeFromNavbar = useCallback(async () => { + if (!selectedProvider || !navbarActiveSession || isResumingNavbarSession) { + return; + } + if (launchInFlightRef.current || streamStatus !== "idle") { + return; + } + + launchInFlightRef.current = true; + setIsResumingNavbarSession(true); + let loadingStep: StreamLoadingStatus = "setup"; + const updateLoadingStep = (next: StreamLoadingStatus): void => { + loadingStep = next; + setStreamStatus(next); + }; + + setLaunchError(null); + setQueuePosition(undefined); + setSessionStartedAtMs(Date.now()); + setSessionElapsedSeconds(0); + setStreamWarning(null); + updateLoadingStep("setup"); + + try { + await claimAndConnectSession(navbarActiveSession); + setNavbarActiveSession(null); + } catch (error) { + console.error("Navbar resume failed:", error); + setLaunchError(toLaunchErrorState(error, loadingStep)); + await window.openNow.disconnectSignaling().catch(() => {}); + clientRef.current?.dispose(); + clientRef.current = null; + setSession(null); + setStreamStatus("idle"); + setQueuePosition(undefined); + setSessionStartedAtMs(null); + setSessionElapsedSeconds(0); + setStreamWarning(null); + setEscHoldReleaseIndicator({ visible: false, progress: 0 }); + setDiagnostics(defaultDiagnostics()); + void refreshNavbarActiveSession(); + } finally { + launchInFlightRef.current = false; + setIsResumingNavbarSession(false); + } + }, [ + claimAndConnectSession, + isResumingNavbarSession, + navbarActiveSession, + refreshNavbarActiveSession, + selectedProvider, + streamStatus, + ]); + + // Stop stream handler + const handleStopStream = useCallback(async () => { + try { + resolveExitPrompt(false); + await window.openNow.disconnectSignaling(); + + const current = sessionRef.current; + if (current) { + const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; + await window.openNow.stopSession({ + token: token || undefined, + streamingBaseUrl: current.streamingBaseUrl, + serverIp: current.serverIp, + zone: current.zone, + sessionId: current.sessionId, + }); + } + + clientRef.current?.dispose(); + clientRef.current = null; + setSession(null); + setStreamStatus("idle"); + setStreamingGame(null); + setNavbarActiveSession(null); + setLaunchError(null); + setSessionStartedAtMs(null); + setSessionElapsedSeconds(0); + setStreamWarning(null); + setEscHoldReleaseIndicator({ visible: false, progress: 0 }); + setDiagnostics(defaultDiagnostics()); + void refreshNavbarActiveSession(); + } catch (error) { + console.error("Stop failed:", error); + } + }, [authSession, refreshNavbarActiveSession, resolveExitPrompt]); + + const handleDismissLaunchError = useCallback(async () => { + await window.openNow.disconnectSignaling().catch(() => {}); + clientRef.current?.dispose(); + clientRef.current = null; + setSession(null); + setLaunchError(null); + setStreamingGame(null); + setQueuePosition(undefined); + setSessionStartedAtMs(null); + setSessionElapsedSeconds(0); + setStreamWarning(null); + setEscHoldReleaseIndicator({ visible: false, progress: 0 }); + setDiagnostics(defaultDiagnostics()); + void refreshNavbarActiveSession(); + }, [refreshNavbarActiveSession]); + + const releasePointerLockIfNeeded = useCallback(async () => { + if (document.pointerLockElement) { + document.exitPointerLock(); + setEscHoldReleaseIndicator({ visible: false, progress: 0 }); + await sleep(75); + } + }, []); + + const handlePromptedStopStream = useCallback(async () => { + if (streamStatus === "idle") { + return; + } + + await releasePointerLockIfNeeded(); + + const gameName = (streamingGame?.title || "this game").trim(); + const shouldExit = await requestExitPrompt(gameName); + if (!shouldExit) { + return; + } + + await handleStopStream(); + }, [handleStopStream, releasePointerLockIfNeeded, requestExitPrompt, streamStatus, streamingGame?.title]); + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const target = e.target as HTMLElement | null; + const isTyping = !!target && ( + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable + ); + if (isTyping) { + return; + } + + if (exitPrompt.open) { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleExitPromptCancel(); + } else if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleExitPromptConfirm(); + } + return; + } + + const isPasteShortcut = e.key.toLowerCase() === "v" && !e.altKey && (isMac ? e.metaKey : e.ctrlKey); + if (streamStatus === "streaming" && isPasteShortcut) { + // Always stop local/browser paste behavior while streaming. + // If clipboard paste is enabled, send clipboard text into the stream. + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + if (settings.clipboardPaste) { + void (async () => { + const client = clientRef.current; + if (!client) return; + + try { + const text = await navigator.clipboard.readText(); + if (text && client.sendText(text) > 0) { + return; + } + } catch (error) { + console.warn("Clipboard read failed, falling back to paste shortcut:", error); + } + + client.sendPasteShortcut(isMac); + })(); + } + return; + } + + if (isShortcutMatch(e, shortcuts.toggleStats)) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + setShowStatsOverlay((prev) => !prev); + return; + } + + if (isShortcutMatch(e, shortcuts.togglePointerLock)) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + if (streamStatus === "streaming" && videoRef.current) { + if (document.pointerLockElement === videoRef.current) { + document.exitPointerLock(); + } else { + void requestEscLockedPointerCapture(videoRef.current); + } + } + return; + } + + if (isShortcutMatch(e, shortcuts.stopStream)) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + void handlePromptedStopStream(); + return; + } + + if (isShortcutMatch(e, shortcuts.toggleAntiAfk)) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + if (streamStatus === "streaming") { + setAntiAfkEnabled((prev) => !prev); + } + } + }; + + // Use capture phase so app shortcuts run before stream input capture listeners. + window.addEventListener("keydown", handleKeyDown, true); + return () => window.removeEventListener("keydown", handleKeyDown, true); + }, [ + exitPrompt.open, + handleExitPromptCancel, + handleExitPromptConfirm, + handlePromptedStopStream, + requestEscLockedPointerCapture, + settings.clipboardPaste, + shortcuts, + streamStatus, + ]); + + // Filter games by search + const filteredGames = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + if (!query) return games; + return games.filter((g) => g.title.toLowerCase().includes(query)); + }, [games, searchQuery]); + + const filteredLibraryGames = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + if (!query) return libraryGames; + return libraryGames.filter((g) => g.title.toLowerCase().includes(query)); + }, [libraryGames, searchQuery]); + + const gameTitleByAppId = useMemo(() => { + const titles = new Map(); + const allKnownGames = [...games, ...libraryGames]; + + for (const game of allKnownGames) { + const idsForGame = new Set(); + const launchId = parseNumericId(game.launchAppId); + if (launchId !== null) { + idsForGame.add(launchId); + } + for (const variant of game.variants) { + const variantId = parseNumericId(variant.id); + if (variantId !== null) { + idsForGame.add(variantId); + } + } + for (const appId of idsForGame) { + if (!titles.has(appId)) { + titles.set(appId, game.title); + } + } + } + + return titles; + }, [games, libraryGames]); + + const activeSessionGameTitle = useMemo(() => { + if (!navbarActiveSession) return null; + const mappedTitle = gameTitleByAppId.get(navbarActiveSession.appId); + if (mappedTitle) { + return mappedTitle; + } + if (session?.sessionId === navbarActiveSession.sessionId && streamingGame?.title) { + return streamingGame.title; + } + return null; + }, [gameTitleByAppId, navbarActiveSession, session?.sessionId, streamingGame?.title]); + + // Show login screen if not authenticated + if (!authSession) { + return ( + + ); + } + + const showLaunchOverlay = streamStatus !== "idle" || launchError !== null; + + // Show stream lifecycle (waiting/connecting/streaming/failure) + if (showLaunchOverlay) { + const loadingStatus = launchError ? launchError.stage : toLoadingStatus(streamStatus); + return ( + <> + {streamStatus !== "idle" && ( + { + if (document.fullscreenElement) { + document.exitFullscreen().catch(() => {}); + } else { + document.documentElement.requestFullscreen().catch(() => {}); + } + }} + onConfirmExit={handleExitPromptConfirm} + onCancelExit={handleExitPromptCancel} + onEndSession={() => { + void handlePromptedStopStream(); + }} + /> + )} + {streamStatus !== "streaming" && ( + { + if (launchError) { + void handleDismissLaunchError(); + return; + } + void handlePromptedStopStream(); + }} + /> + )} + + ); + } + + // Main app layout + return ( +
+ {startupRefreshNotice && ( +
+ {startupRefreshNotice.text} +
+ )} + { + void handleResumeFromNavbar(); + }} + onLogout={handleLogout} + /> + +
+ {currentPage === "home" && ( + + )} + + {currentPage === "library" && ( + + )} + + {currentPage === "settings" && ( + + )} +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/GameCard.tsx b/opennow-stable/src/renderer/src/components/GameCard.tsx new file mode 100644 index 0000000..5153fca --- /dev/null +++ b/opennow-stable/src/renderer/src/components/GameCard.tsx @@ -0,0 +1,209 @@ +import { Play, Monitor } from "lucide-react"; +import { memo } from "react"; +import type { JSX } from "react"; +import type { GameInfo, GameVariant } from "@shared/gfn"; + +interface GameCardProps { + game: GameInfo; + isSelected?: boolean; + onPlay: () => void; + onSelect: () => void; +} + +/* ── Official store brand icons (Simple Icons / MDI, viewBox 0 0 24 24) ── */ + +function SteamIcon(): JSX.Element { + return ( + + + + ); +} + +function EpicIcon(): JSX.Element { + return ( + + + + ); +} + +function UbisoftIcon(): JSX.Element { + return ( + + + + ); +} + +function EaIcon(): JSX.Element { + return ( + + + + ); +} + +function GogIcon(): JSX.Element { + return ( + + + + ); +} + +function XboxIcon(): JSX.Element { + return ( + + + + ); +} + +function BattleNetIcon(): JSX.Element { + return ( + + + + ); +} + +function DefaultStoreIcon(): JSX.Element { + return ( + + + + ); +} + +/* ── Store icon / name maps keyed by normalized uppercase store ID ── */ + +const STORE_ICON_MAP: Record JSX.Element> = { + STEAM: SteamIcon, + EPIC_GAMES_STORE: EpicIcon, + EPIC: EpicIcon, + EGS: EpicIcon, + UPLAY: UbisoftIcon, + UBISOFT: UbisoftIcon, + UBISOFT_CONNECT: UbisoftIcon, + EA_APP: EaIcon, + EA: EaIcon, + ORIGIN: EaIcon, + GOG_COM: GogIcon, + GOG: GogIcon, + XBOX_GAME_PASS: XboxIcon, + XBOX: XboxIcon, + MICROSOFT_STORE: XboxIcon, + MICROSOFT: XboxIcon, + BATTLE_NET: BattleNetIcon, + BATTLENET: BattleNetIcon, +}; + +const STORE_DISPLAY_NAME: Record = { + STEAM: "Steam", + EPIC_GAMES_STORE: "Epic", + EPIC: "Epic", + EGS: "Epic", + UPLAY: "Ubisoft", + UBISOFT: "Ubisoft", + UBISOFT_CONNECT: "Ubisoft", + EA_APP: "EA", + EA: "EA", + ORIGIN: "EA", + GOG_COM: "GOG", + GOG: "GOG", + XBOX_GAME_PASS: "Xbox", + XBOX: "Xbox", + MICROSOFT_STORE: "Xbox", + MICROSOFT: "Xbox", + BATTLE_NET: "Battle.net", + BATTLENET: "Battle.net", +}; + +/** Normalize an appStore value to the uppercase key used by the icon/name maps. */ +function normalizeStoreKey(raw: string): string { + return raw.toUpperCase().replace(/[\s-]+/g, "_"); +} + +function getUniqueStores(game: GameInfo): string[] { + const seen = new Set(); + const stores: string[] = []; + for (const v of game.variants) { + const key = normalizeStoreKey(v.store); + if (key !== "UNKNOWN" && key !== "NONE" && !seen.has(key)) { + seen.add(key); + stores.push(key); + } + } + return stores; +} + +export const GameCard = memo(function GameCard({ game, isSelected = false, onPlay, onSelect }: GameCardProps): JSX.Element { + const stores = getUniqueStores(game); + + const handlePlayClick = (event: React.MouseEvent): void => { + event.stopPropagation(); + onPlay(); + }; + + return ( +
{ + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onSelect(); + } + }} + role="button" + tabIndex={0} + aria-label={`Select ${game.title}`} + > +
+ {game.imageUrl ? ( + {game.title} + ) : ( +
+ +
+ )} + +
+
+ +
+
+ +
+

+ {game.title} +

+ {stores.length > 0 && ( +
+ {stores.map((store) => { + const IconComponent = STORE_ICON_MAP[store] ?? DefaultStoreIcon; + const displayName = STORE_DISPLAY_NAME[store] ?? store; + return ( + + + + ); + })} +
+ )} +
+
+ ); +}); diff --git a/opennow-stable/src/renderer/src/components/HomePage.tsx b/opennow-stable/src/renderer/src/components/HomePage.tsx new file mode 100644 index 0000000..9452882 --- /dev/null +++ b/opennow-stable/src/renderer/src/components/HomePage.tsx @@ -0,0 +1,103 @@ +import { Search, LayoutGrid, Globe, Loader2 } from "lucide-react"; +import type { JSX } from "react"; +import type { GameInfo } from "@shared/gfn"; +import { GameCard } from "./GameCard"; + +export interface HomePageProps { + games: GameInfo[]; + source: "main" | "library" | "public"; + onSourceChange: (source: "main" | "library" | "public") => void; + searchQuery: string; + onSearchChange: (query: string) => void; + onPlayGame: (game: GameInfo) => void; + isLoading: boolean; + selectedGameId: string; + onSelectGame: (id: string) => void; +} + +export function HomePage({ + games, + source, + onSourceChange, + searchQuery, + onSearchChange, + onPlayGame, + isLoading, + selectedGameId, + onSelectGame, +}: HomePageProps): JSX.Element { + const hasGames = games.length > 0; + + return ( +
+ {/* Top bar: tabs + search + count */} +
+
+ + +
+ +
+ + onSearchChange(e.target.value)} + /> +
+ + + {isLoading ? "Loading..." : `${games.length} game${games.length !== 1 ? "s" : ""}`} + +
+ + {/* Game grid */} +
+ {isLoading ? ( +
+ +

Loading games...

+
+ ) : !hasGames ? ( +
+ +

No games found

+

+ {searchQuery + ? "Try adjusting your search terms" + : "Check back later for new additions"} +

+
+ ) : ( +
+ {games.map((game, index) => ( + onSelectGame(game.id)} + onPlay={() => onPlayGame(game)} + /> + ))} +
+ )} +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/LibraryPage.tsx b/opennow-stable/src/renderer/src/components/LibraryPage.tsx new file mode 100644 index 0000000..eadb00e --- /dev/null +++ b/opennow-stable/src/renderer/src/components/LibraryPage.tsx @@ -0,0 +1,117 @@ +import { Library, Search, Clock, Gamepad2, Loader2 } from "lucide-react"; +import type { JSX } from "react"; +import type { GameInfo } from "@shared/gfn"; +import { GameCard } from "./GameCard"; + +export interface LibraryPageProps { + games: GameInfo[]; + searchQuery: string; + onSearchChange: (query: string) => void; + onPlayGame: (game: GameInfo) => void; + isLoading: boolean; + selectedGameId: string; + onSelectGame: (id: string) => void; +} + +function formatLastPlayed(date?: string): string { + if (!date) return "Never played"; + + const lastPlayed = new Date(date); + const now = new Date(); + const diffMs = now.getTime() - lastPlayed.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return "Just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + + return lastPlayed.toLocaleDateString(); +} + +export function LibraryPage({ + games, + searchQuery, + onSearchChange, + onPlayGame, + isLoading, + selectedGameId, + onSelectGame, +}: LibraryPageProps): JSX.Element { + const filteredGames = searchQuery.trim() + ? games.filter((game) => + game.title.toLowerCase().includes(searchQuery.trim().toLowerCase()) + ) + : games; + + return ( +
+ {/* Toolbar: title + search + count */} +
+
+ +

My Library

+
+ +
+ + onSearchChange(e.target.value)} + placeholder="Search your library..." + className="library-search-input" + /> +
+ + {games.length} game{games.length !== 1 ? "s" : ""} +
+ + {/* Game grid */} +
+ {isLoading ? ( +
+ +

Loading your library...

+
+ ) : games.length === 0 ? ( +
+ +

Your library is empty

+

Games you own will appear here. Browse the catalog to find games.

+
+ ) : filteredGames.length === 0 ? ( +
+ +

No results

+

No games match “{searchQuery}”

+
+ ) : ( +
+ {filteredGames.map((game, index) => ( +
+ onSelectGame(game.id)} + onPlay={() => onPlayGame(game)} + /> + {/* @ts-expect-error - lastPlayed may exist on library games */} + {game.lastPlayed && ( +
+ + {/* @ts-expect-error - lastPlayed may exist on library games */} + {formatLastPlayed(game.lastPlayed)} +
+ )} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/LoginScreen.tsx b/opennow-stable/src/renderer/src/components/LoginScreen.tsx new file mode 100644 index 0000000..a1a78fb --- /dev/null +++ b/opennow-stable/src/renderer/src/components/LoginScreen.tsx @@ -0,0 +1,150 @@ +import { useState, useRef, useEffect } from "react"; +import type { JSX } from "react"; +import { LogIn, ChevronDown, Zap } from "lucide-react"; +import type { LoginProvider } from "@shared/gfn"; + +export interface LoginScreenProps { + providers: LoginProvider[]; + selectedProviderId: string; + onProviderChange: (id: string) => void; + onLogin: () => void; + isLoading: boolean; + error: string | null; + isInitializing?: boolean; + statusMessage?: string; +} + +export function LoginScreen({ + providers, + selectedProviderId, + onProviderChange, + onLogin, + isLoading, + error, + isInitializing = false, + statusMessage, +}: LoginScreenProps): JSX.Element { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + const selectedProvider = providers.find((p) => p.idpId === selectedProviderId); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleProviderSelect = (providerId: string) => { + onProviderChange(providerId); + setIsDropdownOpen(false); + }; + + return ( +
+
+
+
+
+
+
+ +
+ {/* Brand */} +
+
+ +
+ OpenNOW +
+ + {/* Card */} +
+
+

Sign in

+

Cloud gaming, open source.

+
+ + {error && ( +
+ + {error} +
+ )} + + {isInitializing && statusMessage && ( +
+ + {statusMessage} +
+ )} + +
+ + + + {isDropdownOpen && ( +
+ {providers.map((provider) => ( + + ))} +
+ )} +
+ + +
+ +

Open-source cloud gaming client

+
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/Navbar.tsx b/opennow-stable/src/renderer/src/components/Navbar.tsx new file mode 100644 index 0000000..2b000fa --- /dev/null +++ b/opennow-stable/src/renderer/src/components/Navbar.tsx @@ -0,0 +1,351 @@ +import type { ActiveSessionInfo, AuthUser, SubscriptionInfo } from "@shared/gfn"; +import { House, Library, Settings, User, LogOut, Zap, Timer, HardDrive, X, Loader2, PlayCircle } from "lucide-react"; +import { useEffect, useState, type JSX } from "react"; +import { createPortal } from "react-dom"; + +interface NavbarProps { + currentPage: "home" | "library" | "settings"; + onNavigate: (page: "home" | "library" | "settings") => void; + user: AuthUser | null; + subscription: SubscriptionInfo | null; + activeSession: ActiveSessionInfo | null; + activeSessionGameTitle: string | null; + isResumingSession: boolean; + onResumeSession: () => void; + onLogout: () => void; +} + +type NavbarModalType = "time" | "storage" | null; + +function getTierDisplay(tier: string): { label: string; className: string } { + const t = tier.toUpperCase(); + if (t === "ULTIMATE") return { label: "Ultimate", className: "tier-ultimate" }; + if (t === "PRIORITY" || t === "PERFORMANCE") return { label: "Priority", className: "tier-priority" }; + return { label: "Free", className: "tier-free" }; +} + +export function Navbar({ + currentPage, + onNavigate, + user, + subscription, + activeSession, + activeSessionGameTitle, + isResumingSession, + onResumeSession, + onLogout, +}: NavbarProps): JSX.Element { + const [modalType, setModalType] = useState(null); + + const navItems = [ + { id: "home" as const, label: "Store", icon: House }, + { id: "library" as const, label: "Library", icon: Library }, + { id: "settings" as const, label: "Settings", icon: Settings }, + ]; + + const tierInfo = user ? getTierDisplay(user.membershipTier) : null; + const formatHours = (value: number): string => { + if (!Number.isFinite(value)) return "0"; + const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10; + return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1); + }; + const formatGb = (value: number): string => { + if (!Number.isFinite(value)) return "0"; + return Number.isInteger(value) ? String(value) : value.toFixed(1); + }; + const formatPercent = (value: number): string => { + if (!Number.isFinite(value)) return "0%"; + const rounded = Math.max(0, Math.min(100, Math.round(value))); + return `${rounded}%`; + }; + const formatDateTime = (value: string | undefined): string | null => { + if (!value) return null; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return null; + return parsed.toLocaleString(); + }; + const clamp = (value: number): number => Math.min(1, Math.max(0, value)); + const toneByLeftRatio = (ratio: number): "good" | "warn" | "critical" => { + if (ratio <= 0.15) return "critical"; + if (ratio <= 0.4) return "warn"; + return "good"; + }; + + const timeTotal = subscription?.totalHours ?? 0; + const timeLeft = subscription?.remainingHours ?? 0; + const timeUsed = subscription?.usedHours ?? Math.max(timeTotal - timeLeft, 0); + const allottedHours = subscription?.allottedHours ?? 0; + const purchasedHours = subscription?.purchasedHours ?? 0; + const rolledOverHours = subscription?.rolledOverHours ?? 0; + const timeUsedRatio = + subscription && !subscription.isUnlimited && timeTotal > 0 ? clamp(timeUsed / timeTotal) : 0; + const timeLeftRatio = + subscription && !subscription.isUnlimited && timeTotal > 0 ? clamp(timeLeft / timeTotal) : 1; + const timeTone: "good" | "warn" | "critical" = subscription?.isUnlimited + ? "good" + : toneByLeftRatio(timeLeftRatio); + const timeLabel = subscription + ? subscription.isUnlimited + ? "Unlimited time" + : `${formatHours(timeLeft)}h left` + : null; + + const storageTotal = subscription?.storageAddon?.sizeGb; + const storageUsed = subscription?.storageAddon?.usedGb; + const storageHasData = storageTotal !== undefined && storageUsed !== undefined; + const storageLeft = + storageHasData + ? Math.max(storageTotal - storageUsed, 0) + : undefined; + const storageUsedRatio = + storageHasData && storageTotal > 0 ? clamp(storageUsed / storageTotal) : 0; + const storageLeftRatio = + storageHasData && storageTotal > 0 ? clamp((storageLeft ?? 0) / storageTotal) : 1; + const storageTone = toneByLeftRatio(storageLeftRatio); + const storageLabel = + storageHasData + ? `${formatGb(storageLeft ?? 0)} GB left` + : storageTotal !== undefined + ? `${formatGb(storageTotal)} GB total` + : null; + + const spanStart = formatDateTime(subscription?.currentSpanStartDateTime); + const spanEnd = formatDateTime(subscription?.currentSpanEndDateTime); + const firstEntitlementStart = formatDateTime(subscription?.firstEntitlementStartDateTime); + const modalTitle = modalType === "time" ? "Playtime Details" : "Storage Details"; + const activeSessionTitle = activeSessionGameTitle?.trim() || null; + + useEffect(() => { + if (!modalType) return; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setModalType(null); + } + }; + window.addEventListener("keydown", onKeyDown); + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + window.removeEventListener("keydown", onKeyDown); + document.body.style.overflow = previousOverflow; + }; + }, [modalType]); + + const modal = modalType && subscription + ? createPortal( +
setModalType(null)}> +
event.stopPropagation()} + > +
+

{modalTitle}

+ +
+ + {modalType === "time" && ( +
+ {!subscription.isUnlimited && timeTotal > 0 && ( +
+
+ Time Usage + {formatPercent(timeUsedRatio * 100)} used +
+
+ +
+
+ {formatHours(timeUsed)}h used + {formatHours(timeLeft)}h left +
+
+ )} +
Tier{subscription.membershipTier}
+ {subscription.subscriptionType && ( +
Type{subscription.subscriptionType}
+ )} + {subscription.subscriptionSubType && ( +
Sub Type{subscription.subscriptionSubType}
+ )} +
Time Left{subscription.isUnlimited ? "Unlimited" : `${formatHours(timeLeft)}h`}
+
Total Time{subscription.isUnlimited ? "Unlimited" : `${formatHours(timeTotal)}h`}
+
Used Time{formatHours(timeUsed)}h
+
Allotted{formatHours(allottedHours)}h
+
Purchased{formatHours(purchasedHours)}h
+
Rolled Over{formatHours(rolledOverHours)}h
+ {firstEntitlementStart && ( +
First Entitlement{firstEntitlementStart}
+ )} + {spanStart &&
Period Start{spanStart}
} + {spanEnd &&
Period End{spanEnd}
} + {subscription.notifyUserWhenTimeRemainingInMinutes !== undefined && ( +
Notify At (General){subscription.notifyUserWhenTimeRemainingInMinutes} min
+ )} + {subscription.notifyUserOnSessionWhenRemainingTimeInMinutes !== undefined && ( +
Notify At (In Session){subscription.notifyUserOnSessionWhenRemainingTimeInMinutes} min
+ )} + {subscription.state &&
Plan State{subscription.state}
} + {subscription.isGamePlayAllowed !== undefined && ( +
Gameplay Allowed{subscription.isGamePlayAllowed ? "Yes" : "No"}
+ )} +
+ )} + + {modalType === "storage" && ( +
+ {storageHasData && ( +
+
+ Storage Usage + {formatPercent(storageUsedRatio * 100)} used +
+
+ +
+
+ {formatGb(storageUsed ?? 0)} GB used + {formatGb(storageLeft ?? 0)} GB left +
+
+ )} +
Storage Left{storageLeft !== undefined ? `${formatGb(storageLeft)} GB` : "N/A"}
+
Storage Used{storageUsed !== undefined ? `${formatGb(storageUsed)} GB` : "N/A"}
+
Storage Total{storageTotal !== undefined ? `${formatGb(storageTotal)} GB` : "N/A"}
+ {subscription.storageAddon?.regionName && ( +
Storage Region{subscription.storageAddon.regionName}
+ )} + {subscription.storageAddon?.regionCode && ( +
Storage Region Code{subscription.storageAddon.regionCode}
+ )} + {subscription.serverRegionId && ( +
Server Region (VPC){subscription.serverRegionId}
+ )} +
+ )} +
+
, + document.body, + ) + : null; + + return ( + + ); +} diff --git a/opennow-stable/src/renderer/src/components/SettingsPage.tsx b/opennow-stable/src/renderer/src/components/SettingsPage.tsx new file mode 100644 index 0000000..f072170 --- /dev/null +++ b/opennow-stable/src/renderer/src/components/SettingsPage.tsx @@ -0,0 +1,1123 @@ +import { Monitor, Volume2, Mouse, Settings2, Globe, Save, Check, Search, X, Loader, Cpu, Zap } from "lucide-react"; +import { useState, useCallback, useMemo, useEffect } from "react"; +import type { JSX } from "react"; + +import type { + Settings, + StreamRegion, + VideoCodec, + ColorQuality, + EntitledResolution, + VideoAccelerationPreference, +} from "@shared/gfn"; +import { colorQualityRequiresHevc } from "@shared/gfn"; +import { formatShortcutForDisplay, normalizeShortcut } from "../shortcuts"; + +interface SettingsPageProps { + settings: Settings; + regions: StreamRegion[]; + onSettingChange: (key: K, value: Settings[K]) => void; +} + +const codecOptions: VideoCodec[] = ["H264", "H265", "AV1"]; + +const accelerationOptions: { value: VideoAccelerationPreference; label: string }[] = [ + { value: "auto", label: "Auto" }, + { value: "hardware", label: "Hardware" }, + { value: "software", label: "Software (CPU)" }, +]; + +const colorQualityOptions: { value: ColorQuality; label: string; description: string }[] = [ + { value: "8bit_420", label: "8-bit 4:2:0", description: "Most compatible" }, + { value: "8bit_444", label: "8-bit 4:4:4", description: "Better color" }, + { value: "10bit_420", label: "10-bit 4:2:0", description: "HDR ready" }, + { value: "10bit_444", label: "10-bit 4:4:4", description: "Best quality" }, +]; + +/* ── Static fallbacks (used when MES API is unavailable) ─────────── */ + +interface ResolutionPreset { + value: string; + label: string; +} + +interface FpsPreset { + value: number; +} + +const STATIC_RESOLUTION_PRESETS: ResolutionPreset[] = [ + { value: "1280x720", label: "720p" }, + { value: "1920x1080", label: "1080p" }, + { value: "2560x1440", label: "1440p" }, + { value: "3840x2160", label: "4K" }, + { value: "2560x1080", label: "Ultrawide 1080p" }, + { value: "3440x1440", label: "Ultrawide 1440p" }, + { value: "5120x1440", label: "Super Ultrawide" }, +]; + +const STATIC_FPS_PRESETS: FpsPreset[] = [ + { value: 30 }, + { value: 60 }, + { value: 90 }, + { value: 120 }, + { value: 144 }, + { value: 165 }, + { value: 240 }, + { value: 360 }, +]; + +const isMac = navigator.platform.toLowerCase().includes("mac"); +const shortcutExamples = "Examples: F3, Ctrl+Shift+Q, Ctrl+Shift+K"; +const shortcutDefaults = { + shortcutToggleStats: "F3", + shortcutTogglePointerLock: "F8", + shortcutStopStream: "Ctrl+Shift+Q", + shortcutToggleAntiAfk: "Ctrl+Shift+K", +} as const; + +/* ── Aspect ratio helpers ─────────────────────────────────────────── */ + +const ASPECT_RATIO_ORDER = [ + "16:9 Standard", + "16:10 Widescreen", + "21:9 Ultrawide", + "32:9 Super Ultrawide", + "4:3 Legacy", + "Other", +] as const; + +function classifyAspectRatio(width: number, height: number): string { + const ratio = width / height; + if (Math.abs(ratio - 16 / 9) < 0.05) return "16:9 Standard"; + if (Math.abs(ratio - 16 / 10) < 0.05) return "16:10 Widescreen"; + if (Math.abs(ratio - 21 / 9) < 0.05) return "21:9 Ultrawide"; + if (Math.abs(ratio - 32 / 9) < 0.05) return "32:9 Super Ultrawide"; + if (Math.abs(ratio - 4 / 3) < 0.05) return "4:3 Legacy"; + return "Other"; +} + +function friendlyResolutionName(width: number, height: number): string { + if (width === 1280 && height === 720) return "720p (HD)"; + if (width === 1920 && height === 1080) return "1080p (FHD)"; + if (width === 2560 && height === 1440) return "1440p (QHD)"; + if (width === 3840 && height === 2160) return "4K (UHD)"; + if (width === 2560 && height === 1080) return "2560x1080 (UW)"; + if (width === 3440 && height === 1440) return "3440x1440 (UW)"; + if (width === 5120 && height === 1440) return "5120x1440 (SUW)"; + return `${width}x${height}`; +} + +interface ResolutionGroup { + category: string; + resolutions: { width: number; height: number; value: string; label: string }[]; +} + +function groupResolutions(entitled: EntitledResolution[]): ResolutionGroup[] { + // Deduplicate by (width, height) + const seen = new Set(); + const unique: { width: number; height: number }[] = []; + // Sort by width desc, height desc + const sorted = [...entitled].sort((a, b) => b.width - a.width || b.height - a.height); + for (const res of sorted) { + const key = `${res.width}x${res.height}`; + if (seen.has(key)) continue; + seen.add(key); + unique.push(res); + } + + // Group by aspect ratio + const groupMap = new Map(); + for (const res of unique) { + const cat = classifyAspectRatio(res.width, res.height); + const value = `${res.width}x${res.height}`; + const label = friendlyResolutionName(res.width, res.height); + if (!groupMap.has(cat)) groupMap.set(cat, []); + groupMap.get(cat)!.push({ width: res.width, height: res.height, value, label }); + } + + // Return in canonical order + const result: ResolutionGroup[] = []; + for (const cat of ASPECT_RATIO_ORDER) { + const items = groupMap.get(cat); + if (items && items.length > 0) { + result.push({ category: cat, resolutions: items }); + } + } + return result; +} + +function getFpsForResolution(entitled: EntitledResolution[], resolution: string): number[] { + const parts = resolution.split("x"); + const w = parseInt(parts[0], 10); + const h = parseInt(parts[1], 10); + + let fpsList = entitled + .filter((r) => r.width === w && r.height === h) + .map((r) => r.fps); + + // Fallback: if no exact match, collect all FPS from all resolutions + if (fpsList.length === 0) { + fpsList = entitled.map((r) => r.fps); + } + + // Deduplicate and sort ascending + return [...new Set(fpsList)].sort((a, b) => a - b); +} + +/* ── Codec diagnostics ────────────────────────────────────────────── */ + +interface CodecTestResult { + codec: string; + /** Whether WebRTC can negotiate this codec at all */ + webrtcSupported: boolean; + /** Whether MediaCapabilities reports decode support */ + decodeSupported: boolean; + /** Whether MediaCapabilities says HW-accelerated (powerEfficient) */ + hwAccelerated: boolean; + /** Whether encode is supported */ + encodeSupported: boolean; + /** Whether encode is HW-accelerated */ + encodeHwAccelerated: boolean; + /** Human-readable decode method (e.g. "D3D11", "VAAPI", "VideoToolbox", "Software") */ + decodeVia: string; + /** Human-readable encode method */ + encodeVia: string; + /** Profiles found in WebRTC capabilities */ + profiles: string[]; +} + +/** Map of codec name to MediaCapabilities contentType and profile strings */ +const CODEC_TEST_CONFIGS: { + name: string; + webrtcMime: string; + decodeContentType: string; + encodeContentType: string; + profiles: { label: string; contentType: string }[]; +}[] = [ + { + name: "H264", + webrtcMime: "video/H264", + decodeContentType: "video/mp4; codecs=\"avc1.42E01E\"", + encodeContentType: "video/mp4; codecs=\"avc1.42E01E\"", + profiles: [ + { label: "Baseline", contentType: "video/mp4; codecs=\"avc1.42E01E\"" }, + { label: "Main", contentType: "video/mp4; codecs=\"avc1.4D401E\"" }, + { label: "High", contentType: "video/mp4; codecs=\"avc1.64001E\"" }, + ], + }, + { + name: "H265", + webrtcMime: "video/H265", + decodeContentType: "video/mp4; codecs=\"hev1.1.6.L93.B0\"", + encodeContentType: "video/mp4; codecs=\"hev1.1.6.L93.B0\"", + profiles: [ + { label: "Main", contentType: "video/mp4; codecs=\"hev1.1.6.L93.B0\"" }, + { label: "Main 10", contentType: "video/mp4; codecs=\"hev1.2.4.L93.B0\"" }, + ], + }, + { + name: "AV1", + webrtcMime: "video/AV1", + decodeContentType: "video/mp4; codecs=\"av01.0.08M.08\"", + encodeContentType: "video/mp4; codecs=\"av01.0.08M.08\"", + profiles: [ + { label: "Main 8-bit", contentType: "video/mp4; codecs=\"av01.0.08M.08\"" }, + { label: "Main 10-bit", contentType: "video/mp4; codecs=\"av01.0.08M.10\"" }, + ], + }, +]; + +const CODEC_TEST_RESULTS_STORAGE_KEY = "opennow.codec-test-results.v1"; +const ENTITLED_RESOLUTIONS_STORAGE_KEY = "opennow.entitled-resolutions.v1"; + +interface EntitledResolutionsCache { + userId: string; + entitledResolutions: EntitledResolution[]; +} + +function loadStoredCodecResults(): CodecTestResult[] | null { + try { + const raw = window.sessionStorage.getItem(CODEC_TEST_RESULTS_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as unknown; + if (!Array.isArray(parsed)) return null; + return parsed as CodecTestResult[]; + } catch { + return null; + } +} + +function loadCachedEntitledResolutions(): EntitledResolutionsCache | null { + try { + const raw = window.sessionStorage.getItem(ENTITLED_RESOLUTIONS_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + if (!parsed || typeof parsed.userId !== "string" || !Array.isArray(parsed.entitledResolutions)) { + return null; + } + return { + userId: parsed.userId, + entitledResolutions: parsed.entitledResolutions, + }; + } catch { + return null; + } +} + +function saveCachedEntitledResolutions(cache: EntitledResolutionsCache): void { + try { + window.sessionStorage.setItem(ENTITLED_RESOLUTIONS_STORAGE_KEY, JSON.stringify(cache)); + } catch { + // Ignore storage failures + } +} + +function guessDecodeBackend(hwAccelerated: boolean): string { + if (!hwAccelerated) return "Software (CPU)"; + const platform = navigator.platform?.toLowerCase() ?? ""; + const ua = navigator.userAgent?.toLowerCase() ?? ""; + if (platform.includes("win") || ua.includes("windows")) return "D3D11 (GPU)"; + if (platform.includes("mac") || ua.includes("macintosh")) return "VideoToolbox (GPU)"; + if (platform.includes("linux") || ua.includes("linux")) return "VA-API (GPU)"; + return "Hardware (GPU)"; +} + +function guessEncodeBackend(hwAccelerated: boolean): string { + if (!hwAccelerated) return "Software (CPU)"; + const platform = navigator.platform?.toLowerCase() ?? ""; + const ua = navigator.userAgent?.toLowerCase() ?? ""; + if (platform.includes("win") || ua.includes("windows")) return "Media Foundation (GPU)"; + if (platform.includes("mac") || ua.includes("macintosh")) return "VideoToolbox (GPU)"; + if (platform.includes("linux") || ua.includes("linux")) return "VA-API (GPU)"; + return "Hardware (GPU)"; +} + +async function testCodecSupport(): Promise { + const results: CodecTestResult[] = []; + + // Get WebRTC receiver capabilities once + const webrtcCaps = RTCRtpReceiver.getCapabilities?.("video"); + const webrtcCodecMimes = new Set( + webrtcCaps?.codecs.map((c) => c.mimeType.toLowerCase()) ?? [], + ); + + // Collect WebRTC profiles per codec + const webrtcProfiles = new Map(); + if (webrtcCaps) { + for (const c of webrtcCaps.codecs) { + const mime = c.mimeType.toLowerCase(); + const sdpLine = (c as unknown as Record).sdpFmtpLine ?? ""; + if (!mime.includes("rtx") && !mime.includes("red") && !mime.includes("ulpfec")) { + const existing = webrtcProfiles.get(mime) ?? []; + if (sdpLine) existing.push(sdpLine); + webrtcProfiles.set(mime, existing); + } + } + } + + for (const config of CODEC_TEST_CONFIGS) { + const webrtcSupported = webrtcCodecMimes.has(config.webrtcMime.toLowerCase()); + const profiles = webrtcProfiles.get(config.webrtcMime.toLowerCase()) ?? []; + + // Test decode via MediaCapabilities API + let decodeSupported = false; + let hwAccelerated = false; + try { + const decodeResult = await navigator.mediaCapabilities.decodingInfo({ + type: "webrtc", + video: { + contentType: config.webrtcMime === "video/H265" ? "video/h265" : config.webrtcMime.toLowerCase(), + width: 1920, + height: 1080, + framerate: 60, + bitrate: 20_000_000, + }, + }); + decodeSupported = decodeResult.supported; + hwAccelerated = decodeResult.powerEfficient; + } catch { + // webrtc type may not be supported, fall back to file type + try { + const decodeResult = await navigator.mediaCapabilities.decodingInfo({ + type: "file", + video: { + contentType: config.decodeContentType, + width: 1920, + height: 1080, + framerate: 60, + bitrate: 20_000_000, + }, + }); + decodeSupported = decodeResult.supported; + hwAccelerated = decodeResult.powerEfficient; + } catch { + // Codec not recognized at all + } + } + + // Test encode via MediaCapabilities API + let encodeSupported = false; + let encodeHwAccelerated = false; + try { + const encodeResult = await navigator.mediaCapabilities.encodingInfo({ + type: "webrtc", + video: { + contentType: config.webrtcMime === "video/H265" ? "video/h265" : config.webrtcMime.toLowerCase(), + width: 1920, + height: 1080, + framerate: 60, + bitrate: 20_000_000, + }, + }); + encodeSupported = encodeResult.supported; + encodeHwAccelerated = encodeResult.powerEfficient; + } catch { + try { + const encodeResult = await navigator.mediaCapabilities.encodingInfo({ + type: "record", + video: { + contentType: config.encodeContentType, + width: 1920, + height: 1080, + framerate: 60, + bitrate: 20_000_000, + }, + }); + encodeSupported = encodeResult.supported; + encodeHwAccelerated = encodeResult.powerEfficient; + } catch { + // Codec not recognized at all + } + } + + results.push({ + codec: config.name, + webrtcSupported, + decodeSupported: decodeSupported || webrtcSupported, // WebRTC support implies decode + hwAccelerated, + encodeSupported, + encodeHwAccelerated, + decodeVia: (decodeSupported || webrtcSupported) + ? guessDecodeBackend(hwAccelerated) + : "Unsupported", + encodeVia: encodeSupported + ? guessEncodeBackend(encodeHwAccelerated) + : "Unsupported", + profiles, + }); + } + + return results; +} + +/* ── Component ────────────────────────────────────────────────────── */ + +export function SettingsPage({ settings, regions, onSettingChange }: SettingsPageProps): JSX.Element { + const [savedIndicator, setSavedIndicator] = useState(false); + const [regionSearch, setRegionSearch] = useState(""); + const [regionDropdownOpen, setRegionDropdownOpen] = useState(false); + + // Codec diagnostics + const initialCodecResults = useMemo(() => loadStoredCodecResults(), []); + const [codecResults, setCodecResults] = useState(initialCodecResults); + const [codecTesting, setCodecTesting] = useState(false); + const [codecTestOpen, setCodecTestOpen] = useState(() => initialCodecResults !== null); + const platformHardwareLabel = useMemo(() => { + const platform = navigator.platform.toLowerCase(); + if (platform.includes("win")) return "D3D11 / DXVA"; + if (platform.includes("mac")) return "VideoToolbox"; + if (platform.includes("linux")) return "VA-API"; + return "Hardware"; + }, []); + + const runCodecTest = useCallback(async () => { + setCodecTesting(true); + setCodecTestOpen(true); + try { + const results = await testCodecSupport(); + setCodecResults(results); + } catch (err) { + console.error("Codec test failed:", err); + } finally { + setCodecTesting(false); + } + }, []); + + useEffect(() => { + try { + if (codecResults && codecResults.length > 0) { + window.sessionStorage.setItem(CODEC_TEST_RESULTS_STORAGE_KEY, JSON.stringify(codecResults)); + } else { + window.sessionStorage.removeItem(CODEC_TEST_RESULTS_STORAGE_KEY); + } + } catch { + // Ignore storage failures (private mode / denied storage) + } + }, [codecResults]); + + const [toggleStatsInput, setToggleStatsInput] = useState(settings.shortcutToggleStats); + const [togglePointerLockInput, setTogglePointerLockInput] = useState(settings.shortcutTogglePointerLock); + const [stopStreamInput, setStopStreamInput] = useState(settings.shortcutStopStream); + const [toggleAntiAfkInput, setToggleAntiAfkInput] = useState(settings.shortcutToggleAntiAfk); + const [toggleStatsError, setToggleStatsError] = useState(false); + const [togglePointerLockError, setTogglePointerLockError] = useState(false); + const [stopStreamError, setStopStreamError] = useState(false); + const [toggleAntiAfkError, setToggleAntiAfkError] = useState(false); + + // Dynamic entitled resolutions from MES API + const [entitledResolutions, setEntitledResolutions] = useState([]); + const [subscriptionLoading, setSubscriptionLoading] = useState(true); + + useEffect(() => { + setToggleStatsInput(settings.shortcutToggleStats); + }, [settings.shortcutToggleStats]); + + useEffect(() => { + setTogglePointerLockInput(settings.shortcutTogglePointerLock); + }, [settings.shortcutTogglePointerLock]); + + useEffect(() => { + setStopStreamInput(settings.shortcutStopStream); + }, [settings.shortcutStopStream]); + + useEffect(() => { + setToggleAntiAfkInput(settings.shortcutToggleAntiAfk); + }, [settings.shortcutToggleAntiAfk]); + + // Fetch subscription data (cached per account; reload only when account changes) + useEffect(() => { + let cancelled = false; + + async function load(): Promise { + try { + const sessionResult = await window.openNow.getAuthSession(); + const session = sessionResult.session; + if (!session || cancelled) { + setEntitledResolutions([]); + setSubscriptionLoading(false); + return; + } + + const userId = session.user.userId; + const cached = loadCachedEntitledResolutions(); + if (cached && cached.userId === userId) { + setEntitledResolutions(cached.entitledResolutions); + setSubscriptionLoading(false); + return; + } + + const sub = await window.openNow.fetchSubscription({ + userId, + }); + + if (!cancelled) { + setEntitledResolutions(sub.entitledResolutions); + saveCachedEntitledResolutions({ + userId, + entitledResolutions: sub.entitledResolutions, + }); + } + } catch (err) { + console.warn("Failed to fetch subscription for settings:", err); + } finally { + if (!cancelled) setSubscriptionLoading(false); + } + } + + load(); + return () => { cancelled = true; }; + }, []); + + const hasDynamic = entitledResolutions.length > 0; + + // Grouped resolution presets (dynamic) + const resolutionGroups = useMemo( + () => (hasDynamic ? groupResolutions(entitledResolutions) : []), + [entitledResolutions, hasDynamic] + ); + + // Dynamic FPS presets based on current resolution + const dynamicFpsOptions = useMemo( + () => (hasDynamic ? getFpsForResolution(entitledResolutions, settings.resolution) : []), + [entitledResolutions, settings.resolution, hasDynamic] + ); + + const handleChange = useCallback( + (key: K, value: Settings[K]) => { + onSettingChange(key, value); + setSavedIndicator(true); + setTimeout(() => setSavedIndicator(false), 1500); + }, + [onSettingChange] + ); + + /** Change color quality, auto-switching codec to H265 if the mode requires HEVC */ + const handleColorQualityChange = useCallback( + (cq: ColorQuality) => { + handleChange("colorQuality", cq); + if (colorQualityRequiresHevc(cq) && settings.codec === "H264") { + handleChange("codec", "H265"); + } + }, + [handleChange, settings.codec] + ); + + const filteredRegions = useMemo(() => { + if (!regionSearch.trim()) return regions; + const q = regionSearch.trim().toLowerCase(); + return regions.filter((r) => r.name.toLowerCase().includes(q)); + }, [regions, regionSearch]); + + const selectedRegionName = useMemo(() => { + if (!settings.region) return "Auto (Best)"; + const found = regions.find((r) => r.url === settings.region); + return found?.name ?? settings.region; + }, [settings.region, regions]); + + const handleShortcutBlur = ( + key: K, + rawValue: string, + setInput: (value: string) => void, + setError: (value: boolean) => void + ): void => { + const normalized = normalizeShortcut(rawValue.trim()); + if (!normalized.valid) { + setError(true); + return; + } + setError(false); + setInput(normalized.canonical); + if (settings[key] !== normalized.canonical) { + handleChange(key, normalized.canonical as Settings[K]); + } + }; + + const handleShortcutKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + (e.target as HTMLInputElement).blur(); + } + }; + + const areShortcutsDefault = useMemo( + () => + settings.shortcutToggleStats === shortcutDefaults.shortcutToggleStats + && settings.shortcutTogglePointerLock === shortcutDefaults.shortcutTogglePointerLock + && settings.shortcutStopStream === shortcutDefaults.shortcutStopStream + && settings.shortcutToggleAntiAfk === shortcutDefaults.shortcutToggleAntiAfk, + [ + settings.shortcutToggleStats, + settings.shortcutTogglePointerLock, + settings.shortcutStopStream, + settings.shortcutToggleAntiAfk, + ] + ); + + const handleResetShortcuts = useCallback(() => { + setToggleStatsInput(shortcutDefaults.shortcutToggleStats); + setTogglePointerLockInput(shortcutDefaults.shortcutTogglePointerLock); + setStopStreamInput(shortcutDefaults.shortcutStopStream); + setToggleAntiAfkInput(shortcutDefaults.shortcutToggleAntiAfk); + setToggleStatsError(false); + setTogglePointerLockError(false); + setStopStreamError(false); + setToggleAntiAfkError(false); + + const shortcutKeys = [ + "shortcutToggleStats", + "shortcutTogglePointerLock", + "shortcutStopStream", + "shortcutToggleAntiAfk", + ] as const; + + for (const key of shortcutKeys) { + const value = shortcutDefaults[key]; + if (settings[key] !== value) { + handleChange(key, value); + } + } + }, [handleChange, settings]); + + return ( +
+
+ +

Settings

+
+ + Saved +
+
+ +
+ {/* ── Video ──────────────────────────────────────── */} +
+
+ +

Video

+
+ +
+ {/* Resolution — dynamic or static chips */} +
+ + + {hasDynamic ? ( +
+ {resolutionGroups.map((group) => ( +
+ {group.category} +
+ {group.resolutions.map((res) => ( + + ))} +
+
+ ))} +
+ ) : ( +
+ {STATIC_RESOLUTION_PRESETS.map((preset) => ( + + ))} +
+ )} +
+ + {/* FPS — dynamic or static chips */} +
+ +
+ {(hasDynamic ? dynamicFpsOptions.map((v) => ({ value: v })) : STATIC_FPS_PRESETS).map( + (preset) => ( + + ) + )} +
+
+ + {/* Codec */} +
+ +
+ {codecOptions.map((codec) => ( + + ))} +
+
+ + {/* Decoder preference */} +
+ +
+ {accelerationOptions.map((option) => ( + + ))} +
+ Applies after app restart. +
+ + {/* Encoder preference */} +
+ +
+ {accelerationOptions.map((option) => ( + + ))} +
+ Applies after app restart. +
+ + {/* Color Quality */} +
+ +
+ {colorQualityOptions.map((opt) => { + const needsHevc = colorQualityRequiresHevc(opt.value); + return ( + + ); + })} +
+ {colorQualityRequiresHevc(settings.colorQuality) && settings.codec === "H264" && ( + This mode requires H265 or AV1. Codec will be auto-switched. + )} +
+ + {/* Bitrate slider */} +
+
+ + {settings.maxBitrateMbps} Mbps +
+ handleChange("maxBitrateMbps", parseInt(e.target.value, 10))} + /> +
+
+
+ + {/* ── Codec Diagnostics ──────────────────────────── */} +
+
+ +

Codec Diagnostics

+
+
+
+ + +
+ + {codecTestOpen && codecResults && ( +
+ {codecResults.map((result) => ( +
+
+ {result.codec} + + {result.webrtcSupported ? "WebRTC Ready" : "Not in WebRTC"} + +
+ +
+ {/* Decode row */} +
+ Decode + + {result.decodeSupported + ? result.hwAccelerated + ? "GPU" + : "CPU" + : "No"} + + {result.decodeVia} +
+ + {/* Encode row */} +
+ Encode + + {result.encodeSupported + ? result.encodeHwAccelerated + ? "GPU" + : "CPU" + : "No"} + + {result.encodeVia} +
+
+ + {/* Profiles */} + {result.profiles.length > 0 && ( +
+ Profiles: +
+ {result.profiles.map((p, i) => ( + {p} + ))} +
+
+ )} +
+ ))} +
+ )} +
+
+ + {/* ── Audio ──────────────────────────────────────── */} +
+
+ +

Audio

+
+
+
Audio configuration coming soon
+
+
+ + {/* ── Input ──────────────────────────────────────── */} +
+
+ +

Input

+
+
+
+ + +
+ +
+
+ +
+ Editable + +
+
+ +
+ + + + + + + +
+ + {(toggleStatsError || togglePointerLockError || stopStreamError || toggleAntiAfkError) && ( + + Invalid shortcut. Use {shortcutExamples} + + )} + + {!toggleStatsError && !togglePointerLockError && !stopStreamError && !toggleAntiAfkError && ( + + {shortcutExamples}. Current stop shortcut: {formatShortcutForDisplay(settings.shortcutStopStream, isMac)}. + + )} +
+
+
+ + {/* ── Region ────────────────────────────────────── */} +
+
+ +

Region

+
+
+ {/* Region selector with search */} +
+ + + {regionDropdownOpen && ( +
+
+ + setRegionSearch(e.target.value)} + autoFocus + /> + {regionSearch && ( + + )} +
+ +
+ + + {filteredRegions.map((region) => ( + + ))} + + {filteredRegions.length === 0 && regions.length > 0 && ( +
No regions match “{regionSearch}”
+ )} +
+
+ )} +
+
+
+
+ + {/* Footer */} +
+ +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/StatsOverlay.tsx b/opennow-stable/src/renderer/src/components/StatsOverlay.tsx new file mode 100644 index 0000000..eac695a --- /dev/null +++ b/opennow-stable/src/renderer/src/components/StatsOverlay.tsx @@ -0,0 +1,100 @@ +import { Monitor, Wifi, Activity, Gamepad2, AlertTriangle } from "lucide-react"; +import type { StreamDiagnostics } from "../gfn/webrtcClient"; +import type { JSX } from "react"; + +interface StatsOverlayProps { + stats: StreamDiagnostics; + isVisible: boolean; + serverRegion?: string; + connectedControllers: number; +} + +function getRttColor(rttMs: number): string { + if (rttMs <= 0) return "var(--ink-muted)"; + if (rttMs < 30) return "var(--success)"; + if (rttMs < 60) return "var(--warning)"; + return "var(--error)"; +} + +function formatBitrate(kbps: number): string { + if (kbps >= 1000) return `${(kbps / 1000).toFixed(1)} Mbps`; + return `${kbps.toFixed(0)} kbps`; +} + +export function StatsOverlay({ + stats, + isVisible, + serverRegion, + connectedControllers, +}: StatsOverlayProps): JSX.Element | null { + if (!isVisible) return null; + + const rttColor = getRttColor(stats.rttMs); + const showPacketLoss = stats.packetLossPercent > 0; + const hasData = stats.resolution !== "" || stats.bitrateKbps > 0; + + if (!hasData) { + return ( +
+
+ Connecting... +
+
+ ); + } + + return ( +
+
+ {/* Resolution & FPS */} +
+ + {stats.resolution} @ {stats.decodeFps} FPS +
+ + {/* Bitrate */} +
+ + {formatBitrate(stats.bitrateKbps)} +
+ + {/* RTT / Latency */} +
+ + + {stats.rttMs > 0 ? `${stats.rttMs.toFixed(0)}ms` : "-- ms"} + +
+ + {/* Codec */} + {stats.codec && ( +
+ {stats.codec} + {stats.isHdr && HDR} +
+ )} + + {/* Packet Loss */} + {showPacketLoss && ( +
+ + {stats.packetLossPercent.toFixed(1)}% loss +
+ )} + + {/* Controller Status */} + {connectedControllers > 0 && ( +
+ + {connectedControllers} +
+ )} + + {/* Server Region */} + {serverRegion && ( +
{serverRegion}
+ )} +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/StreamLoading.tsx b/opennow-stable/src/renderer/src/components/StreamLoading.tsx new file mode 100644 index 0000000..4480258 --- /dev/null +++ b/opennow-stable/src/renderer/src/components/StreamLoading.tsx @@ -0,0 +1,159 @@ +import { Loader2, Monitor, Cpu, Wifi, X, XCircle } from "lucide-react"; +import type { JSX } from "react"; + +export interface StreamLoadingProps { + gameTitle: string; + gameCover?: string; + status: "queue" | "setup" | "starting" | "connecting"; + queuePosition?: number; + estimatedWait?: string; + error?: { + title: string; + description: string; + code?: string; + }; + onCancel: () => void; +} + +const steps = [ + { id: "queue", label: "Queue", icon: Monitor }, + { id: "setup", label: "Setup", icon: Cpu }, + { id: "ready", label: "Ready", icon: Wifi }, +] as const; + +function getStatusMessage( + status: StreamLoadingProps["status"], + queuePosition?: number, + isError = false, +): string { + if (isError) { + return "Game launch failed"; + } + switch (status) { + case "queue": + return queuePosition ? `Position #${queuePosition} in queue` : "Waiting in queue..."; + case "setup": + return "Setting up your gaming rig..."; + case "starting": + return "Starting stream..."; + case "connecting": + return "Connecting to server..."; + default: + return "Loading..."; + } +} + +function getActiveStepIndex(status: StreamLoadingProps["status"]): number { + switch (status) { + case "queue": + return 0; + case "setup": + return 1; + case "starting": + case "connecting": + return 2; + default: + return 0; + } +} + +export function StreamLoading({ + gameTitle, + gameCover, + status, + queuePosition, + estimatedWait, + error, + onCancel, +}: StreamLoadingProps): JSX.Element { + const hasError = Boolean(error); + const activeStepIndex = getActiveStepIndex(status); + const statusMessage = getStatusMessage(status, queuePosition, hasError); + + return ( +
+
+ + {/* Animated accent glow behind content */} +
+ +
+ {/* Game Info Header */} +
+
+ {gameCover ? ( + {gameTitle} + ) : ( +
+ +
+ )} +
+
+
+ {hasError ? "Launch Error" : "Now Loading"} +

+ {gameTitle} +

+
+
+ + {/* Progress Steps */} +
+ {steps.map((step, index) => { + const StepIcon = step.icon; + const isFailed = hasError && index === activeStepIndex; + const isActive = !isFailed && index === activeStepIndex; + const isCompleted = index < activeStepIndex; + const isPending = index > activeStepIndex; + const nextIsFailed = hasError && index + 1 === activeStepIndex; + + return ( +
+
+ {isFailed ? : } +
+ {step.label} + {index < steps.length - 1 && ( +
+
+
+ )} +
+ ); + })} +
+ + {/* Status Display */} +
+ {hasError ? : } +
+

{statusMessage}

+ {hasError && error && ( + <> +

{error.title}

+

{error.description}

+ {error.code &&

{error.code}

} + + )} + {status === "queue" && queuePosition !== undefined && queuePosition > 0 && ( +

+ Position #{queuePosition} + {estimatedWait && · ~{estimatedWait}} +

+ )} +
+
+ + {/* Cancel */} + +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/StreamView.tsx b/opennow-stable/src/renderer/src/components/StreamView.tsx new file mode 100644 index 0000000..3140bed --- /dev/null +++ b/opennow-stable/src/renderer/src/components/StreamView.tsx @@ -0,0 +1,350 @@ +import { useState, useEffect, useCallback } from "react"; +import type { JSX } from "react"; +import { Maximize, Minimize, Gamepad2, Loader2, LogOut, Clock3, AlertTriangle } from "lucide-react"; +import type { StreamDiagnostics } from "../gfn/webrtcClient"; + +interface StreamViewProps { + videoRef: React.Ref; + audioRef: React.Ref; + stats: StreamDiagnostics; + showStats: boolean; + shortcuts: { + toggleStats: string; + togglePointerLock: string; + stopStream: string; + }; + serverRegion?: string; + connectedControllers: number; + antiAfkEnabled: boolean; + escHoldReleaseIndicator: { + visible: boolean; + progress: number; + }; + exitPrompt: { + open: boolean; + gameTitle: string; + }; + sessionElapsedSeconds: number; + streamWarning: { + code: 1 | 2 | 3; + message: string; + tone: "warn" | "critical"; + secondsLeft?: number; + } | null; + isConnecting: boolean; + gameTitle: string; + onToggleFullscreen: () => void; + onConfirmExit: () => void; + onCancelExit: () => void; + onEndSession: () => void; +} + +function getRttColor(rttMs: number): string { + if (rttMs <= 0) return "var(--ink-muted)"; + if (rttMs < 30) return "var(--success)"; + if (rttMs < 60) return "var(--warning)"; + return "var(--error)"; +} + +function getPacketLossColor(lossPercent: number): string { + if (lossPercent <= 0.15) return "var(--success)"; + if (lossPercent < 1) return "var(--warning)"; + return "var(--error)"; +} + +function getTimingColor(valueMs: number, goodMax: number, warningMax: number): string { + if (valueMs <= 0) return "var(--ink-muted)"; + if (valueMs <= goodMax) return "var(--success)"; + if (valueMs <= warningMax) return "var(--warning)"; + return "var(--error)"; +} + +function getInputQueueColor(bufferedBytes: number, dropCount: number): string { + if (dropCount > 0 || bufferedBytes >= 65536) return "var(--error)"; + if (bufferedBytes >= 32768) return "var(--warning)"; + return "var(--success)"; +} + +function formatElapsed(totalSeconds: number): string { + const safe = Math.max(0, Math.floor(totalSeconds)); + const hours = Math.floor(safe / 3600); + const minutes = Math.floor((safe % 3600) / 60); + const seconds = safe % 60; + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + } + return `${minutes}:${seconds.toString().padStart(2, "0")}`; +} + +function formatWarningSeconds(value: number | undefined): string | null { + if (value === undefined || !Number.isFinite(value) || value < 0) { + return null; + } + const total = Math.floor(value); + const minutes = Math.floor(total / 60); + const seconds = total % 60; + if (minutes > 0) { + return `${minutes}m ${seconds.toString().padStart(2, "0")}s`; + } + return `${seconds}s`; +} + +export function StreamView({ + videoRef, + audioRef, + stats, + showStats, + shortcuts, + serverRegion, + connectedControllers, + antiAfkEnabled, + escHoldReleaseIndicator, + exitPrompt, + sessionElapsedSeconds, + streamWarning, + isConnecting, + gameTitle, + onToggleFullscreen, + onConfirmExit, + onCancelExit, + onEndSession, +}: StreamViewProps): JSX.Element { + const [isFullscreen, setIsFullscreen] = useState(false); + const [showHints, setShowHints] = useState(true); + + const handleFullscreenToggle = useCallback(() => { + onToggleFullscreen(); + }, [onToggleFullscreen]); + + useEffect(() => { + const timer = setTimeout(() => setShowHints(false), 5000); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); + }, []); + + const bitrateMbps = (stats.bitrateKbps / 1000).toFixed(1); + const hasResolution = stats.resolution && stats.resolution !== ""; + const hasCodec = stats.codec && stats.codec !== ""; + const regionLabel = stats.serverRegion || serverRegion || ""; + const decodeColor = getTimingColor(stats.decodeTimeMs, 8, 16); + const renderColor = getTimingColor(stats.renderTimeMs, 12, 22); + const jitterBufferColor = getTimingColor(stats.jitterBufferDelayMs, 10, 24); + const lossColor = getPacketLossColor(stats.packetLossPercent); + const dText = stats.decodeTimeMs > 0 ? `${stats.decodeTimeMs.toFixed(1)}ms` : "--"; + const rText = stats.renderTimeMs > 0 ? `${stats.renderTimeMs.toFixed(1)}ms` : "--"; + const jbText = stats.jitterBufferDelayMs > 0 ? `${stats.jitterBufferDelayMs.toFixed(1)}ms` : "--"; + const inputLive = stats.inputReady && stats.connectionState === "connected"; + const escHoldProgress = Math.max(0, Math.min(1, escHoldReleaseIndicator.progress)); + const escHoldSecondsLeft = Math.max(0, 5 - Math.floor(escHoldProgress * 5)); + const inputQueueColor = getInputQueueColor(stats.inputQueueBufferedBytes, stats.inputQueueDropCount); + const inputQueueText = `${(stats.inputQueueBufferedBytes / 1024).toFixed(1)}KB`; + const warningSeconds = formatWarningSeconds(streamWarning?.secondsLeft); + const sessionTimeText = formatElapsed(sessionElapsedSeconds); + + return ( +
+ {/* Video element */} +
+ ); +} diff --git a/opennow-stable/src/renderer/src/gfn/inputProtocol.ts b/opennow-stable/src/renderer/src/gfn/inputProtocol.ts new file mode 100644 index 0000000..1e326ff --- /dev/null +++ b/opennow-stable/src/renderer/src/gfn/inputProtocol.ts @@ -0,0 +1,687 @@ +export const INPUT_HEARTBEAT = 2; +export const INPUT_KEY_DOWN = 3; +export const INPUT_KEY_UP = 4; +export const INPUT_MOUSE_REL = 7; +export const INPUT_MOUSE_BUTTON_DOWN = 8; +export const INPUT_MOUSE_BUTTON_UP = 9; +export const INPUT_MOUSE_WHEEL = 10; +export const INPUT_GAMEPAD = 12; + +// Mouse button constants (1-based for GFN protocol) +// GFN uses: 1=Left, 2=Middle, 3=Right, 4=Back, 5=Forward +export const MOUSE_LEFT = 1; +export const MOUSE_MIDDLE = 2; +export const MOUSE_RIGHT = 3; +export const MOUSE_BACK = 4; +export const MOUSE_FORWARD = 5; + +// XInput button flags (matching Windows XINPUT_GAMEPAD_* constants) +export const GAMEPAD_DPAD_UP = 0x0001; +export const GAMEPAD_DPAD_DOWN = 0x0002; +export const GAMEPAD_DPAD_LEFT = 0x0004; +export const GAMEPAD_DPAD_RIGHT = 0x0008; +export const GAMEPAD_START = 0x0010; +export const GAMEPAD_BACK = 0x0020; +export const GAMEPAD_LS = 0x0040; // Left stick click (L3) +export const GAMEPAD_RS = 0x0080; // Right stick click (R3) +export const GAMEPAD_LB = 0x0100; // Left bumper +export const GAMEPAD_RB = 0x0200; // Right bumper +export const GAMEPAD_GUIDE = 0x0400; // Xbox/Guide button +export const GAMEPAD_A = 0x1000; +export const GAMEPAD_B = 0x2000; +export const GAMEPAD_X = 0x4000; +export const GAMEPAD_Y = 0x8000; + +// Axis indices for gamepad +export const GAMEPAD_AXIS_LX = 0; // Left stick X +export const GAMEPAD_AXIS_LY = 1; // Left stick Y +export const GAMEPAD_AXIS_RX = 2; // Right stick X +export const GAMEPAD_AXIS_RY = 3; // Right stick Y +export const GAMEPAD_AXIS_LT = 4; // Left trigger +export const GAMEPAD_AXIS_RT = 5; // Right trigger + +// Gamepad constants +export const GAMEPAD_MAX_CONTROLLERS = 4; +export const GAMEPAD_PACKET_SIZE = 38; +export const GAMEPAD_DEADZONE = 0.15; // 15% radial deadzone + +export interface KeyboardPayload { + keycode: number; + scancode: number; + modifiers: number; + timestampUs: bigint; +} + +export interface MouseMovePayload { + dx: number; + dy: number; + timestampUs: bigint; +} + +export interface MouseButtonPayload { + button: number; + timestampUs: bigint; +} + +export interface MouseWheelPayload { + delta: number; + timestampUs: bigint; +} + +export interface GamepadInput { + controllerId: number; // 0-3 + buttons: number; // 16-bit button flags + leftTrigger: number; // 0-255 + rightTrigger: number; // 0-255 + leftStickX: number; // -32768 to 32767 + leftStickY: number; // -32768 to 32767 (inverted in XInput) + rightStickX: number; // -32768 to 32767 + rightStickY: number; // -32768 to 32767 (inverted in XInput) + connected: boolean; // true = connected, false = disconnected + timestampUs: bigint; +} + +const codeMap: Record = { + // Letters + KeyA: { vk: 0x41, scancode: 0x04 }, + KeyB: { vk: 0x42, scancode: 0x05 }, + KeyC: { vk: 0x43, scancode: 0x06 }, + KeyD: { vk: 0x44, scancode: 0x07 }, + KeyE: { vk: 0x45, scancode: 0x08 }, + KeyF: { vk: 0x46, scancode: 0x09 }, + KeyG: { vk: 0x47, scancode: 0x0a }, + KeyH: { vk: 0x48, scancode: 0x0b }, + KeyI: { vk: 0x49, scancode: 0x0c }, + KeyJ: { vk: 0x4a, scancode: 0x0d }, + KeyK: { vk: 0x4b, scancode: 0x0e }, + KeyL: { vk: 0x4c, scancode: 0x0f }, + KeyM: { vk: 0x4d, scancode: 0x10 }, + KeyN: { vk: 0x4e, scancode: 0x11 }, + KeyO: { vk: 0x4f, scancode: 0x12 }, + KeyP: { vk: 0x50, scancode: 0x13 }, + KeyQ: { vk: 0x51, scancode: 0x14 }, + KeyR: { vk: 0x52, scancode: 0x15 }, + KeyS: { vk: 0x53, scancode: 0x16 }, + KeyT: { vk: 0x54, scancode: 0x17 }, + KeyU: { vk: 0x55, scancode: 0x18 }, + KeyV: { vk: 0x56, scancode: 0x19 }, + KeyW: { vk: 0x57, scancode: 0x1a }, + KeyX: { vk: 0x58, scancode: 0x1b }, + KeyY: { vk: 0x59, scancode: 0x1c }, + KeyZ: { vk: 0x5a, scancode: 0x1d }, + // Numbers + Digit1: { vk: 0x31, scancode: 0x1e }, + Digit2: { vk: 0x32, scancode: 0x1f }, + Digit3: { vk: 0x33, scancode: 0x20 }, + Digit4: { vk: 0x34, scancode: 0x21 }, + Digit5: { vk: 0x35, scancode: 0x22 }, + Digit6: { vk: 0x36, scancode: 0x23 }, + Digit7: { vk: 0x37, scancode: 0x24 }, + Digit8: { vk: 0x38, scancode: 0x25 }, + Digit9: { vk: 0x39, scancode: 0x26 }, + Digit0: { vk: 0x30, scancode: 0x27 }, + // Special keys + Enter: { vk: 0x0d, scancode: 0x28 }, + Escape: { vk: 0x1b, scancode: 0x29 }, + Backspace: { vk: 0x08, scancode: 0x2a }, + Tab: { vk: 0x09, scancode: 0x2b }, + Space: { vk: 0x20, scancode: 0x2c }, + // Punctuation + Minus: { vk: 0xbd, scancode: 0x2d }, + Equal: { vk: 0xbb, scancode: 0x2e }, + BracketLeft: { vk: 0xdb, scancode: 0x2f }, + BracketRight: { vk: 0xdd, scancode: 0x30 }, + Backslash: { vk: 0xdc, scancode: 0x31 }, + Semicolon: { vk: 0xba, scancode: 0x33 }, + Quote: { vk: 0xde, scancode: 0x34 }, + Backquote: { vk: 0xc0, scancode: 0x35 }, + Comma: { vk: 0xbc, scancode: 0x36 }, + Period: { vk: 0xbe, scancode: 0x37 }, + Slash: { vk: 0xbf, scancode: 0x38 }, + // Function keys + F1: { vk: 0x70, scancode: 0x3a }, + F2: { vk: 0x71, scancode: 0x3b }, + F3: { vk: 0x72, scancode: 0x3c }, + F4: { vk: 0x73, scancode: 0x3d }, + F5: { vk: 0x74, scancode: 0x3e }, + F6: { vk: 0x75, scancode: 0x3f }, + F7: { vk: 0x76, scancode: 0x40 }, + F8: { vk: 0x77, scancode: 0x41 }, + F9: { vk: 0x78, scancode: 0x42 }, + F10: { vk: 0x79, scancode: 0x43 }, + F11: { vk: 0x7a, scancode: 0x44 }, + F12: { vk: 0x7b, scancode: 0x45 }, + F13: { vk: 0x7c, scancode: 0x64 }, + // Navigation keys + ArrowRight: { vk: 0x27, scancode: 0x4f }, + ArrowLeft: { vk: 0x25, scancode: 0x50 }, + ArrowDown: { vk: 0x28, scancode: 0x51 }, + ArrowUp: { vk: 0x26, scancode: 0x52 }, + // Modifier keys + ControlLeft: { vk: 0xa2, scancode: 0xe0 }, + ShiftLeft: { vk: 0xa0, scancode: 0xe1 }, + AltLeft: { vk: 0xa4, scancode: 0xe2 }, + MetaLeft: { vk: 0x5b, scancode: 0xe3 }, + ControlRight: { vk: 0xa3, scancode: 0xe4 }, + ShiftRight: { vk: 0xa1, scancode: 0xe5 }, + AltRight: { vk: 0xa5, scancode: 0xe6 }, + MetaRight: { vk: 0x5c, scancode: 0xe7 }, + // Caps Lock and Num Lock + CapsLock: { vk: 0x14, scancode: 0x39 }, + NumLock: { vk: 0x90, scancode: 0x53 }, + // Navigation cluster + Insert: { vk: 0x2d, scancode: 0x49 }, + Delete: { vk: 0x2e, scancode: 0x4c }, + Home: { vk: 0x24, scancode: 0x4a }, + End: { vk: 0x23, scancode: 0x4d }, + PageUp: { vk: 0x21, scancode: 0x4b }, + PageDown: { vk: 0x22, scancode: 0x4e }, + // System keys + PrintScreen: { vk: 0x2c, scancode: 0x46 }, + ScrollLock: { vk: 0x91, scancode: 0x47 }, + Pause: { vk: 0x13, scancode: 0x48 }, + // Context Menu key + ContextMenu: { vk: 0x5d, scancode: 0x65 }, + // Numpad keys + Numpad0: { vk: 0x60, scancode: 0x62 }, + Numpad1: { vk: 0x61, scancode: 0x59 }, + Numpad2: { vk: 0x62, scancode: 0x5a }, + Numpad3: { vk: 0x63, scancode: 0x5b }, + Numpad4: { vk: 0x64, scancode: 0x5c }, + Numpad5: { vk: 0x65, scancode: 0x5d }, + Numpad6: { vk: 0x66, scancode: 0x5e }, + Numpad7: { vk: 0x67, scancode: 0x5f }, + Numpad8: { vk: 0x68, scancode: 0x60 }, + Numpad9: { vk: 0x69, scancode: 0x61 }, + NumpadAdd: { vk: 0x6b, scancode: 0x57 }, + NumpadSubtract: { vk: 0x6d, scancode: 0x56 }, + NumpadMultiply: { vk: 0x6a, scancode: 0x55 }, + NumpadDivide: { vk: 0x6f, scancode: 0x54 }, + NumpadDecimal: { vk: 0x6e, scancode: 0x63 }, + NumpadEnter: { vk: 0x0d, scancode: 0x58 }, +}; + +const keyFallbackMap: Record = { + Escape: { vk: 0x1b, scancode: 0x29 }, + Esc: { vk: 0x1b, scancode: 0x29 }, +}; + +/** + * Write an 8-byte big-endian timestamp (performance.now() * 1000 = microseconds) + * into a DataView at the given offset. Matches official GFN client's _r() function. + */ +function writeTimestamp(view: DataView, offset: number): void { + const tsUs = performance.now() * 1000; + const lo = Math.floor(tsUs) & 0xFFFFFFFF; + const hi = Math.floor(tsUs / 4294967296); + view.setUint32(offset, hi, false); // high 32 bits, big-endian + view.setUint32(offset + 4, lo, false); // low 32 bits, big-endian +} + +/** + * Protocol v3+ wrapper for SINGLE non-mouse events (keyboard, mouse button, wheel). + * Format: [0x23][8B timestamp][0x22][payload] + * + * 0x23 = outer timestamp wrapper (added by yc() in official client) + * 0x22 = single-event sub-message marker (added by Ec() allocator in official client) + * + * For protocol v1-v2, returns the raw payload unchanged. + */ +function wrapSingleEvent(payload: Uint8Array, protocolVersion: number): Uint8Array { + if (protocolVersion <= 2) { + return payload; + } + // [0x23][8B timestamp][0x22][payload] + const wrapped = new Uint8Array(9 + 1 + payload.length); + const view = new DataView(wrapped.buffer); + wrapped[0] = 0x23; + writeTimestamp(view, 1); + wrapped[9] = 0x22; // single-event sub-message marker + wrapped.set(payload, 10); + return wrapped; +} + +/** + * Protocol v3+ wrapper for MOUSE MOVE events. + * Format: [0x23][8B timestamp][0x21][2B event-length][payload] + * + * 0x23 = outer timestamp wrapper + * 0x21 = mouse/cursor event marker (used by Tc() coalescer in official client) + * 2B = payload length (BE uint16) — official client's Wa() with no endian param = BE + * + * For protocol v1-v2, returns the raw payload unchanged. + */ +function wrapMouseMoveEvent(payload: Uint8Array, protocolVersion: number): Uint8Array { + if (protocolVersion <= 2) { + return payload; + } + // [0x23][8B timestamp][0x21][2B length][payload] + const wrapped = new Uint8Array(9 + 1 + 2 + payload.length); + const view = new DataView(wrapped.buffer); + wrapped[0] = 0x23; + writeTimestamp(view, 1); + wrapped[9] = 0x21; // mouse/cursor event marker + view.setUint16(10, payload.length, false); // event length (BE, matches official setUint16) + wrapped.set(payload, 12); + return wrapped; +} + +/** + * Protocol v3+ wrapper for GAMEPAD events on the RELIABLE channel. + * Format: [0x23][8B timestamp][0x21][2B size BE][payload] + * + * Official GFN client's ul() with m=false writes [0x21][2B size] then yc() prepends [0x23][8B ts]. + * Gamepad goes through the same batching system as other events. + * + * For protocol v1-v2, returns the raw payload unchanged. + */ +function wrapGamepadReliable(payload: Uint8Array, protocolVersion: number): Uint8Array { + if (protocolVersion <= 2) { + return payload; + } + // [0x23][8B timestamp][0x21][2B size][payload] + const wrapped = new Uint8Array(9 + 1 + 2 + payload.length); + const view = new DataView(wrapped.buffer); + wrapped[0] = 0x23; + writeTimestamp(view, 1); + wrapped[9] = 0x21; // batched event marker (m=false path in ul()) + view.setUint16(10, payload.length, false); // size (BE, Wa() with no endian param) + wrapped.set(payload, 12); + return wrapped; +} + +/** + * Protocol v3+ wrapper for GAMEPAD events on the PARTIALLY RELIABLE channel. + * Format: [0x23][8B timestamp][0x26][1B gamepadIdx][2B seqNum BE][0x21][2B size BE][payload] + * + * Official GFN client's ul() adds [0x26][idx][seq] header when gamepad index is specified + * (partially reliable path), then [0x21][2B size], then yc() prepends [0x23][8B ts]. + * + * 0x26 = 38 decimal, PR sequence header byte (written by Va(38) in ul()) + * + * For protocol v1-v2, returns the raw payload unchanged. + */ +function wrapGamepadPartiallyReliable( + payload: Uint8Array, + protocolVersion: number, + gamepadIndex: number, + sequenceNumber: number, +): Uint8Array { + if (protocolVersion <= 2) { + return payload; + } + // [0x23][8B ts][0x26][1B idx][2B seq][0x21][2B size][payload] + const wrapped = new Uint8Array(9 + 1 + 1 + 2 + 1 + 2 + payload.length); + const view = new DataView(wrapped.buffer); + wrapped[0] = 0x23; + writeTimestamp(view, 1); + wrapped[9] = 0x26; // PR sequence header (decimal 38, written by Va(38)) + wrapped[10] = gamepadIndex & 0xFF; // gamepad index byte + view.setUint16(11, sequenceNumber, false); // sequence number (BE, Wa() with no endian param) + wrapped[13] = 0x21; // batched event marker + view.setUint16(14, payload.length, false); // size (BE) + wrapped.set(payload, 16); + return wrapped; +} + +export class InputEncoder { + private protocolVersion = 2; + // Per-gamepad sequence numbers for partially reliable channel framing. + // Official GFN client tracks this per-gamepad-index via this.tc Map. + private gamepadSequence: Map = new Map(); + + setProtocolVersion(version: number): void { + this.protocolVersion = version; + } + + /** Get and increment the sequence number for a gamepad on the PR channel. + * Wraps at 65536 (uint16 range), matching official client's cl() function. */ + getNextGamepadSequence(gamepadIndex: number): number { + const current = this.gamepadSequence.get(gamepadIndex) ?? 1; + this.gamepadSequence.set(gamepadIndex, (current + 1) % 65536); + return current; + } + + resetGamepadSequences(): void { + this.gamepadSequence.clear(); + } + + encodeHeartbeat(): Uint8Array { + // Heartbeat is sent RAW — no v3 wrapper. + // Official GFN client's Jc() sends [u32 LE = 2] directly, no 0x23/0x22 prefix. + const payload = new Uint8Array(4); + const view = new DataView(payload.buffer); + view.setUint32(0, INPUT_HEARTBEAT, true); + return payload; + } + + encodeKeyDown(payload: KeyboardPayload): Uint8Array { + return this.encodeKey(INPUT_KEY_DOWN, payload); + } + + encodeKeyUp(payload: KeyboardPayload): Uint8Array { + return this.encodeKey(INPUT_KEY_UP, payload); + } + + encodeMouseMove(payload: MouseMovePayload): Uint8Array { + const bytes = new Uint8Array(22); + const view = new DataView(bytes.buffer); + // [type 4B LE][dx 2B BE][dy 2B BE][reserved 6B BE][timestamp 8B BE] + view.setUint32(0, INPUT_MOUSE_REL, true); // type: LE + view.setInt16(4, payload.dx, false); // dx: BE + view.setInt16(6, payload.dy, false); // dy: BE + view.setUint16(8, 0, false); // reserved: BE + view.setUint32(10, 0, false); // reserved: BE + view.setBigUint64(14, payload.timestampUs, false); // timestamp: BE + return wrapMouseMoveEvent(bytes, this.protocolVersion); + } + + encodeMouseButtonDown(payload: MouseButtonPayload): Uint8Array { + return this.encodeMouseButton(INPUT_MOUSE_BUTTON_DOWN, payload); + } + + encodeMouseButtonUp(payload: MouseButtonPayload): Uint8Array { + return this.encodeMouseButton(INPUT_MOUSE_BUTTON_UP, payload); + } + + encodeMouseWheel(payload: MouseWheelPayload): Uint8Array { + const bytes = new Uint8Array(22); + const view = new DataView(bytes.buffer); + // [type 4B LE][horiz 2B BE][vert 2B BE][reserved 6B BE][timestamp 8B BE] + view.setUint32(0, INPUT_MOUSE_WHEEL, true); // type: LE + view.setInt16(4, 0, false); // horizontal: BE + view.setInt16(6, payload.delta, false); // vertical: BE + view.setUint16(8, 0, false); // reserved: BE + view.setUint32(10, 0, false); // reserved: BE + view.setBigUint64(14, payload.timestampUs, false); // timestamp: BE + return wrapSingleEvent(bytes, this.protocolVersion); + } + + encodeGamepadState(payload: GamepadInput, bitmap: number, usePartiallyReliable: boolean): Uint8Array { + const bytes = new Uint8Array(GAMEPAD_PACKET_SIZE); + const view = new DataView(bytes.buffer); + + // Match official GFN client's gl() function exactly (vendor_beautified.js line 13469-13470): + // gl(i, u, m, w, P, L, $=0, ae=0) where: + // i=DataView, u=base offset (0), m=gamepad index, w=buttons, + // P=triggers, L=axes[4], $=timestamp, ae=bitmap + + // Offset 0x00: Type (u32 LE) - event type 12 + view.setUint32(0, INPUT_GAMEPAD, true); + + // Offset 0x04: Payload size (u16 LE) = 26 + view.setUint16(4, 26, true); + + // Offset 0x06: Gamepad index (u16 LE) + view.setUint16(6, payload.controllerId & 0x03, true); + + // Offset 0x08: Bitmap (u16 LE) — NOT a simple connected flag! + // Official client uses a bitmask: bit i = gamepad i connected, bit (i+8) = additional state. + // Passed as the `ae` parameter in gl() from the gamepad manager's this.nu field. + view.setUint16(8, bitmap, true); + + // Offset 0x0A: Inner payload size (u16 LE) = 20 + view.setUint16(10, 20, true); + + // Offset 0x0C: Button flags (u16 LE) - XInput format + view.setUint16(12, payload.buttons, true); + + // Offset 0x0E: Packed triggers (u16 LE: low byte=LT, high byte=RT) + const packedTriggers = (payload.leftTrigger & 0xFF) | ((payload.rightTrigger & 0xFF) << 8); + view.setUint16(14, packedTriggers, true); + + // Offset 0x10: Left stick X (i16 LE) + view.setInt16(16, payload.leftStickX, true); + + // Offset 0x12: Left stick Y (i16 LE) + view.setInt16(18, payload.leftStickY, true); + + // Offset 0x14: Right stick X (i16 LE) + view.setInt16(20, payload.rightStickX, true); + + // Offset 0x16: Right stick Y (i16 LE) + view.setInt16(22, payload.rightStickY, true); + + // Offset 0x18: Reserved (u16 LE) = 0 + view.setUint16(24, 0, true); + + // Offset 0x1A: Magic constant (u16 LE) = 85 (0x55) + view.setUint16(26, 85, true); + + // Offset 0x1C: Reserved (u16 LE) = 0 + view.setUint16(28, 0, true); + + // Offset 0x1E: Timestamp (u64 LE) + view.setBigUint64(30, payload.timestampUs, true); + + // Gamepad packets ARE wrapped in protocol v3+ — the official client's yc() function + // applies the 0x23 wrapper for ALL channels (the v2+ check does NOT exclude PR). + // The batching system also adds 0x21 inner framing. + if (usePartiallyReliable) { + // PR channel: [0x23][8B ts][0x26][1B idx][2B seq][0x21][2B size][38B payload] + const seq = this.getNextGamepadSequence(payload.controllerId); + return wrapGamepadPartiallyReliable(bytes, this.protocolVersion, payload.controllerId, seq); + } + // Reliable channel: [0x23][8B ts][0x21][2B size][38B payload] + return wrapGamepadReliable(bytes, this.protocolVersion); + } + + private encodeKey(type: number, payload: KeyboardPayload): Uint8Array { + const bytes = new Uint8Array(18); + const view = new DataView(bytes.buffer); + // [type 4B LE][keycode 2B BE][modifiers 2B BE][scancode 2B BE][timestamp 8B BE] + view.setUint32(0, type, true); // type: LE + view.setUint16(4, payload.keycode, false); // keycode: BE + view.setUint16(6, payload.modifiers, false); // modifiers: BE + view.setUint16(8, payload.scancode, false); // scancode: BE + view.setBigUint64(10, payload.timestampUs, false); // timestamp: BE + return wrapSingleEvent(bytes, this.protocolVersion); + } + + private encodeMouseButton(type: number, payload: MouseButtonPayload): Uint8Array { + const bytes = new Uint8Array(18); + const view = new DataView(bytes.buffer); + // [type 4B LE][button 1B][pad 1B][reserved 4B BE][timestamp 8B BE] + view.setUint32(0, type, true); // type: LE + view.setUint8(4, payload.button); + view.setUint8(5, 0); + view.setUint32(6, 0, false); // reserved: BE + view.setBigUint64(10, payload.timestampUs, false); // timestamp: BE + return wrapSingleEvent(bytes, this.protocolVersion); + } +} + +export function modifierFlags(event: KeyboardEvent): number { + let flags = 0; + // Basic modifiers (match Rust implementation) + if (event.shiftKey) flags |= 0x01; // SHIFT + if (event.ctrlKey) flags |= 0x02; // CTRL + if (event.altKey) flags |= 0x04; // ALT + if (event.metaKey) flags |= 0x08; // META + // Lock keys (match Rust modifier flags) + if (event.getModifierState("CapsLock")) flags |= 0x10; // CAPS_LOCK + if (event.getModifierState("NumLock")) flags |= 0x20; // NUM_LOCK + return flags; +} + +export function mapKeyboardEvent(event: KeyboardEvent): { vk: number; scancode: number } | null { + const mapped = codeMap[event.code]; + if (mapped) { + return mapped; + } + + const fallbackMapped = keyFallbackMap[event.key]; + if (fallbackMapped) { + return fallbackMapped; + } + + const key = event.key; + if (key.length === 1) { + const upper = key.toUpperCase(); + if (upper >= "A" && upper <= "Z") { + return { vk: upper.charCodeAt(0), scancode: 0 }; + } + if (key >= "0" && key <= "9") { + return { vk: key.charCodeAt(0), scancode: 0 }; + } + } + + return null; +} + +/** + * Convert browser mouse button (0-based) to GFN protocol (1-based). + * Browser: 0=Left, 1=Middle, 2=Right, 3=Back, 4=Forward + * GFN: 1=Left, 2=Middle, 3=Right, 4=Back, 5=Forward + */ +export function toMouseButton(button: number): number { + // Convert 0-based browser button to 1-based GFN button + return button + 1; +} + +/** + * Apply radial deadzone to analog stick values. + * Uses a circular deadzone where values inside the threshold are zeroed. + * @param x X-axis value (-1.0 to 1.0) + * @param y Y-axis value (-1.0 to 1.0) + * @param deadzone Deadzone threshold (0.0 to 1.0), default 15% + * @returns Adjusted {x, y} values + */ +export function applyDeadzone( + x: number, + y: number, + deadzone: number = GAMEPAD_DEADZONE +): { x: number; y: number } { + // Calculate magnitude (distance from center) + const magnitude = Math.sqrt(x * x + y * y); + + // If inside deadzone, return zero + if (magnitude < deadzone) { + return { x: 0, y: 0 }; + } + + // Normalize and rescale to full range + const normalizedX = x / magnitude; + const normalizedY = y / magnitude; + + // Scale from deadzone edge to 1.0 + const scaledMagnitude = (magnitude - deadzone) / (1.0 - deadzone); + const clampedMagnitude = Math.min(1.0, scaledMagnitude); + + return { + x: normalizedX * clampedMagnitude, + y: normalizedY * clampedMagnitude, + }; +} + +/** + * Convert a normalized axis value (-1.0 to 1.0) to signed 16-bit integer. + * @param value Normalized value (-1.0 to 1.0) + * @returns Signed 16-bit integer (-32768 to 32767) + */ +export function normalizeToInt16(value: number): number { + return Math.max(-32768, Math.min(32767, Math.round(value * 32767))); +} + +/** + * Convert a normalized trigger value (0.0 to 1.0) to unsigned 8-bit integer. + * @param value Normalized value (0.0 to 1.0) + * @returns Unsigned 8-bit integer (0 to 255) + */ +export function normalizeToUint8(value: number): number { + return Math.max(0, Math.min(255, Math.round(value * 255))); +} + +/** + * Map Standard Gamepad API buttons to XInput button flags. + * Standard Gamepad: https://w3c.github.io/gamepad/#remapping + * + * Uses button.value (not button.pressed) to match the official GFN client's NA() function. + * button.value is a float 0.0-1.0; any non-zero value counts as pressed. + * This catches partial analog button presses that button.pressed might miss. + */ +export function mapGamepadButtons(gamepad: Gamepad): number { + let buttons = 0; + const b = gamepad.buttons; + + // Standard Gamepad mapping to XInput (matches official client's NA() exactly) + // Face buttons + if (b[0]?.value) buttons |= GAMEPAD_A; // Bottom (A/Cross) + if (b[1]?.value) buttons |= GAMEPAD_B; // Right (B/Circle) + if (b[2]?.value) buttons |= GAMEPAD_X; // Left (X/Square) + if (b[3]?.value) buttons |= GAMEPAD_Y; // Top (Y/Triangle) + + // Bumpers + if (b[4]?.value) buttons |= GAMEPAD_LB; // Left Bumper + if (b[5]?.value) buttons |= GAMEPAD_RB; // Right Bumper + + // buttons[6] and [7] are LT/RT as buttons — we use analog trigger values instead + + // Center buttons + if (b[8]?.value) buttons |= GAMEPAD_BACK; // Back/Select + if (b[9]?.value) buttons |= GAMEPAD_START; // Start + + // Stick clicks (L3/R3) + if (b[10]?.value) buttons |= GAMEPAD_LS; // L3 (Left Stick click) + if (b[11]?.value) buttons |= GAMEPAD_RS; // R3 (Right Stick click) + + // D-Pad + if (b[12]?.value) buttons |= GAMEPAD_DPAD_UP; + if (b[13]?.value) buttons |= GAMEPAD_DPAD_DOWN; + if (b[14]?.value) buttons |= GAMEPAD_DPAD_LEFT; + if (b[15]?.value) buttons |= GAMEPAD_DPAD_RIGHT; + + // Guide button + if (b[16]?.value) buttons |= GAMEPAD_GUIDE; // Guide (Center/Xbox) + + return buttons; +} + +/** + * Read analog axes from Standard Gamepad API and apply deadzone. + * @param gamepad The Gamepad object from navigator.getGamepads() + * @returns Object with left/right stick and trigger values + */ +export function readGamepadAxes(gamepad: Gamepad): { + leftStickX: number; + leftStickY: number; + rightStickX: number; + rightStickY: number; + leftTrigger: number; + rightTrigger: number; +} { + // Left stick (axes 0, 1) + const lx = gamepad.axes[0] ?? 0; + const ly = gamepad.axes[1] ?? 0; + const leftStick = applyDeadzone(lx, ly); + + // Right stick (axes 2, 3) + const rx = gamepad.axes[2] ?? 0; + const ry = gamepad.axes[3] ?? 0; + const rightStick = applyDeadzone(rx, ry); + + // Triggers - can be buttons (6, 7) or axes (4, 5) depending on browser + let leftTrigger = 0; + let rightTrigger = 0; + + if (gamepad.buttons[6]) { + leftTrigger = gamepad.buttons[6].value; + } else if (gamepad.axes[4] !== undefined && gamepad.axes[4] > 0) { + leftTrigger = gamepad.axes[4]; + } + + if (gamepad.buttons[7]) { + rightTrigger = gamepad.buttons[7].value; + } else if (gamepad.axes[5] !== undefined && gamepad.axes[5] > 0) { + rightTrigger = gamepad.axes[5]; + } + + return { + leftStickX: leftStick.x, + leftStickY: -leftStick.y, // Invert Y to match XInput convention + rightStickX: rightStick.x, + rightStickY: -rightStick.y, // Invert Y to match XInput convention + leftTrigger, + rightTrigger, + }; +} diff --git a/opennow-stable/src/renderer/src/gfn/sdp.ts b/opennow-stable/src/renderer/src/gfn/sdp.ts new file mode 100644 index 0000000..1b9efdf --- /dev/null +++ b/opennow-stable/src/renderer/src/gfn/sdp.ts @@ -0,0 +1,617 @@ +import type { ColorQuality, VideoCodec } from "@shared/gfn"; + +interface IceCredentials { + ufrag: string; + pwd: string; + fingerprint: string; +} + +/** + * Convert dash-separated hostname to dotted IP if it matches the GFN pattern. + * e.g. "80-250-97-40.cloudmatchbeta.nvidiagrid.net" -> "80.250.97.40" + * e.g. "161-248-11-132.bpc.geforcenow.nvidiagrid.net" -> "161.248.11.132" + */ +export function extractPublicIp(hostOrIp: string): string | null { + if (!hostOrIp) return null; + + // Already a dotted IP? + if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostOrIp)) { + return hostOrIp; + } + + // Dash-separated hostname: take the first label, convert dashes to dots + const firstLabel = hostOrIp.split(".")[0] ?? ""; + const parts = firstLabel.split("-"); + if (parts.length === 4 && parts.every((p) => /^\d{1,3}$/.test(p))) { + return parts.join("."); + } + + return null; +} + +/** + * Fix 0.0.0.0 in the server's SDP offer with the actual server IP. + * Matches Rust's fix_server_ip() — replaces "c=IN IP4 0.0.0.0" with real IP. + * Also fixes a=candidate: lines that contain 0.0.0.0 as the candidate IP, + * since Chrome's WebRTC stack treats those as unreachable and ICE fails. + */ +export function fixServerIp(sdp: string, serverIp: string): string { + const ip = extractPublicIp(serverIp); + if (!ip) { + console.log(`[SDP] fixServerIp: could not extract IP from "${serverIp}"`); + return sdp; + } + // 1. Fix connection lines: c=IN IP4 0.0.0.0 + const cCount = (sdp.match(/c=IN IP4 0\.0\.0\.0/g) ?? []).length; + let fixed = sdp.replace(/c=IN IP4 0\.0\.0\.0/g, `c=IN IP4 ${ip}`); + console.log(`[SDP] fixServerIp: replaced ${cCount} c= lines with ${ip}`); + + // 2. Fix ICE candidate lines: a=candidate:... 0.0.0.0 ... + // Format: a=candidate: typ + const candidateCount = (fixed.match(/(a=candidate:\S+\s+\d+\s+\w+\s+\d+\s+)0\.0\.0\.0(\s+)/g) ?? []).length; + if (candidateCount > 0) { + fixed = fixed.replace( + /(a=candidate:\S+\s+\d+\s+\w+\s+\d+\s+)0\.0\.0\.0(\s+)/g, + `$1${ip}$2`, + ); + console.log(`[SDP] fixServerIp: replaced ${candidateCount} a=candidate lines with ${ip}`); + } + + return fixed; +} + +/** + * Extract the server's ice-ufrag from the offer SDP. + * Needed for manual ICE candidate injection (ice-lite servers). + */ +export function extractIceUfragFromOffer(sdp: string): string { + const match = sdp.match(/a=ice-ufrag:([^\r\n]+)/); + return match?.[1]?.trim() ?? ""; +} + +export function extractIceCredentials(sdp: string): IceCredentials { + const ufrag = sdp + .split(/\r?\n/) + .find((line) => line.startsWith("a=ice-ufrag:")) + ?.replace("a=ice-ufrag:", "") + .trim(); + const pwd = sdp + .split(/\r?\n/) + .find((line) => line.startsWith("a=ice-pwd:")) + ?.replace("a=ice-pwd:", "") + .trim(); + const fingerprint = sdp + .split(/\r?\n/) + .find((line) => line.startsWith("a=fingerprint:sha-256 ")) + ?.replace("a=fingerprint:sha-256 ", "") + .trim(); + + return { + ufrag: ufrag ?? "", + pwd: pwd ?? "", + fingerprint: fingerprint ?? "", + }; +} + +function normalizeCodec(name: string): string { + const upper = name.toUpperCase(); + return upper === "HEVC" ? "H265" : upper; +} + +export function rewriteH265TierFlag( + sdp: string, + tierFlag: 0 | 1, +): { sdp: string; replacements: number } { + const lineEnding = sdp.includes("\r\n") ? "\r\n" : "\n"; + const lines = sdp.split(/\r?\n/); + + const h265Payloads = new Set(); + let inVideoSection = false; + + for (const line of lines) { + if (line.startsWith("m=video")) { + inVideoSection = true; + continue; + } + if (line.startsWith("m=") && inVideoSection) { + inVideoSection = false; + } + if (!inVideoSection || !line.startsWith("a=rtpmap:")) { + continue; + } + + const [, rest = ""] = line.split(":", 2); + const [pt = "", codecPart = ""] = rest.split(/\s+/, 2); + const codecName = normalizeCodec((codecPart.split("/")[0] ?? "").trim()); + if (pt && codecName === "H265") { + h265Payloads.add(pt); + } + } + + if (h265Payloads.size === 0) { + return { sdp, replacements: 0 }; + } + + let replacements = 0; + const rewritten = lines.map((line) => { + if (!line.startsWith("a=fmtp:")) { + return line; + } + + const [, rest = ""] = line.split(":", 2); + const [pt = ""] = rest.split(/\s+/, 1); + if (!pt || !h265Payloads.has(pt)) { + return line; + } + + const next = line.replace(/tier-flag=1/gi, `tier-flag=${tierFlag}`); + if (next !== line) { + replacements += 1; + } + return next; + }); + + return { + sdp: rewritten.join(lineEnding), + replacements, + }; +} + +export function rewriteH265LevelIdByProfile( + sdp: string, + maxLevelByProfile: Partial>, +): { sdp: string; replacements: number } { + const lineEnding = sdp.includes("\r\n") ? "\r\n" : "\n"; + const lines = sdp.split(/\r?\n/); + + const h265Payloads = new Set(); + let inVideoSection = false; + + for (const line of lines) { + if (line.startsWith("m=video")) { + inVideoSection = true; + continue; + } + if (line.startsWith("m=") && inVideoSection) { + inVideoSection = false; + } + if (!inVideoSection || !line.startsWith("a=rtpmap:")) { + continue; + } + + const [, rest = ""] = line.split(":", 2); + const [pt = "", codecPart = ""] = rest.split(/\s+/, 2); + const codecName = normalizeCodec((codecPart.split("/")[0] ?? "").trim()); + if (pt && codecName === "H265") { + h265Payloads.add(pt); + } + } + + if (h265Payloads.size === 0) { + return { sdp, replacements: 0 }; + } + + let replacements = 0; + const rewritten = lines.map((line) => { + if (!line.startsWith("a=fmtp:")) { + return line; + } + + const [, rest = ""] = line.split(":", 2); + const [pt = "", params = ""] = rest.split(/\s+/, 2); + if (!pt || !params || !h265Payloads.has(pt)) { + return line; + } + + const profileMatch = params.match(/(?:^|;)\s*profile-id=(\d+)/i); + const levelMatch = params.match(/(?:^|;)\s*level-id=(\d+)/i); + if (!profileMatch?.[1] || !levelMatch?.[1]) { + return line; + } + + const profileNum = Number.parseInt(profileMatch[1], 10) as 1 | 2; + const offeredLevel = Number.parseInt(levelMatch[1], 10); + const maxLevel = maxLevelByProfile[profileNum]; + if (!Number.isFinite(offeredLevel) || !maxLevel || offeredLevel <= maxLevel) { + return line; + } + + const next = line.replace(/(level-id=)(\d+)/i, `$1${maxLevel}`); + if (next !== line) { + replacements += 1; + } + return next; + }); + + return { + sdp: rewritten.join(lineEnding), + replacements, + }; +} + +interface PreferCodecOptions { + preferHevcProfileId?: 1 | 2; +} + +export function preferCodec(sdp: string, codec: VideoCodec, options?: PreferCodecOptions): string { + console.log(`[SDP] preferCodec: filtering SDP for codec "${codec}"`); + const lineEnding = sdp.includes("\r\n") ? "\r\n" : "\n"; + const lines = sdp.split(/\r?\n/); + + let inVideoSection = false; + const payloadTypesByCodec = new Map(); + const codecByPayloadType = new Map(); + const rtxAptByPayloadType = new Map(); + const fmtpByPayloadType = new Map(); + + for (const line of lines) { + if (line.startsWith("m=video")) { + inVideoSection = true; + continue; + } + if (line.startsWith("m=") && inVideoSection) { + inVideoSection = false; + } + + if (!inVideoSection || !line.startsWith("a=rtpmap:")) { + continue; + } + + const [, rest = ""] = line.split("a=rtpmap:"); + const [pt, codecPart] = rest.split(/\s+/, 2); + const codecName = normalizeCodec((codecPart ?? "").split("/")[0] ?? ""); + if (!pt || !codecName) { + continue; + } + + const list = payloadTypesByCodec.get(codecName) ?? []; + list.push(pt); + payloadTypesByCodec.set(codecName, list); + codecByPayloadType.set(pt, codecName); + + continue; + } + + // Parse RTX apt mappings from fmtp lines so we can keep RTX for chosen codec payloads + inVideoSection = false; + for (const line of lines) { + if (line.startsWith("m=video")) { + inVideoSection = true; + continue; + } + if (line.startsWith("m=") && inVideoSection) { + inVideoSection = false; + } + if (!inVideoSection || !line.startsWith("a=fmtp:")) { + continue; + } + + const [, rest = ""] = line.split(":", 2); + const [pt = "", params = ""] = rest.split(/\s+/, 2); + if (!pt || !params) { + continue; + } + + const aptMatch = params.match(/(?:^|;)\s*apt=(\d+)/i); + if (aptMatch?.[1]) { + rtxAptByPayloadType.set(pt, aptMatch[1]); + } + fmtpByPayloadType.set(pt, params); + } + + // Log all codecs found in the SDP + for (const [name, pts] of payloadTypesByCodec.entries()) { + console.log(`[SDP] preferCodec: found codec ${name} with payload types [${pts.join(", ")}]`); + } + + const preferredPayloads = payloadTypesByCodec.get(codec) ?? []; + if (preferredPayloads.length === 0) { + console.log(`[SDP] preferCodec: codec "${codec}" NOT found in offer — returning SDP unmodified`); + return sdp; + } + + // H265 often appears with multiple profiles in one offer. + // Prefer profile-id=1 first (widest decoder compatibility), then others. + const orderedPreferredPayloads = codec === "H265" && options?.preferHevcProfileId + ? [...preferredPayloads].sort((a, b) => { + const pa = fmtpByPayloadType.get(a) ?? ""; + const pb = fmtpByPayloadType.get(b) ?? ""; + const score = (fmtp: string): number => { + const profile = fmtp.match(/(?:^|;)\s*profile-id=(\d+)/i)?.[1]; + if (profile === String(options.preferHevcProfileId)) return 0; + if (!profile) return 1; + return 2; + }; + return score(pa) - score(pb); + }) + : preferredPayloads; + + const preferred = new Set(orderedPreferredPayloads); + + const allowed = new Set(preferred); + + // Keep RTX payloads linked to preferred payloads (apt mapping) + for (const [rtxPt, apt] of rtxAptByPayloadType.entries()) { + if (preferred.has(apt) && codecByPayloadType.get(rtxPt) === "RTX") { + allowed.add(rtxPt); + } + } + + // Do NOT keep FLEXFEC/RED/ULPFEC during hard codec filtering. + // Chromium can otherwise negotiate a "video" m-line with only FEC payloads + // when primary codec intersection fails, causing black video with live audio. + + console.log(`[SDP] preferCodec: preferred ordered payloads [${orderedPreferredPayloads.join(", ")}] for ${codec}`); + console.log(`[SDP] preferCodec: keeping payload types [${Array.from(allowed).join(", ")}] for ${codec}`); + + const filtered: string[] = []; + inVideoSection = false; + + for (const line of lines) { + if (line.startsWith("m=video")) { + inVideoSection = true; + const parts = line.split(/\s+/); + const header = parts.slice(0, 3); + const available = parts.slice(3).filter((pt) => allowed.has(pt)); + const ordered: string[] = []; + + for (const pt of orderedPreferredPayloads) { + if (available.includes(pt)) { + ordered.push(pt); + } + } + for (const pt of available) { + if (!preferred.has(pt)) { + ordered.push(pt); + } + } + + filtered.push(ordered.length > 0 ? [...header, ...ordered].join(" ") : line); + continue; + } + + if (line.startsWith("m=") && inVideoSection) { + inVideoSection = false; + } + + if (inVideoSection) { + if ( + line.startsWith("a=rtpmap:") || + line.startsWith("a=fmtp:") || + line.startsWith("a=rtcp-fb:") + ) { + const [, rest = ""] = line.split(":", 2); + const [pt = ""] = rest.split(/\s+/, 1); + if (pt && !allowed.has(pt)) { + continue; + } + } + } + + filtered.push(line); + } + + return filtered.join(lineEnding); +} + +interface NvstParams { + width: number; + height: number; + fps: number; + maxBitrateKbps: number; + partialReliableThresholdMs: number; + codec: VideoCodec; + colorQuality: ColorQuality; + credentials: IceCredentials; +} + +/** + * Munge an SDP answer to inject bitrate limits and optimize audio codec params. + * + * This matches what the official GFN browser client does: + * 1. Adds "b=AS:" after each m= line to signal our max receive bitrate + * 2. Adds "stereo=1" to the opus fmtp line for stereo audio support + * + * These are hints to the server encoder — they don't enforce limits client-side + * but help the server avoid overshooting our link capacity. + */ +export function mungeAnswerSdp(sdp: string, maxBitrateKbps: number): string { + const lineEnding = sdp.includes("\r\n") ? "\r\n" : "\n"; + const lines = sdp.split(/\r?\n/); + const result: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + result.push(line); + + // After each m= line, inject b=AS: if not already present + if (line.startsWith("m=video") || line.startsWith("m=audio")) { + const bitrateForSection = line.startsWith("m=video") + ? maxBitrateKbps + : 128; // 128 kbps for audio is plenty for opus stereo + const nextLine = lines[i + 1] ?? ""; + if (!nextLine.startsWith("b=")) { + result.push(`b=AS:${bitrateForSection}`); + } + } + + // Append stereo=1 to opus fmtp line if not already present + if (line.startsWith("a=fmtp:") && line.includes("minptime=") && !line.includes("stereo=1")) { + // Replace the line we just pushed with the stereo-augmented version + result[result.length - 1] = line + ";stereo=1"; + } + } + + console.log(`[SDP] mungeAnswerSdp: injected b=AS:${maxBitrateKbps} for video, b=AS:128 for audio, stereo=1 for opus`); + return result.join(lineEnding); +} + +export function buildNvstSdp(params: NvstParams): string { + console.log(`[SDP] buildNvstSdp: ${params.width}x${params.height}@${params.fps}fps, codec=${params.codec}, colorQuality=${params.colorQuality}, maxBitrate=${params.maxBitrateKbps}kbps`); + console.log(`[SDP] buildNvstSdp: ICE ufrag=${params.credentials.ufrag}, pwd=${params.credentials.pwd.slice(0, 8)}..., fingerprint=${params.credentials.fingerprint.slice(0, 20)}...`); + // Adaptive profile: + // allow bitrate to scale down under congestion to reduce stutter and input lag. + const minBitrate = Math.max(5000, Math.floor(params.maxBitrateKbps * 0.35)); + const initialBitrate = Math.max(minBitrate, Math.floor(params.maxBitrateKbps * 0.7)); + const isHighFps = params.fps >= 90; + const is120Fps = params.fps === 120; + const is240Fps = params.fps >= 240; + const isAv1 = params.codec === "AV1"; + const bitDepth = params.colorQuality.startsWith("10bit") ? 10 : 8; + + const lines: string[] = [ + "v=0", + "o=SdpTest test_id_13 14 IN IPv4 127.0.0.1", + "s=-", + "t=0 0", + `a=general.icePassword:${params.credentials.pwd}`, + `a=general.iceUserNameFragment:${params.credentials.ufrag}`, + `a=general.dtlsFingerprint:${params.credentials.fingerprint}`, + "m=video 0 RTP/AVP", + "a=msid:fbc-video-0", + // FEC settings + "a=vqos.fec.rateDropWindow:10", + "a=vqos.fec.minRequiredFecPackets:2", + "a=vqos.fec.repairMinPercent:5", + "a=vqos.fec.repairPercent:5", + "a=vqos.fec.repairMaxPercent:35", + // DRC — always disabled to allow full bitrate + "a=vqos.drc.enable:0", + ]; + + // Force-disable dynamic frame control to avoid server-side FPS/resolution adaptation. + lines.push("a=vqos.dfc.enable:0"); + + // Video encoder settings + lines.push( + "a=video.dx9EnableNv12:1", + "a=video.dx9EnableHdr:1", + "a=vqos.qpg.enable:1", + "a=vqos.resControl.qp.qpg.featureSetting:7", + "a=bwe.useOwdCongestionControl:1", + "a=video.enableRtpNack:1", + "a=vqos.bw.txRxLag.minFeedbackTxDeltaMs:200", + "a=vqos.drc.bitrateIirFilterFactor:18", + "a=video.packetSize:1140", + "a=packetPacing.minNumPacketsPerGroup:15", + ); + + // High FPS optimizations + if (isHighFps) { + lines.push( + "a=bwe.iirFilterFactor:8", + "a=video.encoderFeatureSetting:47", + "a=video.encoderPreset:6", + "a=vqos.resControl.cpmRtc.badNwSkipFramesCount:600", + "a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:9", + `a=video.fbcDynamicFpsGrabTimeoutMs:${is120Fps ? 6 : 18}`, + `a=vqos.resControl.cpmRtc.serverResolutionUpdateCoolDownCount:${is120Fps ? 6000 : 12000}`, + ); + } + + // 240+ FPS optimizations + if (is240Fps) { + lines.push( + "a=video.enableNextCaptureMode:1", + "a=vqos.maxStreamFpsEstimate:240", + "a=video.videoSplitEncodeStripsPerFrame:3", + "a=video.updateSplitEncodeStateDynamically:1", + ); + } + + // Out-of-focus handling + disable ALL dynamic resolution control + lines.push( + "a=vqos.adjustStreamingFpsDuringOutOfFocus:1", + "a=vqos.resControl.cpmRtc.ignoreOutOfFocusWindowState:1", + "a=vqos.resControl.perfHistory.rtcIgnoreOutOfFocusWindowState:1", + // Disable CPM-based resolution changes (prevents SSRC switches) + "a=vqos.resControl.cpmRtc.featureMask:0", + "a=vqos.resControl.cpmRtc.enable:0", + // Never scale down resolution + "a=vqos.resControl.cpmRtc.minResolutionPercent:100", + // Infinite cooldown to prevent resolution changes + "a=vqos.resControl.cpmRtc.resolutionChangeHoldonMs:999999", + // Packet pacing + `a=packetPacing.numGroups:${is120Fps ? 3 : 5}`, + "a=packetPacing.maxDelayUs:1000", + "a=packetPacing.minNumPacketsFrame:10", + // NACK queue settings + "a=video.rtpNackQueueLength:1024", + "a=video.rtpNackQueueMaxPackets:512", + "a=video.rtpNackMaxPacketCount:25", + // Resolution/quality thresholds — high values prevent downscaling + "a=vqos.drc.qpMaxResThresholdAdj:4", + "a=vqos.grc.qpMaxResThresholdAdj:4", + "a=vqos.drc.iirFilterFactor:100", + ); + + // AV1-specific DRC/GRC tuning (mirrors official client intent): + // bias towards QP adaptation before resolution downgrade. + if (isAv1) { + lines.push( + "a=vqos.drc.minQpHeadroom:20", + "a=vqos.drc.lowerQpThreshold:100", + "a=vqos.drc.upperQpThreshold:200", + "a=vqos.drc.minAdaptiveQpThreshold:180", + "a=vqos.drc.qpCodecThresholdAdj:0", + // official client scales this up for AV1 + "a=vqos.drc.qpMaxResThresholdAdj:20", + // mirror to DFC/GRC + "a=vqos.dfc.minQpHeadroom:20", + "a=vqos.dfc.qpLowerLimit:100", + "a=vqos.dfc.qpMaxUpperLimit:200", + "a=vqos.dfc.qpMinUpperLimit:180", + "a=vqos.dfc.qpMaxResThresholdAdj:20", + "a=vqos.dfc.qpCodecThresholdAdj:0", + "a=vqos.grc.minQpHeadroom:20", + "a=vqos.grc.lowerQpThreshold:100", + "a=vqos.grc.upperQpThreshold:200", + "a=vqos.grc.minAdaptiveQpThreshold:180", + "a=vqos.grc.qpMaxResThresholdAdj:20", + "a=vqos.grc.qpCodecThresholdAdj:0", + "a=video.minQp:25", + // official client can enable this for AV1 depending on resolution class + "a=video.enableAv1RcPrecisionFactor:1", + ); + } + + // Viewport, FPS, and bitrate + lines.push( + `a=video.clientViewportWd:${params.width}`, + `a=video.clientViewportHt:${params.height}`, + `a=video.maxFPS:${params.fps}`, + `a=video.initialBitrateKbps:${initialBitrate}`, + `a=video.initialPeakBitrateKbps:${params.maxBitrateKbps}`, + `a=vqos.bw.maximumBitrateKbps:${params.maxBitrateKbps}`, + `a=vqos.bw.minimumBitrateKbps:${minBitrate}`, + `a=vqos.bw.peakBitrateKbps:${params.maxBitrateKbps}`, + `a=vqos.bw.serverPeakBitrateKbps:${params.maxBitrateKbps}`, + "a=vqos.bw.enableBandwidthEstimation:1", + "a=vqos.bw.disableBitrateLimit:0", + // GRC — disabled + `a=vqos.grc.maximumBitrateKbps:${params.maxBitrateKbps}`, + "a=vqos.grc.enable:0", + // Encoder settings + "a=video.maxNumReferenceFrames:4", + "a=video.mapRtpTimestampsToFrames:1", + "a=video.encoderCscMode:3", + "a=video.dynamicRangeMode:0", + `a=video.bitDepth:${bitDepth}`, + // Disable server-side scaling and prefilter (prevents resolution downgrade) + `a=video.scalingFeature1:${isAv1 ? 1 : 0}`, + "a=video.prefilterParams.prefilterModel:0", + // Audio track + "m=audio 0 RTP/AVP", + "a=msid:audio", + // Mic track + "m=mic 0 RTP/AVP", + "a=msid:mic", + // Input/application track + "m=application 0 RTP/AVP", + "a=msid:input_1", + `a=ri.partialReliableThresholdMs:${params.partialReliableThresholdMs}`, + "", + ); + + return lines.join("\n"); +} diff --git a/opennow-stable/src/renderer/src/gfn/webrtcClient.ts b/opennow-stable/src/renderer/src/gfn/webrtcClient.ts new file mode 100644 index 0000000..7f3753a --- /dev/null +++ b/opennow-stable/src/renderer/src/gfn/webrtcClient.ts @@ -0,0 +1,2737 @@ +import type { + IceCandidatePayload, + ColorQuality, + IceServer, + SessionInfo, + VideoCodec, +} from "@shared/gfn"; + +import { + InputEncoder, + mapKeyboardEvent, + modifierFlags, + toMouseButton, + mapGamepadButtons, + readGamepadAxes, + normalizeToInt16, + normalizeToUint8, + GAMEPAD_MAX_CONTROLLERS, + type GamepadInput, +} from "./inputProtocol"; +import { + buildNvstSdp, + extractIceCredentials, + extractIceUfragFromOffer, + extractPublicIp, + fixServerIp, + mungeAnswerSdp, + preferCodec, + rewriteH265LevelIdByProfile, + rewriteH265TierFlag, +} from "./sdp"; + +interface OfferSettings { + codec: VideoCodec; + colorQuality: ColorQuality; + resolution: string; + fps: number; + maxBitrateKbps: number; +} + +interface KeyStrokeSpec { + vk: number; + scancode: number; + shift?: boolean; +} + +const baseCharKeyMap: Record = { + " ": { vk: 0x20, scancode: 0x2c }, + "\n": { vk: 0x0d, scancode: 0x28 }, + "\r": { vk: 0x0d, scancode: 0x28 }, + "\t": { vk: 0x09, scancode: 0x2b }, + "0": { vk: 0x30, scancode: 0x27 }, + "1": { vk: 0x31, scancode: 0x1e }, + "2": { vk: 0x32, scancode: 0x1f }, + "3": { vk: 0x33, scancode: 0x20 }, + "4": { vk: 0x34, scancode: 0x21 }, + "5": { vk: 0x35, scancode: 0x22 }, + "6": { vk: 0x36, scancode: 0x23 }, + "7": { vk: 0x37, scancode: 0x24 }, + "8": { vk: 0x38, scancode: 0x25 }, + "9": { vk: 0x39, scancode: 0x26 }, + "-": { vk: 0xbd, scancode: 0x2d }, + "=": { vk: 0xbb, scancode: 0x2e }, + "[": { vk: 0xdb, scancode: 0x2f }, + "]": { vk: 0xdd, scancode: 0x30 }, + "\\": { vk: 0xdc, scancode: 0x31 }, + ";": { vk: 0xba, scancode: 0x33 }, + "'": { vk: 0xde, scancode: 0x34 }, + "`": { vk: 0xc0, scancode: 0x35 }, + ",": { vk: 0xbc, scancode: 0x36 }, + ".": { vk: 0xbe, scancode: 0x37 }, + "/": { vk: 0xbf, scancode: 0x38 }, +}; + +const shiftedCharKeyMap: Record = { + "!": { vk: 0x31, scancode: 0x1e, shift: true }, + "@": { vk: 0x32, scancode: 0x1f, shift: true }, + "#": { vk: 0x33, scancode: 0x20, shift: true }, + "$": { vk: 0x34, scancode: 0x21, shift: true }, + "%": { vk: 0x35, scancode: 0x22, shift: true }, + "^": { vk: 0x36, scancode: 0x23, shift: true }, + "&": { vk: 0x37, scancode: 0x24, shift: true }, + "*": { vk: 0x38, scancode: 0x25, shift: true }, + "(": { vk: 0x39, scancode: 0x26, shift: true }, + ")": { vk: 0x30, scancode: 0x27, shift: true }, + "_": { vk: 0xbd, scancode: 0x2d, shift: true }, + "+": { vk: 0xbb, scancode: 0x2e, shift: true }, + "{": { vk: 0xdb, scancode: 0x2f, shift: true }, + "}": { vk: 0xdd, scancode: 0x30, shift: true }, + "|": { vk: 0xdc, scancode: 0x31, shift: true }, + ":": { vk: 0xba, scancode: 0x33, shift: true }, + "\"": { vk: 0xde, scancode: 0x34, shift: true }, + "~": { vk: 0xc0, scancode: 0x35, shift: true }, + "<": { vk: 0xbc, scancode: 0x36, shift: true }, + ">": { vk: 0xbe, scancode: 0x37, shift: true }, + "?": { vk: 0xbf, scancode: 0x38, shift: true }, +}; + +function mapTextCharToKeySpec(char: string): KeyStrokeSpec | null { + if (baseCharKeyMap[char]) { + return baseCharKeyMap[char]; + } + + if (shiftedCharKeyMap[char]) { + return shiftedCharKeyMap[char]; + } + + if (char >= "a" && char <= "z") { + const code = char.charCodeAt(0); + return { vk: code - 32, scancode: 0x04 + (code - 97) }; + } + + if (char >= "A" && char <= "Z") { + const code = char.charCodeAt(0); + return { vk: code, scancode: 0x04 + (code - 65), shift: true }; + } + + return null; +} + +function hevcPreferredProfileId(colorQuality: ColorQuality): 1 | 2 { + // 10-bit modes should prefer HEVC Main10 profile-id=2. + return colorQuality.startsWith("10bit") ? 2 : 1; +} + +export interface StreamDiagnostics { + // Connection state + connectionState: RTCPeerConnectionState | "closed"; + inputReady: boolean; + connectedGamepads: number; + + // Video stats + resolution: string; + codec: string; + isHdr: boolean; + bitrateKbps: number; + decodeFps: number; + renderFps: number; + + // Network stats + packetsLost: number; + packetsReceived: number; + packetLossPercent: number; + jitterMs: number; + rttMs: number; + + // Frame counters + framesReceived: number; + framesDecoded: number; + framesDropped: number; + + // Timing + decodeTimeMs: number; + renderTimeMs: number; + jitterBufferDelayMs: number; + + // Input channel pressure + inputQueueBufferedBytes: number; + inputQueuePeakBufferedBytes: number; + inputQueueDropCount: number; + inputQueueMaxSchedulingDelayMs: number; + + // System info + gpuType: string; + serverRegion: string; +} + +export interface StreamTimeWarning { + code: 1 | 2 | 3; + secondsLeft?: number; +} + +interface ClientOptions { + videoElement: HTMLVideoElement; + audioElement: HTMLAudioElement; + onLog: (line: string) => void; + onStats?: (stats: StreamDiagnostics) => void; + onEscHoldProgress?: (visible: boolean, progress: number) => void; + onTimeWarning?: (warning: StreamTimeWarning) => void; +} + +function timestampUs(sourceTimestampMs?: number): bigint { + const base = + typeof sourceTimestampMs === "number" && Number.isFinite(sourceTimestampMs) && sourceTimestampMs >= 0 + ? sourceTimestampMs + : performance.now(); + return BigInt(Math.floor(base * 1000)); +} + +function parsePartialReliableThresholdMs(sdp: string): number | null { + const match = sdp.match(/a=ri\.partialReliableThresholdMs:(\d+)/i); + if (!match?.[1]) { + return null; + } + const parsed = Number.parseInt(match[1], 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + return Math.max(1, Math.min(5000, parsed)); +} + +class MouseDeltaFilter { + private x = 0; + private y = 0; + private lastTsMs = 0; + private velocityX = 0; + private velocityY = 0; + private rejectedX = 0; + private rejectedY = 0; + private pendingX = 0; + private pendingY = 0; + private sawZero = false; + + public getX(): number { + return this.x; + } + + public getY(): number { + return this.y; + } + + public reset(): void { + this.x = 0; + this.y = 0; + this.lastTsMs = 0; + this.velocityX = 0; + this.velocityY = 0; + this.rejectedX = 0; + this.rejectedY = 0; + this.pendingX = 0; + this.pendingY = 0; + this.sawZero = false; + } + + public update(dx: number, dy: number, tsMs: number): boolean { + if (dx === 0 && dy === 0) { + if (this.sawZero) { + this.pendingX = 0; + this.pendingY = 0; + } else { + this.sawZero = true; + } + return false; + } + + this.sawZero = false; + if (this.pendingX === 0 && this.pendingY === 0) { + if (tsMs < this.lastTsMs) { + this.pendingX = dx; + this.pendingY = dy; + return false; + } + } else { + dx += this.pendingX; + dy += this.pendingY; + this.pendingX = 0; + this.pendingY = 0; + } + + const dot = dx * this.x + dy * this.y; + const magIncoming = dx * dx + dy * dy; + const magPrev = this.x * this.x + this.y * this.y; + let accept = true; + + const dtMs = tsMs - this.lastTsMs; + if (dtMs < 0.95 && dot < 0 && magPrev !== 0 && dot * dot > 0.81 * magIncoming * magPrev) { + const ratio = Math.sqrt(magIncoming) / Math.sqrt(magPrev); + let distToInt = Math.abs(ratio - Math.trunc(ratio)); + if (distToInt > 0.5) { + distToInt = 1 - distToInt; + } + if (distToInt < 0.1) { + accept = false; + } + } + + const diffX = dx - this.x; + const diffY = dy - this.y; + const diffMag = diffX * diffX + diffY * diffY; + + if (accept) { + const scale = 1 + 0.1 * Math.max(1, Math.min(16, dtMs)); + const vx2 = 2 * scale * Math.abs(this.velocityX); + const vy2 = 2 * scale * Math.abs(this.velocityY); + const threshold = Math.max(8100, vx2 * vx2 + vy2 * vy2); + accept = diffMag < threshold; + if (!accept && (this.rejectedX !== 0 || this.rejectedY !== 0)) { + const rx = dx - this.rejectedX; + const ry = dy - this.rejectedY; + accept = rx * rx + ry * ry < threshold; + } + } + + if (accept) { + this.velocityX = 0.4 * this.velocityX + 0.6 * diffX; + this.velocityY = 0.4 * this.velocityY + 0.6 * diffY; + this.x = dx; + this.y = dy; + this.lastTsMs = tsMs; + this.rejectedX = 0; + this.rejectedY = 0; + return true; + } + + this.rejectedX = dx; + this.rejectedY = dy; + return false; + } +} + +function parseResolution(resolution: string): { width: number; height: number } { + const [rawWidth, rawHeight] = resolution.split("x"); + const width = Number.parseInt(rawWidth ?? "", 10); + const height = Number.parseInt(rawHeight ?? "", 10); + + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return { width: 1920, height: 1080 }; + } + + return { width, height }; +} + +function toRtcIceServers(iceServers: IceServer[]): RTCIceServer[] { + return iceServers.map((server) => ({ + urls: server.urls, + username: server.username, + credential: server.credential, + })); +} + +async function toBytes(data: string | Blob | ArrayBuffer): Promise { + if (typeof data === "string") { + return new TextEncoder().encode(data); + } + if (data instanceof ArrayBuffer) { + return new Uint8Array(data); + } + const arrayBuffer = await data.arrayBuffer(); + return new Uint8Array(arrayBuffer); +} + +/** + * Detect GPU type using browser APIs + * Uses WebGL renderer string to identify GPU vendor/model + */ +function detectGpuType(): string { + try { + const canvas = document.createElement("canvas"); + const gl = canvas.getContext("webgl2") || canvas.getContext("webgl"); + if (!gl) { + return "Unknown"; + } + + const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); + if (debugInfo) { + const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL); + const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); + + // Clean up renderer string - extract main GPU name + let gpuName = renderer; + + // Remove common prefixes/suffixes for cleaner display + gpuName = gpuName + .replace(/\(R\)/g, "") + .replace(/\(TM\)/g, "") + .replace(/NVIDIA /i, "") + .replace(/AMD /i, "") + .replace(/Intel /i, "") + .replace(/Microsoft Corporation - /i, "") + .replace(/D3D12 /i, "") + .replace(/Direct3D11 /i, "") + .replace(/OpenGL Engine/i, "") + .trim(); + + // Limit length + if (gpuName.length > 30) { + gpuName = gpuName.substring(0, 27) + "..."; + } + + return gpuName || vendor || "Unknown"; + } + return "Unknown"; + } catch { + return "Unknown"; + } +} + +/** + * Extract codec name from codecId string (e.g., "VP09" -> "VP9", "AV1X" -> "AV1") + */ +function normalizeCodecName(codecId: string): string { + const upper = codecId.toUpperCase(); + + if (upper.startsWith("H264") || upper === "H264") { + return "H264"; + } + if (upper.startsWith("H265") || upper === "H265" || upper.startsWith("HEVC")) { + return "H265"; + } + if (upper.startsWith("AV1")) { + return "AV1"; + } + if (upper.startsWith("VP9") || upper.startsWith("VP09")) { + return "VP9"; + } + if (upper.startsWith("VP8")) { + return "VP8"; + } + + return codecId; +} + +export class GfnWebRtcClient { + private readonly videoStream = new MediaStream(); + private readonly audioStream = new MediaStream(); + private readonly inputEncoder = new InputEncoder(); + + private pc: RTCPeerConnection | null = null; + private reliableInputChannel: RTCDataChannel | null = null; + private mouseInputChannel: RTCDataChannel | null = null; + private controlChannel: RTCDataChannel | null = null; + private audioContext: AudioContext | null = null; + + private inputReady = false; + private inputProtocolVersion = 2; + private heartbeatTimer: number | null = null; + private mouseFlushTimer: number | null = null; + private statsTimer: number | null = null; + private statsPollInFlight = false; + private gamepadPollTimer: number | null = null; + private pendingMouseDx = 0; + private pendingMouseDy = 0; + private inputCleanup: Array<() => void> = []; + private queuedCandidates: RTCIceCandidateInit[] = []; + + // Input mode: auto-switches between mouse+keyboard and gamepad + // When gamepad has activity, mouse/keyboard are suppressed (and vice versa) + private activeInputMode: "mkb" | "gamepad" = "mkb"; + // Timestamp of last gamepad state change — used for mode-switch lockout + private lastGamepadActivityMs = 0; + // Timestamp of last gamepad packet sent — used for keepalive + private lastGamepadSendMs = 0; + // Gamepad keepalive interval: resend last state every 100ms to keep server controller alive + private static readonly GAMEPAD_KEEPALIVE_MS = 100; + // How long to wait after last gamepad activity before allowing switch to mkb (seconds) + // Prevents accidental key/mouse events from disrupting controller gameplay + private static readonly GAMEPAD_MODE_LOCKOUT_MS = 3000; + private static readonly MOUSE_FLUSH_FAST_MS = 4; + private static readonly MOUSE_FLUSH_NORMAL_MS = 8; + private static readonly MOUSE_FLUSH_SAFE_MS = 16; + private static readonly DEFAULT_PARTIAL_RELIABLE_THRESHOLD_MS = 300; + private static readonly RELIABLE_MOUSE_BACKPRESSURE_BYTES = 64 * 1024; + private static readonly BACKPRESSURE_LOG_INTERVAL_MS = 2000; + + // Gamepad bitmap: tracks which gamepads are connected, matching official client's this.nu field. + // Bit i (0-3) = gamepad i is connected. Sent in every gamepad packet at offset 8. + private gamepadBitmap = 0; + + // Stats tracking + private lastStatsSample: { + bytesReceived: number; + framesReceived: number; + framesDecoded: number; + framesDropped: number; + packetsReceived: number; + packetsLost: number; + atMs: number; + } | null = null; + private renderFpsCounter = { frames: 0, lastUpdate: 0, fps: 0 }; + private connectedGamepads: Set = new Set(); + private previousGamepadStates: Map = new Map(); + + // Track currently pressed keys (VK codes) for synthetic Escape detection + private pressedKeys: Set = new Set(); + // Video element reference for pointer lock re-acquisition + private videoElement: HTMLVideoElement | null = null; + // Timer for synthetic Escape on pointer lock loss + private pointerLockEscapeTimer: number | null = null; + // Fallback keyup if browser swallows Escape keyup while keyboard lock is active. + private escapeAutoKeyUpTimer: number | null = null; + // True when we already sent an immediate Escape tap for the current physical hold. + private escapeTapDispatchedForCurrentHold = false; + // Skip one synthetic Escape when pointer lock was intentionally released via hold. + private suppressNextSyntheticEscape = false; + // Hold Escape for 4 seconds to intentionally release mouse lock + private escapeHoldReleaseTimer: number | null = null; + private escapeHoldIndicatorDelayTimer: number | null = null; + private escapeHoldProgressTimer: number | null = null; + private escapeHoldStartedAtMs: number | null = null; + private mouseBackpressureLoggedAtMs = 0; + private mouseFlushIntervalMs = GfnWebRtcClient.MOUSE_FLUSH_NORMAL_MS; + private mouseFlushLastTickMs = 0; + private pendingMouseTimestampUs: bigint | null = null; + private mouseDeltaFilter = new MouseDeltaFilter(); + + private partialReliableThresholdMs = GfnWebRtcClient.DEFAULT_PARTIAL_RELIABLE_THRESHOLD_MS; + private inputQueuePeakBufferedBytesWindow = 0; + private inputQueueMaxSchedulingDelayMsWindow = 0; + private inputQueuePressureLoggedAtMs = 0; + private inputQueueDropCount = 0; + + // Stream info + private currentCodec = ""; + private currentResolution = ""; + private isHdr = false; + private videoDecodeStallWarningSent = false; + private serverRegion = ""; + private gpuType = ""; + + private diagnostics: StreamDiagnostics = { + connectionState: "closed", + inputReady: false, + connectedGamepads: 0, + resolution: "", + codec: "", + isHdr: false, + bitrateKbps: 0, + decodeFps: 0, + renderFps: 0, + packetsLost: 0, + packetsReceived: 0, + packetLossPercent: 0, + jitterMs: 0, + rttMs: 0, + framesReceived: 0, + framesDecoded: 0, + framesDropped: 0, + decodeTimeMs: 0, + renderTimeMs: 0, + jitterBufferDelayMs: 0, + inputQueueBufferedBytes: 0, + inputQueuePeakBufferedBytes: 0, + inputQueueDropCount: 0, + inputQueueMaxSchedulingDelayMs: 0, + gpuType: "", + serverRegion: "", + }; + + constructor(private readonly options: ClientOptions) { + options.videoElement.srcObject = this.videoStream; + options.audioElement.srcObject = this.audioStream; + options.audioElement.muted = true; + + // Configure video element for lowest latency playback + this.configureVideoElementForLowLatency(options.videoElement); + + // Detect GPU once on construction + this.gpuType = detectGpuType(); + this.diagnostics.gpuType = this.gpuType; + } + + /** + * Configure the video element for minimum latency streaming. + * Sets attributes that reduce internal buffering and prioritize + * immediate frame display over smooth playback. + */ + private configureVideoElementForLowLatency(video: HTMLVideoElement): void { + // disableRemotePlayback prevents Chrome from offering cast/remote playback + // which can add buffering layers + video.disableRemotePlayback = true; + + // Disable picture-in-picture to prevent additional compositor layers + video.disablePictureInPicture = true; + + // Ensure no preload buffering (we get frames via WebRTC, not a URL) + video.preload = "none"; + + // Set playback rate to 1.0 explicitly (some browsers may adjust) + video.playbackRate = 1.0; + video.defaultPlaybackRate = 1.0; + + this.log("Video element configured for low-latency playback"); + } + + /** + * Configure an RTCRtpReceiver for minimum jitter buffer delay. + * + * jitterBufferTarget controls how long Chrome holds decoded frames before + * displaying them. Setting to 0 tells the browser to use the absolute + * minimum buffer — effectively "display as soon as decoded". This is + * aggressive but correct for cloud gaming where we prioritize latency + * over smoothness. + * + * The official GFN browser client doesn't set this at all (defaulting to + * ~100-200ms). As an Electron app we can be more aggressive. + * + */ + private configureReceiverForLowLatency(receiver: RTCRtpReceiver, kind: string): void { + try { + const targetMs = kind === "video" ? 12 : 20; + const rawReceiver = receiver as unknown as Record; + + if ("jitterBufferTarget" in receiver) { + rawReceiver.jitterBufferTarget = targetMs; + this.log(`${kind} receiver: jitterBufferTarget set to ${targetMs}ms`); + } + + if ("playoutDelayHint" in receiver) { + const playoutDelaySeconds = kind === "video" ? 0.012 : 0.02; + rawReceiver.playoutDelayHint = playoutDelaySeconds; + this.log(`${kind} receiver: playoutDelayHint set to ${playoutDelaySeconds}s`); + } + + if (kind === "video" && "contentHint" in receiver.track) { + receiver.track.contentHint = "motion"; + } + } catch (error) { + this.log(`Warning: could not apply ${kind} low-latency receiver tuning: ${String(error)}`); + } + } + + private log(message: string): void { + this.options.onLog(message); + } + + private emitStats(): void { + if (this.options.onStats) { + this.options.onStats({ ...this.diagnostics }); + } + } + + private resetDiagnostics(): void { + this.lastStatsSample = null; + this.currentCodec = ""; + this.currentResolution = ""; + this.isHdr = false; + this.videoDecodeStallWarningSent = false; + this.diagnostics = { + connectionState: this.pc?.connectionState ?? "closed", + inputReady: false, + connectedGamepads: 0, + resolution: "", + codec: "", + isHdr: false, + bitrateKbps: 0, + decodeFps: 0, + renderFps: 0, + packetsLost: 0, + packetsReceived: 0, + packetLossPercent: 0, + jitterMs: 0, + rttMs: 0, + framesReceived: 0, + framesDecoded: 0, + framesDropped: 0, + decodeTimeMs: 0, + renderTimeMs: 0, + jitterBufferDelayMs: 0, + inputQueueBufferedBytes: 0, + inputQueuePeakBufferedBytes: 0, + inputQueueDropCount: 0, + inputQueueMaxSchedulingDelayMs: 0, + gpuType: this.gpuType, + serverRegion: this.serverRegion, + }; + this.emitStats(); + } + + private resetInputState(): void { + this.inputReady = false; + this.inputProtocolVersion = 2; + this.inputEncoder.setProtocolVersion(2); + this.diagnostics.inputReady = false; + this.emitStats(); + } + + private closeDataChannels(): void { + if (this.controlChannel) { + this.controlChannel.onmessage = null; + this.controlChannel.onclose = null; + this.controlChannel.onerror = null; + } + this.reliableInputChannel?.close(); + this.mouseInputChannel?.close(); + this.controlChannel?.close(); + this.reliableInputChannel = null; + this.mouseInputChannel = null; + this.controlChannel = null; + } + + private clearTimers(): void { + if (this.heartbeatTimer !== null) { + window.clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + if (this.mouseFlushTimer !== null) { + window.clearInterval(this.mouseFlushTimer); + this.mouseFlushTimer = null; + } + if (this.statsTimer !== null) { + window.clearInterval(this.statsTimer); + this.statsTimer = null; + } + if (this.gamepadPollTimer !== null) { + window.clearInterval(this.gamepadPollTimer); + this.gamepadPollTimer = null; + } + } + + private setupStatsPolling(): void { + if (this.statsTimer !== null) { + window.clearInterval(this.statsTimer); + } + + this.statsTimer = window.setInterval(() => { + if (this.statsPollInFlight) { + return; + } + this.statsPollInFlight = true; + void this.collectStats().finally(() => { + this.statsPollInFlight = false; + }); + }, 500); + } + + private updateRenderFps(): void { + const now = performance.now(); + this.renderFpsCounter.frames++; + + // Update FPS every 500ms + if (now - this.renderFpsCounter.lastUpdate >= 500) { + const elapsed = (now - this.renderFpsCounter.lastUpdate) / 1000; + this.renderFpsCounter.fps = Math.round(this.renderFpsCounter.frames / elapsed); + this.renderFpsCounter.frames = 0; + this.renderFpsCounter.lastUpdate = now; + this.diagnostics.renderFps = this.renderFpsCounter.fps; + } + } + + private async collectStats(): Promise { + if (!this.pc) { + return; + } + + const report = await this.pc.getStats(); + const now = performance.now(); + let inboundVideo: Record | null = null; + let activePair: Record | null = null; + const codecs = new Map>(); + + for (const entry of report.values()) { + const stats = entry as unknown as Record; + + if (entry.type === "inbound-rtp" && stats.kind === "video") { + inboundVideo = stats; + } + + if (entry.type === "candidate-pair") { + if (stats.state === "succeeded" && stats.nominated === true) { + activePair = stats; + } + } + + // Collect codec information + if (entry.type === "codec") { + const codecId = stats.id as string; + codecs.set(codecId, stats); + } + } + + // Process video track stats + if (inboundVideo) { + const bytes = Number(inboundVideo.bytesReceived ?? 0); + const framesReceived = Number(inboundVideo.framesReceived ?? 0); + const framesDecoded = Number(inboundVideo.framesDecoded ?? 0); + const framesDropped = Number(inboundVideo.framesDropped ?? 0); + const packetsReceived = Number(inboundVideo.packetsReceived ?? 0); + const packetsLost = Number(inboundVideo.packetsLost ?? 0); + + // Calculate bitrate + if (this.lastStatsSample) { + const bytesDelta = bytes - this.lastStatsSample.bytesReceived; + const timeDeltaMs = now - this.lastStatsSample.atMs; + if (bytesDelta >= 0 && timeDeltaMs > 0) { + const kbps = (bytesDelta * 8) / (timeDeltaMs / 1000) / 1000; + this.diagnostics.bitrateKbps = Math.max(0, Math.round(kbps)); + } + + // Calculate packet loss percentage over the interval + const packetsDelta = packetsReceived - this.lastStatsSample.packetsReceived; + const lostDelta = packetsLost - this.lastStatsSample.packetsLost; + if (packetsDelta > 0) { + const totalPackets = packetsDelta + lostDelta; + this.diagnostics.packetLossPercent = totalPackets > 0 + ? (lostDelta / totalPackets) * 100 + : 0; + } + } + + // Store current values for next delta calculation + this.lastStatsSample = { + bytesReceived: bytes, + framesReceived, + framesDecoded, + framesDropped, + packetsReceived, + packetsLost, + atMs: now, + }; + + // Frame counters + this.diagnostics.framesReceived = framesReceived; + this.diagnostics.framesDecoded = framesDecoded; + this.diagnostics.framesDropped = framesDropped; + + if ( + !this.videoDecodeStallWarningSent && + framesReceived > 100 && + framesDecoded === 0 + ) { + this.videoDecodeStallWarningSent = true; + this.log("Warning: inbound video packets received but 0 frames decoded (decoder stall)"); + } + + // Decode FPS + this.diagnostics.decodeFps = Math.round(Number(inboundVideo.framesPerSecond ?? 0)); + + // Cumulative packet stats + this.diagnostics.packetsLost = packetsLost; + this.diagnostics.packetsReceived = packetsReceived; + + // Jitter (converted to milliseconds) + this.diagnostics.jitterMs = Math.round(Number(inboundVideo.jitter ?? 0) * 1000 * 10) / 10; + + // Jitter buffer delay — the actual buffering latency added by the jitter buffer. + // jitterBufferDelay is cumulative seconds, jitterBufferEmittedCount is cumulative frames. + // Average = (delay / emittedCount) * 1000 for milliseconds. + const jbDelay = Number(inboundVideo.jitterBufferDelay ?? 0); + const jbEmitted = Number(inboundVideo.jitterBufferEmittedCount ?? 0); + if (jbEmitted > 0) { + this.diagnostics.jitterBufferDelayMs = Math.round((jbDelay / jbEmitted) * 1000 * 10) / 10; + } + + // Get codec information + const codecId = inboundVideo.codecId as string; + if (codecId && codecs.has(codecId)) { + const codecStats = codecs.get(codecId)!; + const mimeType = (codecStats.mimeType as string) || ""; + const sdpFmtpLine = (codecStats.sdpFmtpLine as string) || ""; + + // Extract codec name from MIME type + if (mimeType.includes("H264")) { + this.currentCodec = "H264"; + } else if (mimeType.includes("H265") || mimeType.includes("HEVC")) { + this.currentCodec = "H265"; + } else if (mimeType.includes("AV1")) { + this.currentCodec = "AV1"; + } else if (mimeType.includes("VP9")) { + this.currentCodec = "VP9"; + } else if (mimeType.includes("VP8")) { + this.currentCodec = "VP8"; + } else { + // Try to extract from codecId itself + this.currentCodec = normalizeCodecName(codecId); + } + + // Check for HDR in SDP fmtp line + this.isHdr = sdpFmtpLine.includes("transfer-characteristics=16") || + sdpFmtpLine.includes("hdr") || + sdpFmtpLine.includes("HDR"); + + this.diagnostics.codec = this.currentCodec; + this.diagnostics.isHdr = this.isHdr; + } + + // Get video dimensions from track settings if available + const videoTrack = this.videoStream.getVideoTracks()[0]; + if (videoTrack) { + const settings = videoTrack.getSettings(); + if (settings.width && settings.height) { + this.currentResolution = `${settings.width}x${settings.height}`; + this.diagnostics.resolution = this.currentResolution; + } + } + + // Get decode timing if available + const totalDecodeTime = Number(inboundVideo.totalDecodeTime ?? 0); + const totalInterFrameDelay = Number(inboundVideo.totalInterFrameDelay ?? 0); + const framesDecodedForTiming = Number(inboundVideo.framesDecoded ?? 1); + + if (framesDecodedForTiming > 0) { + this.diagnostics.decodeTimeMs = Math.round((totalDecodeTime / framesDecodedForTiming) * 1000 * 10) / 10; + } + + // Estimate render time from inter-frame delay + if (totalInterFrameDelay > 0 && framesDecodedForTiming > 1) { + const avgFrameDelay = totalInterFrameDelay / (framesDecodedForTiming - 1); + this.diagnostics.renderTimeMs = Math.round(avgFrameDelay * 1000 * 10) / 10; + } + } + + // RTT from active candidate pair + if (activePair?.currentRoundTripTime !== undefined) { + const rtt = Number(activePair.currentRoundTripTime); + this.diagnostics.rttMs = Math.round(rtt * 1000 * 10) / 10; + } + + const reliableBufferedAmount = this.reliableInputChannel?.bufferedAmount ?? 0; + this.inputQueuePeakBufferedBytesWindow = Math.max( + this.inputQueuePeakBufferedBytesWindow, + reliableBufferedAmount, + ); + this.diagnostics.inputQueueBufferedBytes = reliableBufferedAmount; + this.diagnostics.inputQueuePeakBufferedBytes = this.inputQueuePeakBufferedBytesWindow; + this.diagnostics.inputQueueDropCount = this.inputQueueDropCount; + this.diagnostics.inputQueueMaxSchedulingDelayMs = + Math.round(this.inputQueueMaxSchedulingDelayMsWindow * 10) / 10; + + const shouldLogQueuePressure = + reliableBufferedAmount > GfnWebRtcClient.RELIABLE_MOUSE_BACKPRESSURE_BYTES / 2 + || this.inputQueueMaxSchedulingDelayMsWindow >= 4 + || this.inputQueueDropCount > 0; + + if (shouldLogQueuePressure) { + const nowMs = performance.now(); + if (nowMs - this.inputQueuePressureLoggedAtMs >= GfnWebRtcClient.BACKPRESSURE_LOG_INTERVAL_MS) { + this.inputQueuePressureLoggedAtMs = nowMs; + this.log( + `Input queue pressure: buffered=${reliableBufferedAmount}B peak=${this.inputQueuePeakBufferedBytesWindow}B drops=${this.inputQueueDropCount} maxSchedDelay=${this.diagnostics.inputQueueMaxSchedulingDelayMs.toFixed(1)}ms`, + ); + } + } + + this.inputQueuePeakBufferedBytesWindow = reliableBufferedAmount; + this.inputQueueMaxSchedulingDelayMsWindow = 0; + + this.emitStats(); + } + + private detachInputCapture(): void { + for (const cleanup of this.inputCleanup.splice(0)) { + cleanup(); + } + } + + private replaceTrackInStream(stream: MediaStream, track: MediaStreamTrack): void { + const existingTracks = track.kind === "video" + ? stream.getVideoTracks() + : stream.getAudioTracks(); + + for (const existingTrack of existingTracks) { + stream.removeTrack(existingTrack); + } + + stream.addTrack(track); + } + + private cleanupPeerConnection(): void { + this.clearTimers(); + this.detachInputCapture(); + this.closeDataChannels(); + if (this.audioContext) { + void this.audioContext.close(); + this.audioContext = null; + } + this.options.audioElement.pause(); + this.options.audioElement.muted = true; + if (this.pc) { + this.pc.onicecandidate = null; + this.pc.ontrack = null; + this.pc.onconnectionstatechange = null; + this.pc.ondatachannel = null; + this.pc.close(); + this.pc = null; + } + + // Remove old tracks so reconnects don't accumulate ended tracks in srcObject streams. + for (const track of this.videoStream.getTracks()) { + this.videoStream.removeTrack(track); + } + for (const track of this.audioStream.getTracks()) { + this.audioStream.removeTrack(track); + } + + this.resetInputState(); + this.resetDiagnostics(); + this.connectedGamepads.clear(); + this.previousGamepadStates.clear(); + this.gamepadSendCount = 0; + this.lastGamepadSendMs = 0; + this.lastGamepadActivityMs = 0; + this.reliableDropLogged = false; + this.activeInputMode = "mkb"; + this.gamepadBitmap = 0; + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.pendingMouseTimestampUs = null; + this.mouseDeltaFilter.reset(); + this.mouseFlushLastTickMs = 0; + this.inputQueuePeakBufferedBytesWindow = 0; + this.inputQueueMaxSchedulingDelayMsWindow = 0; + this.inputQueueDropCount = 0; + this.inputQueuePressureLoggedAtMs = 0; + this.inputEncoder.resetGamepadSequences(); + } + + private attachTrack(track: MediaStreamTrack): void { + if (track.kind === "video") { + this.replaceTrackInStream(this.videoStream, track); + + // Set up render FPS tracking using video element + const video = this.options.videoElement; + const frameCallback = () => { + this.updateRenderFps(); + if (this.videoStream.active) { + video.requestVideoFrameCallback(frameCallback); + } + }; + video.requestVideoFrameCallback(frameCallback); + + this.log( + `Video element before play: paused=${video.paused}, readyState=${video.readyState}, size=${video.videoWidth}x${video.videoHeight}`, + ); + + // Explicitly start video playback after track attachment. + // Some Chromium/Electron builds keep the video element paused even with autoplay. + video + .play() + .then(() => { + this.log("Video element playback started"); + }) + .catch((playError) => { + this.log(`Video play() failed: ${String(playError)}`); + }); + + window.setTimeout(() => { + this.log( + `Video element post-play: paused=${video.paused}, readyState=${video.readyState}, size=${video.videoWidth}x${video.videoHeight}`, + ); + }, 1500); + + track.onunmute = () => { + this.log("Video track unmuted"); + }; + track.onmute = () => { + this.log("Warning: video track muted by sender"); + }; + track.onended = () => { + this.log("Warning: video track ended"); + }; + + this.log("Video track attached"); + return; + } + + if (track.kind === "audio") { + this.replaceTrackInStream(this.audioStream, track); + + if (this.audioContext) { + void this.audioContext.close(); + this.audioContext = null; + } + + this.options.audioElement.pause(); + this.options.audioElement.muted = true; + + // Route audio through an AudioContext with interactive latency hint. + // This tells the OS audio subsystem to use the smallest possible buffer, + // matching what the official GFN browser client does for low-latency playback. + try { + const ctx = new AudioContext({ + latencyHint: "interactive", + sampleRate: 48000, + }); + this.audioContext = ctx; + const source = ctx.createMediaStreamSource(this.audioStream); + source.connect(ctx.destination); + + // Resume the context (browsers require user gesture, but Electron is more lenient) + if (ctx.state === "suspended") { + void ctx.resume(); + } + + this.log(`Audio routed through AudioContext (latency: ${(ctx.baseLatency * 1000).toFixed(1)}ms, sampleRate: ${ctx.sampleRate}Hz)`); + } catch (error) { + // Fallback: play directly through the audio element + this.log(`AudioContext creation failed, falling back to audio element: ${String(error)}`); + this.options.audioElement.muted = false; + this.options.audioElement + .play() + .then(() => { + this.log("Audio track attached (fallback)"); + }) + .catch((playError) => { + this.log(`Audio autoplay blocked: ${String(playError)}`); + }); + } + } + } + + private async waitForIceGathering(pc: RTCPeerConnection, timeoutMs: number): Promise { + if (pc.iceGatheringState === "complete" && pc.localDescription?.sdp) { + return pc.localDescription.sdp; + } + + await new Promise((resolve) => { + let settled = false; + const done = () => { + if (!settled) { + settled = true; + pc.removeEventListener("icegatheringstatechange", onStateChange); + resolve(); + } + }; + + const onStateChange = () => { + if (pc.iceGatheringState === "complete") { + done(); + } + }; + + pc.addEventListener("icegatheringstatechange", onStateChange); + window.setTimeout(done, timeoutMs); + }); + + const sdp = pc.localDescription?.sdp; + if (!sdp) { + throw new Error("Missing local SDP after ICE gathering"); + } + return sdp; + } + + private setupInputHeartbeat(): void { + if (this.heartbeatTimer !== null) { + window.clearInterval(this.heartbeatTimer); + } + + this.heartbeatTimer = window.setInterval(() => { + if (!this.inputReady) { + return; + } + const bytes = this.inputEncoder.encodeHeartbeat(); + this.sendReliable(bytes); + }, 2000); + } + + private setupGamepadPolling(): void { + if (this.gamepadPollTimer !== null) { + window.clearInterval(this.gamepadPollTimer); + } + + this.log("Gamepad polling started (250Hz)"); + + // Poll at 250Hz (4ms interval) — the practical minimum for setInterval in browsers. + // The Rust reference polls at 1000Hz; browser timers can't go below ~4ms reliably. + // Previous 60Hz (16.6ms) added up to 1-2 frames of input lag at 120fps. + this.gamepadPollTimer = window.setInterval(() => { + if (!this.inputReady) { + return; + } + this.pollGamepads(); + }, 4); + } + + private gamepadSendCount = 0; + + private pollGamepads(): void { + const gamepads = navigator.getGamepads(); + if (!gamepads) { + return; + } + + let connectedCount = 0; + const nowMs = performance.now(); + + for (let i = 0; i < Math.min(gamepads.length, GAMEPAD_MAX_CONTROLLERS); i++) { + const gamepad = gamepads[i]; + + if (gamepad && gamepad.connected) { + connectedCount++; + + // Track connected gamepads and update bitmap + if (!this.connectedGamepads.has(i)) { + this.connectedGamepads.add(i); + // Set bit i in bitmap (matching official client's AA(i) = 1 << i) + this.gamepadBitmap |= (1 << i); + this.log(`Gamepad ${i} connected: ${gamepad.id}`); + this.log(` Buttons: ${gamepad.buttons.length}, Axes: ${gamepad.axes.length}, Mapping: ${gamepad.mapping}`); + this.log(` Bitmap now: 0x${this.gamepadBitmap.toString(16)}`); + this.diagnostics.connectedGamepads = this.connectedGamepads.size; + this.emitStats(); + } + + // Read and encode gamepad state + const gamepadInput = this.readGamepadState(gamepad, i); + const stateChanged = this.hasGamepadStateChanged(i, gamepadInput); + + // Send if state changed OR as a keepalive to maintain server controller presence + // Games detect active input device by receiving packets; if we stop sending, + // the game falls back to showing keyboard/mouse prompts. + const needsKeepalive = this.activeInputMode === "gamepad" + && !stateChanged + && (nowMs - this.lastGamepadSendMs) >= GfnWebRtcClient.GAMEPAD_KEEPALIVE_MS; + + if (stateChanged || needsKeepalive) { + // Determine if we should use the partially reliable channel + const usePR = this.mouseInputChannel?.readyState === "open"; + const bytes = this.inputEncoder.encodeGamepadState(gamepadInput, this.gamepadBitmap, usePR); + this.sendGamepad(bytes); + this.lastGamepadSendMs = nowMs; + + if (stateChanged) { + this.previousGamepadStates.set(i, { ...gamepadInput }); + this.lastGamepadActivityMs = nowMs; + } + + // Switch to gamepad input mode — suppresses mouse/keyboard + if (this.activeInputMode !== "gamepad") { + this.activeInputMode = "gamepad"; + // Discard any pending mouse deltas to avoid a stale burst + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.log("Input mode → gamepad"); + } + + // Log first N gamepad sends for debugging + if (stateChanged) { + this.gamepadSendCount++; + if (this.gamepadSendCount <= 20) { + this.log(`Gamepad send #${this.gamepadSendCount}: pad=${i} btns=0x${gamepadInput.buttons.toString(16)} lt=${gamepadInput.leftTrigger} rt=${gamepadInput.rightTrigger} lx=${gamepadInput.leftStickX} ly=${gamepadInput.leftStickY} rx=${gamepadInput.rightStickX} ry=${gamepadInput.rightStickY} bytes=${bytes.length}`); + } + } + } + } else if (this.connectedGamepads.has(i)) { + // Gamepad disconnected — clear bit from bitmap + this.connectedGamepads.delete(i); + this.previousGamepadStates.delete(i); + this.gamepadBitmap &= ~(1 << i); + this.log(`Gamepad ${i} disconnected, bitmap now: 0x${this.gamepadBitmap.toString(16)}`); + this.diagnostics.connectedGamepads = this.connectedGamepads.size; + this.emitStats(); + + // Send state with updated bitmap (gamepad bit cleared = disconnected) + const disconnectState: GamepadInput = { + controllerId: i, + buttons: 0, + leftTrigger: 0, + rightTrigger: 0, + leftStickX: 0, + leftStickY: 0, + rightStickX: 0, + rightStickY: 0, + connected: false, + timestampUs: timestampUs(), + }; + const usePR = this.mouseInputChannel?.readyState === "open"; + const bytes = this.inputEncoder.encodeGamepadState(disconnectState, this.gamepadBitmap, usePR); + this.sendGamepad(bytes); + } + } + + this.diagnostics.connectedGamepads = connectedCount; + } + + private readGamepadState(gamepad: Gamepad, controllerId: number): GamepadInput { + const buttons = mapGamepadButtons(gamepad); + const axes = readGamepadAxes(gamepad); + + return { + controllerId, + buttons, + leftTrigger: normalizeToUint8(axes.leftTrigger), + rightTrigger: normalizeToUint8(axes.rightTrigger), + leftStickX: normalizeToInt16(axes.leftStickX), + leftStickY: normalizeToInt16(axes.leftStickY), + rightStickX: normalizeToInt16(axes.rightStickX), + rightStickY: normalizeToInt16(axes.rightStickY), + connected: true, + timestampUs: timestampUs(), + }; + } + + private hasGamepadStateChanged(controllerId: number, newState: GamepadInput): boolean { + const prevState = this.previousGamepadStates.get(controllerId); + if (!prevState) { + return true; + } + + return ( + prevState.buttons !== newState.buttons || + prevState.leftTrigger !== newState.leftTrigger || + prevState.rightTrigger !== newState.rightTrigger || + prevState.leftStickX !== newState.leftStickX || + prevState.leftStickY !== newState.leftStickY || + prevState.rightStickX !== newState.rightStickX || + prevState.rightStickY !== newState.rightStickY + ); + } + + private onGamepadConnected = (event: GamepadEvent): void => { + this.log(`Gamepad connected event: ${event.gamepad.id}`); + // The polling loop will detect and handle the new gamepad + }; + + private onGamepadDisconnected = (event: GamepadEvent): void => { + this.log(`Gamepad disconnected event: ${event.gamepad.id}`); + // The polling loop will detect and handle the disconnection + }; + + private onInputHandshakeMessage(bytes: Uint8Array): void { + if (bytes.length < 2) { + this.log(`Input handshake: ignoring short message (${bytes.length} bytes)`); + return; + } + + const hex = Array.from(bytes.slice(0, Math.min(bytes.length, 16))) + .map((b) => b.toString(16).padStart(2, "0")) + .join(" "); + this.log(`Input channel message: ${bytes.length} bytes [${hex}]`); + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const firstWord = view.getUint16(0, true); + let version = 2; + + if (firstWord === 526) { + version = bytes.length >= 4 ? view.getUint16(2, true) : 2; + this.log(`Handshake detected: firstWord=526 (0x020e), version=${version}`); + } else if (bytes[0] === 0x0e) { + version = firstWord; + this.log(`Handshake detected: byte[0]=0x0e, version=${version}`); + } else { + this.log(`Input channel message not a handshake: firstWord=${firstWord} (0x${firstWord.toString(16)})`); + return; + } + + if (!this.inputReady) { + // Official GFN browser client does NOT echo the handshake back. + // It just reads the protocol version and starts sending input. + // (The Rust reference implementation does echo, but that's for its own server.) + this.inputReady = true; + this.inputProtocolVersion = version; + this.inputEncoder.setProtocolVersion(version); + this.diagnostics.inputReady = true; + this.emitStats(); + this.log(`Input handshake complete (protocol v${version}) — starting heartbeat + gamepad polling`); + this.setupInputHeartbeat(); + this.setupGamepadPolling(); + } + } + + private createDataChannels(pc: RTCPeerConnection): void { + this.reliableInputChannel = pc.createDataChannel("input_channel_v1", { + ordered: true, + }); + + this.reliableInputChannel.onopen = () => { + this.log("Reliable input channel open"); + }; + + this.reliableInputChannel.onmessage = async (event) => { + const bytes = await toBytes(event.data as string | Blob | ArrayBuffer); + this.onInputHandshakeMessage(bytes); + }; + + this.mouseInputChannel = pc.createDataChannel("input_channel_partially_reliable", { + ordered: false, + maxPacketLifeTime: this.partialReliableThresholdMs, + }); + + this.mouseInputChannel.onopen = () => { + this.log(`Mouse channel open (partially reliable, maxPacketLifeTime=${this.partialReliableThresholdMs}ms)`); + }; + } + + private mapTimerNotificationCode(rawCode: number): StreamTimeWarning["code"] | null { + // Mirrors official client behavior from timerNotification -> StreamWarningType. + if (rawCode === 1 || rawCode === 2) { + return 1; + } + if (rawCode === 4) { + return 2; + } + if (rawCode === 6) { + return 3; + } + return null; + } + + private async onControlChannelMessage(data: string | Blob | ArrayBuffer): Promise { + let payloadText: string; + if (typeof data === "string") { + payloadText = data; + } else if (data instanceof Blob) { + payloadText = await data.text(); + } else if (data instanceof ArrayBuffer) { + payloadText = new TextDecoder().decode(data); + } else { + return; + } + + let parsed: unknown; + try { + parsed = JSON.parse(payloadText); + } catch { + return; + } + + if (!parsed || typeof parsed !== "object" || !("timerNotification" in parsed)) { + return; + } + + const timerNotification = (parsed as { timerNotification?: unknown }).timerNotification; + if (!timerNotification || typeof timerNotification !== "object") { + return; + } + + const rawCode = Number((timerNotification as { code?: unknown }).code); + const mappedCode = this.mapTimerNotificationCode(rawCode); + if (mappedCode === null) { + this.log(`Control timer notification ignored: code=${rawCode}`); + return; + } + + const rawSecondsLeft = Number((timerNotification as { secondsLeft?: unknown }).secondsLeft); + const secondsLeft = + Number.isFinite(rawSecondsLeft) && rawSecondsLeft >= 0 + ? Math.floor(rawSecondsLeft) + : undefined; + this.log( + `Control timer warning: rawCode=${rawCode} mappedCode=${mappedCode} secondsLeft=${secondsLeft ?? "n/a"}`, + ); + this.options.onTimeWarning?.({ code: mappedCode, secondsLeft }); + } + + private async flushQueuedCandidates(): Promise { + if (!this.pc || !this.pc.remoteDescription) { + return; + } + + while (this.queuedCandidates.length > 0) { + const candidate = this.queuedCandidates.shift(); + if (!candidate) { + continue; + } + await this.pc.addIceCandidate(candidate); + } + } + + private reliableDropLogged = false; + + public sendReliable(payload: Uint8Array): void { + if (this.reliableInputChannel?.readyState === "open") { + const safePayload = Uint8Array.from(payload); + this.reliableInputChannel.send(safePayload.buffer); + } else if (!this.reliableDropLogged) { + this.reliableDropLogged = true; + this.log(`Reliable channel not open (state=${this.reliableInputChannel?.readyState ?? "null"}), dropping event (${payload.length} bytes)`); + } + } + + private async lockEscapeInFullscreen(): Promise { + const nav = navigator as any; + if (!document.fullscreenElement) { + return; + } + if (!nav.keyboard?.lock) { + return; + } + + try { + await nav.keyboard.lock([ + "Escape", "F11", "BrowserBack", "BrowserForward", "BrowserRefresh", + ]); + this.log("Keyboard lock acquired (Escape captured in fullscreen)"); + } catch (error) { + this.log(`Keyboard lock failed: ${String(error)}`); + } + } + + private async requestPointerLockWithEscGuard( + videoElement: HTMLVideoElement, + ensureFullscreen: boolean, + ): Promise { + if (ensureFullscreen && !document.fullscreenElement) { + try { + await document.documentElement.requestFullscreen(); + } catch (error) { + this.log(`Fullscreen request failed: ${String(error)}`); + } + } + + await this.lockEscapeInFullscreen(); + + try { + await (videoElement.requestPointerLock({ unadjustedMovement: true } as any) as unknown as Promise); + this.log("Pointer lock acquired with unadjustedMovement=true (raw/unaccelerated)"); + } catch (err) { + const domErr = err as DOMException; + if (domErr?.name === "NotSupportedError") { + this.log("unadjustedMovement not supported, falling back to standard pointer lock (accelerated)"); + await videoElement.requestPointerLock(); + } else { + throw err; + } + } + } + + private clearEscapeHoldTimer(): void { + if (this.escapeHoldReleaseTimer !== null) { + window.clearTimeout(this.escapeHoldReleaseTimer); + this.escapeHoldReleaseTimer = null; + } + if (this.escapeHoldIndicatorDelayTimer !== null) { + window.clearTimeout(this.escapeHoldIndicatorDelayTimer); + this.escapeHoldIndicatorDelayTimer = null; + } + if (this.escapeHoldProgressTimer !== null) { + window.clearInterval(this.escapeHoldProgressTimer); + this.escapeHoldProgressTimer = null; + } + this.escapeHoldStartedAtMs = null; + this.options.onEscHoldProgress?.(false, 0); + } + + private clearEscapeAutoKeyUpTimer(): void { + if (this.escapeAutoKeyUpTimer !== null) { + window.clearTimeout(this.escapeAutoKeyUpTimer); + this.escapeAutoKeyUpTimer = null; + } + } + + private scheduleEscapeAutoKeyUp(scancode: number): void { + this.clearEscapeAutoKeyUpTimer(); + this.escapeAutoKeyUpTimer = window.setTimeout(() => { + this.escapeAutoKeyUpTimer = null; + if (!this.inputReady) { + return; + } + if (!this.pressedKeys.has(0x1B)) { + return; + } + + this.pressedKeys.delete(0x1B); + const payload = this.inputEncoder.encodeKeyUp({ + keycode: 0x1B, + scancode, + modifiers: 0, + timestampUs: timestampUs(), + }); + this.sendReliable(payload); + this.log("Sent Escape keyup fallback (browser suppressed keyup)"); + }, 120); + } + + private startEscapeHoldRelease(videoElement: HTMLVideoElement): void { + if (this.escapeHoldReleaseTimer !== null) { + return; + } + + this.escapeHoldStartedAtMs = performance.now(); + this.options.onEscHoldProgress?.(false, 0); + + // Show indicator only after 300ms hold, then fill for remaining 4.7s. + this.escapeHoldIndicatorDelayTimer = window.setTimeout(() => { + this.escapeHoldIndicatorDelayTimer = null; + }, 300); + + this.escapeHoldProgressTimer = window.setInterval(() => { + if (this.escapeHoldStartedAtMs === null) { + return; + } + const elapsedMs = performance.now() - this.escapeHoldStartedAtMs; + if (elapsedMs < 300) { + return; + } + const progress = Math.min(1, (elapsedMs - 300) / 4700); + this.options.onEscHoldProgress?.(true, progress); + }, 50); + + this.escapeHoldReleaseTimer = window.setTimeout(() => { + this.escapeHoldReleaseTimer = null; + this.clearEscapeHoldTimer(); + if (document.pointerLockElement === videoElement) { + this.log("Escape held for 5s, releasing pointer lock"); + this.suppressNextSyntheticEscape = true; + // Remove Escape from pressedKeys so keyup doesn't send it to stream + this.pressedKeys.delete(0x1B); + document.exitPointerLock(); + } + }, 5000); + } + + private shouldSendSyntheticEscapeOnPointerLockLoss(): boolean { + if (document.visibilityState !== "visible") { + return false; + } + if (typeof document.hasFocus === "function" && !document.hasFocus()) { + return false; + } + return true; + } + + private releasePressedKeys(reason: string): void { + this.clearEscapeAutoKeyUpTimer(); + if (this.pressedKeys.size === 0 || !this.inputReady) { + this.pressedKeys.clear(); + return; + } + + this.log(`Releasing ${this.pressedKeys.size} key(s): ${reason}`); + for (const vk of this.pressedKeys) { + const payload = this.inputEncoder.encodeKeyUp({ + keycode: vk, + scancode: 0, + modifiers: 0, + timestampUs: timestampUs(), + }); + this.sendReliable(payload); + } + this.pressedKeys.clear(); + } + + private sendKeyPacket(vk: number, scancode: number, modifiers: number, isDown: boolean): void { + const payload = isDown + ? this.inputEncoder.encodeKeyDown({ + keycode: vk, + scancode, + modifiers, + timestampUs: timestampUs(), + }) + : this.inputEncoder.encodeKeyUp({ + keycode: vk, + scancode, + modifiers, + timestampUs: timestampUs(), + }); + this.sendReliable(payload); + } + + private ensureKeyboardInputMode(): boolean { + if (this.activeInputMode !== "gamepad") { + return true; + } + const idleMs = performance.now() - this.lastGamepadActivityMs; + if (idleMs < GfnWebRtcClient.GAMEPAD_MODE_LOCKOUT_MS) { + return false; + } + this.activeInputMode = "mkb"; + this.log("Input mode → mouse+keyboard (gamepad idle)"); + return true; + } + + public sendAntiAfkPulse(): boolean { + if (!this.inputReady) { + return false; + } + + this.sendKeyPacket(0x7c, 0x64, 0, true); // F13 down + window.setTimeout(() => this.sendKeyPacket(0x7c, 0x64, 0, false), 50); // F13 up + return true; + } + + public sendPasteShortcut(useMeta: boolean): boolean { + if (!this.inputReady || !this.ensureKeyboardInputMode()) { + return false; + } + + const modifier = useMeta + ? { vk: 0x5b, scancode: 0xe3, flag: 0x08 } // Meta/Command + : { vk: 0xa2, scancode: 0xe0, flag: 0x02 }; // Ctrl + + this.sendKeyPacket(modifier.vk, modifier.scancode, modifier.flag, true); + this.sendKeyPacket(0x56, 0x19, modifier.flag, true); // V down + this.sendKeyPacket(0x56, 0x19, modifier.flag, false); // V up + this.sendKeyPacket(modifier.vk, modifier.scancode, 0, false); + return true; + } + + public sendText(text: string): number { + if (!this.inputReady || !text || !this.ensureKeyboardInputMode()) { + return 0; + } + + let sent = 0; + const maxChars = 4096; + for (const char of text.slice(0, maxChars)) { + const key = mapTextCharToKeySpec(char); + if (!key) { + continue; + } + + if (key.shift) { + this.sendKeyPacket(0xa0, 0xe1, 0x01, true); // Shift down + } + + const mods = key.shift ? 0x01 : 0; + this.sendKeyPacket(key.vk, key.scancode, mods, true); + this.sendKeyPacket(key.vk, key.scancode, mods, false); + + if (key.shift) { + this.sendKeyPacket(0xa0, 0xe1, 0, false); // Shift up + } + + sent++; + } + + return sent; + } + + /** Send gamepad data on the partially reliable channel (unordered, maxPacketLifeTime). + * Falls back to reliable channel if partially reliable isn't available. + * Official GFN client uses partially reliable ONLY for gamepad, not mouse. */ + private sendGamepad(payload: Uint8Array): void { + if (this.mouseInputChannel?.readyState === "open") { + const safePayload = Uint8Array.from(payload); + this.mouseInputChannel.send(safePayload.buffer); + return; + } + // Fallback to reliable channel if partially reliable not ready + this.sendReliable(payload); + } + + private installInputCapture(videoElement: HTMLVideoElement): void { + this.detachInputCapture(); + + const hasPointerRawUpdate = "onpointerrawupdate" in videoElement; + const hasCoalescedEvents = + typeof PointerEvent !== "undefined" && "getCoalescedEvents" in PointerEvent.prototype; + const pointerMoveEventName: "pointerrawupdate" | "pointermove" | null = hasPointerRawUpdate + ? "pointerrawupdate" + : (typeof PointerEvent !== "undefined" ? "pointermove" : null); + + this.mouseFlushIntervalMs = hasPointerRawUpdate + ? GfnWebRtcClient.MOUSE_FLUSH_FAST_MS + : hasCoalescedEvents + ? GfnWebRtcClient.MOUSE_FLUSH_NORMAL_MS + : GfnWebRtcClient.MOUSE_FLUSH_SAFE_MS; + this.mouseFlushLastTickMs = performance.now(); + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.pendingMouseTimestampUs = null; + this.mouseDeltaFilter.reset(); + this.log( + `Mouse input mode: ${pointerMoveEventName ?? "mousemove"}, coalesced=${hasCoalescedEvents ? "yes" : "no"}, flush=${this.mouseFlushIntervalMs}ms`, + ); + + const flushMouse = () => { + const tickNow = performance.now(); + if (this.mouseFlushLastTickMs > 0) { + const expected = this.mouseFlushLastTickMs + this.mouseFlushIntervalMs; + const schedulingDelay = Math.max(0, tickNow - expected); + this.inputQueueMaxSchedulingDelayMsWindow = Math.max( + this.inputQueueMaxSchedulingDelayMsWindow, + schedulingDelay, + ); + } + this.mouseFlushLastTickMs = tickNow; + + if (!this.inputReady) { + return; + } + + if (this.activeInputMode === "gamepad") { + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.pendingMouseTimestampUs = null; + return; + } + + if (this.pendingMouseDx === 0 && this.pendingMouseDy === 0) { + return; + } + + const reliable = this.reliableInputChannel; + if ( + reliable?.readyState === "open" + && reliable.bufferedAmount > GfnWebRtcClient.RELIABLE_MOUSE_BACKPRESSURE_BYTES + ) { + const now = performance.now(); + this.inputQueueDropCount++; + if (now - this.mouseBackpressureLoggedAtMs >= GfnWebRtcClient.BACKPRESSURE_LOG_INTERVAL_MS) { + this.mouseBackpressureLoggedAtMs = now; + this.log(`Dropping stale mouse movement (reliable bufferedAmount=${reliable.bufferedAmount})`); + } + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.pendingMouseTimestampUs = null; + return; + } + + const payload = this.inputEncoder.encodeMouseMove({ + dx: Math.max(-32768, Math.min(32767, this.pendingMouseDx)), + dy: Math.max(-32768, Math.min(32767, this.pendingMouseDy)), + timestampUs: this.pendingMouseTimestampUs ?? timestampUs(), + }); + + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.pendingMouseTimestampUs = null; + this.sendReliable(payload); + }; + + this.mouseFlushTimer = window.setInterval(flushMouse, this.mouseFlushIntervalMs); + + const queueMouseMovement = (dx: number, dy: number, eventTimestampMs: number): void => { + if (!this.inputReady || document.pointerLockElement !== videoElement) { + return; + } + + if (this.activeInputMode === "gamepad") { + return; + } + + if (!this.mouseDeltaFilter.update(dx, dy, eventTimestampMs)) { + return; + } + + this.pendingMouseDx += Math.round(this.mouseDeltaFilter.getX()); + this.pendingMouseDy += Math.round(this.mouseDeltaFilter.getY()); + this.pendingMouseTimestampUs = timestampUs(eventTimestampMs); + }; + + const onPointerMove = (event: PointerEvent) => { + if (event.pointerType && event.pointerType !== "mouse") { + return; + } + + const samples = hasCoalescedEvents ? event.getCoalescedEvents() : []; + if (samples.length > 0) { + for (const sample of samples) { + queueMouseMovement(sample.movementX, sample.movementY, sample.timeStamp); + } + return; + } + + queueMouseMovement(event.movementX, event.movementY, event.timeStamp); + }; + + const onMouseMove = (event: MouseEvent) => { + queueMouseMovement(event.movementX, event.movementY, event.timeStamp); + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (!this.inputReady) { + return; + } + + const isEscapeEvent = + event.key === "Escape" + || event.key === "Esc" + || event.code === "Escape" + || event.keyCode === 27; + const mapped = mapKeyboardEvent(event) ?? (isEscapeEvent ? { vk: 0x1B, scancode: 0x29 } : null); + + // Keep browser from handling held keys (for example Tab focus traversal) + // while streaming input is active. + if (event.repeat) { + if (document.pointerLockElement === videoElement || mapped) { + event.preventDefault(); + } + return; + } + + if (document.pointerLockElement === videoElement) { + event.preventDefault(); + } + + if (!mapped) { + return; + } + + // Don't send keyboard input while gamepad was recently active. + // This prevents accidental key presses from making the game switch + // to showing keyboard/mouse prompts. The user must put down the + // controller for a few seconds before keyboard input takes over. + if (this.activeInputMode === "gamepad") { + const idleMs = performance.now() - this.lastGamepadActivityMs; + if (idleMs < GfnWebRtcClient.GAMEPAD_MODE_LOCKOUT_MS) { + return; + } + // Gamepad idle long enough — allow switch to mkb + this.activeInputMode = "mkb"; + this.log("Input mode → mouse+keyboard (gamepad idle)"); + } + + event.preventDefault(); + this.pressedKeys.add(mapped.vk); + + if (mapped.vk === 0x1B && document.pointerLockElement === videoElement) { + // Escape with pointer lock active: we start the hold timer for hold-to-exit. + // For a quick tap (< 5s), we send Escape on keyup (not here) so we can distinguish tap vs hold. + // For a hold (>= 5s), pointer lock is released and we suppress sending Escape to stream. + this.escapeTapDispatchedForCurrentHold = false; + this.clearEscapeAutoKeyUpTimer(); + // Start the hold timer (will be cleared on keyup if released before 5s) + this.startEscapeHoldRelease(videoElement); + // Don't send keydown yet - wait to see if this is a tap or hold + return; + } + + const payload = this.inputEncoder.encodeKeyDown({ + keycode: mapped.vk, + scancode: mapped.scancode, + modifiers: modifierFlags(event), + // Use a fresh monotonic timestamp for keyboard events. In some + // fullscreen/keyboard-lock paths, event.timeStamp can be unstable. + timestampUs: timestampUs(), + }); + this.sendReliable(payload); + }; + + const onKeyUp = (event: KeyboardEvent) => { + if (!this.inputReady || this.activeInputMode === "gamepad") { + return; + } + + const isEscapeEvent = + event.key === "Escape" + || event.key === "Esc" + || event.code === "Escape" + || event.keyCode === 27; + const mapped = mapKeyboardEvent(event) ?? (isEscapeEvent ? { vk: 0x1B, scancode: 0x29 } : null); + if (!mapped) { + return; + } + + event.preventDefault(); + if (mapped.vk === 0x1B) { + this.clearEscapeAutoKeyUpTimer(); + // Check if the hold timer still exists - if so, this was a tap (not a hold) + const wasTap = this.escapeHoldReleaseTimer !== null; + this.clearEscapeHoldTimer(); + + if (wasTap && this.pressedKeys.has(0x1B)) { + // This was a quick tap - send Escape to the stream now + this.log("Escape tap detected - sending to stream"); + this.sendKeyPacket(0x1B, mapped.scancode || 0x29, 0, true); + this.sendKeyPacket(0x1B, mapped.scancode || 0x29, 0, false); + } + // If hold timer was already cleared, hold completed and pointer lock was released. + // In that case we don't send Escape to stream. + this.pressedKeys.delete(mapped.vk); + return; + } + this.pressedKeys.delete(mapped.vk); + const payload = this.inputEncoder.encodeKeyUp({ + keycode: mapped.vk, + scancode: mapped.scancode, + modifiers: modifierFlags(event), + timestampUs: timestampUs(), + }); + this.sendReliable(payload); + }; + + const onMouseDown = (event: MouseEvent) => { + if (!this.inputReady) { + return; + } + // Don't send mouse clicks while gamepad was recently active. + // This prevents accidental clicks from making the game switch + // to showing keyboard/mouse prompts. + if (this.activeInputMode === "gamepad") { + const idleMs = performance.now() - this.lastGamepadActivityMs; + if (idleMs < GfnWebRtcClient.GAMEPAD_MODE_LOCKOUT_MS) { + return; + } + // Gamepad idle long enough — allow switch to mkb + this.activeInputMode = "mkb"; + this.log("Input mode → mouse+keyboard (gamepad idle)"); + } + event.preventDefault(); + const payload = this.inputEncoder.encodeMouseButtonDown({ + button: toMouseButton(event.button), + timestampUs: timestampUs(event.timeStamp), + }); + // Official GFN client sends all mouse events on reliable channel (input_channel_v1) + this.sendReliable(payload); + }; + + const onMouseUp = (event: MouseEvent) => { + if (!this.inputReady || this.activeInputMode === "gamepad") { + return; + } + event.preventDefault(); + const payload = this.inputEncoder.encodeMouseButtonUp({ + button: toMouseButton(event.button), + timestampUs: timestampUs(event.timeStamp), + }); + // Official GFN client sends all mouse events on reliable channel (input_channel_v1) + this.sendReliable(payload); + }; + + const onWheel = (event: WheelEvent) => { + if (!this.inputReady || this.activeInputMode === "gamepad") { + return; + } + event.preventDefault(); + // Official GFN client sends negated raw deltaY as int16 (no quantization to ±120). + // Clamp to int16 range since browser deltaY can exceed it with fast scrolling. + const delta = Math.max(-32768, Math.min(32767, Math.round(-event.deltaY))); + const payload = this.inputEncoder.encodeMouseWheel({ + delta, + timestampUs: timestampUs(event.timeStamp), + }); + this.sendReliable(payload); + }; + + const onClick = () => { + // GFN-style sequence: fullscreen -> keyboard lock (Escape) -> pointer lock. + void this.requestPointerLockWithEscGuard(videoElement, true).catch((err: DOMException) => { + this.log(`Pointer lock request failed: ${err.name}: ${err.message}`); + }); + videoElement.focus(); + }; + + // Store video element for pointer lock re-acquisition + this.videoElement = videoElement; + + // Handle pointer lock changes — send synthetic Escape when lock is lost by browser + // (matches official GFN client's "pointerLockEscape" feature) + const onPointerLockChange = () => { + if (document.pointerLockElement) { + // Pointer lock gained — cancel any pending synthetic Escape + if (this.pointerLockEscapeTimer !== null) { + window.clearTimeout(this.pointerLockEscapeTimer); + this.pointerLockEscapeTimer = null; + } + this.suppressNextSyntheticEscape = false; + this.escapeTapDispatchedForCurrentHold = false; + this.clearEscapeHoldTimer(); + return; + } + + this.clearEscapeHoldTimer(); + + // Pointer lock was lost + if (!this.inputReady) return; + + if (this.suppressNextSyntheticEscape) { + this.suppressNextSyntheticEscape = false; + this.releasePressedKeys("pointer lock intentionally released"); + return; + } + + if (!this.shouldSendSyntheticEscapeOnPointerLockLoss()) { + this.releasePressedKeys("pointer lock lost while unfocused"); + return; + } + + // VK 0x1B = 27 = Escape + const escapeWasPressed = this.pressedKeys.has(0x1B); + + if (escapeWasPressed) { + // Escape was already tracked as pressed — the normal keyup handler will fire + // and send Escape keyup to the server. No synthetic needed. + return; + } + + // Escape was NOT tracked as pressed — browser intercepted it before our keydown fired. + // Send synthetic Escape keydown+keyup after 50ms (matches official GFN client). + // Also re-acquire pointer lock so the user stays in the game. + this.pointerLockEscapeTimer = window.setTimeout(() => { + this.pointerLockEscapeTimer = null; + + if (!this.inputReady) return; + + if (!this.shouldSendSyntheticEscapeOnPointerLockLoss()) { + this.releasePressedKeys("focus changed before synthetic Escape"); + return; + } + + // Release all currently held keys first (matching official client's MS() function) + this.releasePressedKeys("pointer lock lost before synthetic Escape"); + + // Send synthetic Escape keydown + keyup + this.log("Sending synthetic Escape (pointer lock lost by browser)"); + const escDown = this.inputEncoder.encodeKeyDown({ + keycode: 0x1B, + scancode: 0x29, // Escape scancode + modifiers: 0, + timestampUs: timestampUs(), + }); + this.sendReliable(escDown); + + const escUp = this.inputEncoder.encodeKeyUp({ + keycode: 0x1B, + scancode: 0x29, + modifiers: 0, + timestampUs: timestampUs(), + }); + this.sendReliable(escUp); + + // Re-acquire pointer lock so the user stays in the game + if (this.videoElement && this.activeInputMode !== "gamepad") { + void this.requestPointerLockWithEscGuard(this.videoElement, false) + .catch(() => {}); + } + }, 50); + }; + + const onWindowBlur = () => { + this.clearEscapeHoldTimer(); + this.releasePressedKeys("window blur"); + }; + + const onVisibilityChange = () => { + if (document.visibilityState !== "visible") { + this.clearEscapeHoldTimer(); + this.releasePressedKeys(`visibility ${document.visibilityState}`); + } + }; + + // Try to lock keyboard (Escape, F11, etc.) when in fullscreen. + // This prevents the browser from processing Escape as pointer lock exit. + // Only works in fullscreen + secure context + Chromium. + const onFullscreenChange = () => { + const nav = navigator as any; + if (document.fullscreenElement) { + void this.lockEscapeInFullscreen(); + } else { + if (nav.keyboard?.unlock) { + nav.keyboard.unlock(); + } + } + }; + + // Add gamepad event listeners + window.addEventListener("gamepadconnected", this.onGamepadConnected); + window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected); + + // Use document capture for keyboard events so Escape remains observable + // when keyboard lock is active in fullscreen. + document.addEventListener("keydown", onKeyDown, true); + document.addEventListener("keyup", onKeyUp, true); + if (pointerMoveEventName) { + document.addEventListener(pointerMoveEventName, onPointerMove as EventListener); + } else { + window.addEventListener("mousemove", onMouseMove); + } + videoElement.addEventListener("mousedown", onMouseDown); + videoElement.addEventListener("mouseup", onMouseUp); + videoElement.addEventListener("wheel", onWheel, { passive: false }); + videoElement.addEventListener("click", onClick); + document.addEventListener("pointerlockchange", onPointerLockChange); + document.addEventListener("fullscreenchange", onFullscreenChange); + window.addEventListener("blur", onWindowBlur); + document.addEventListener("visibilitychange", onVisibilityChange); + + // If already in fullscreen, try to lock keyboard immediately + if (document.fullscreenElement) { + onFullscreenChange(); + } + + this.inputCleanup.push(() => window.removeEventListener("gamepadconnected", this.onGamepadConnected)); + this.inputCleanup.push(() => window.removeEventListener("gamepaddisconnected", this.onGamepadDisconnected)); + this.inputCleanup.push(() => document.removeEventListener("keydown", onKeyDown, true)); + this.inputCleanup.push(() => document.removeEventListener("keyup", onKeyUp, true)); + if (pointerMoveEventName) { + this.inputCleanup.push(() => document.removeEventListener(pointerMoveEventName, onPointerMove as EventListener)); + } else { + this.inputCleanup.push(() => window.removeEventListener("mousemove", onMouseMove)); + } + this.inputCleanup.push(() => videoElement.removeEventListener("mousedown", onMouseDown)); + this.inputCleanup.push(() => videoElement.removeEventListener("mouseup", onMouseUp)); + this.inputCleanup.push(() => videoElement.removeEventListener("wheel", onWheel)); + this.inputCleanup.push(() => videoElement.removeEventListener("click", onClick)); + this.inputCleanup.push(() => document.removeEventListener("pointerlockchange", onPointerLockChange)); + this.inputCleanup.push(() => document.removeEventListener("fullscreenchange", onFullscreenChange)); + this.inputCleanup.push(() => window.removeEventListener("blur", onWindowBlur)); + this.inputCleanup.push(() => document.removeEventListener("visibilitychange", onVisibilityChange)); + this.inputCleanup.push(() => { + if (this.pointerLockEscapeTimer !== null) { + window.clearTimeout(this.pointerLockEscapeTimer); + this.pointerLockEscapeTimer = null; + } + this.escapeTapDispatchedForCurrentHold = false; + this.clearEscapeAutoKeyUpTimer(); + this.clearEscapeHoldTimer(); + this.releasePressedKeys("input cleanup"); + this.pendingMouseDx = 0; + this.pendingMouseDy = 0; + this.pendingMouseTimestampUs = null; + this.mouseDeltaFilter.reset(); + this.videoElement = null; + // Unlock keyboard on cleanup + const nav = navigator as any; + if (nav.keyboard?.unlock) { + nav.keyboard.unlock(); + } + }); + } + + /** + * Query browser for supported video codecs via RTCRtpReceiver.getCapabilities. + * Returns normalized names like "H264", "H265", "AV1", "VP9", "VP8". + */ + private getSupportedVideoCodecs(): string[] { + try { + const capabilities = RTCRtpReceiver.getCapabilities("video"); + if (!capabilities) return []; + const codecs = new Set(); + for (const codec of capabilities.codecs) { + const mime = codec.mimeType.toUpperCase(); + if (mime.includes("H264")) codecs.add("H264"); + else if (mime.includes("H265") || mime.includes("HEVC")) codecs.add("H265"); + else if (mime.includes("AV1")) codecs.add("AV1"); + else if (mime.includes("VP9")) codecs.add("VP9"); + else if (mime.includes("VP8")) codecs.add("VP8"); + } + return Array.from(codecs); + } catch { + return []; + } + } + + /** Get supported HEVC profile-id values from RTCRtpReceiver capabilities (e.g. "1", "2"). */ + private getSupportedHevcProfiles(): Set { + const profiles = new Set(); + try { + const capabilities = RTCRtpReceiver.getCapabilities("video"); + if (!capabilities) return profiles; + for (const codec of capabilities.codecs) { + const mime = codec.mimeType.toUpperCase(); + if (!mime.includes("H265") && !mime.includes("HEVC")) { + continue; + } + const fmtp = codec.sdpFmtpLine ?? ""; + const match = fmtp.match(/(?:^|;)\s*profile-id=(\d+)/i); + if (match?.[1]) { + profiles.add(match[1]); + } + } + } catch { + // Ignore capability failures + } + return profiles; + } + + /** Maximum HEVC level-id by profile-id from receiver capabilities. */ + private getHevcMaxLevelsByProfile(): Partial> { + const result: Partial> = {}; + try { + const capabilities = RTCRtpReceiver.getCapabilities("video"); + if (!capabilities) return result; + for (const codec of capabilities.codecs) { + const mime = codec.mimeType.toUpperCase(); + if (!mime.includes("H265") && !mime.includes("HEVC")) { + continue; + } + + const fmtp = codec.sdpFmtpLine ?? ""; + const profileMatch = fmtp.match(/(?:^|;)\s*profile-id=(\d+)/i); + const levelMatch = fmtp.match(/(?:^|;)\s*level-id=(\d+)/i); + if (!profileMatch?.[1] || !levelMatch?.[1]) { + continue; + } + + const profile = Number.parseInt(profileMatch[1], 10) as 1 | 2; + const level = Number.parseInt(levelMatch[1], 10); + if (!Number.isFinite(level) || (profile !== 1 && profile !== 2)) { + continue; + } + + const current = result[profile]; + if (!current || level > current) { + result[profile] = level; + } + } + } catch { + // Ignore capability failures + } + return result; + } + + /** Whether receiver capabilities explicitly expose HEVC tier-flag=1 support. */ + private supportsHevcTierFlagOne(): boolean { + try { + const capabilities = RTCRtpReceiver.getCapabilities("video"); + if (!capabilities) return false; + return capabilities.codecs.some((codec) => { + const mime = codec.mimeType.toUpperCase(); + if (!mime.includes("H265") && !mime.includes("HEVC")) { + return false; + } + return /(?:^|;)\s*tier-flag=1/i.test(codec.sdpFmtpLine ?? ""); + }); + } catch { + return false; + } + } + + /** + * Apply setCodecPreferences roughly matching GFN web client behavior: + * preferred codec + RTX/FlexFEC only (receiver capabilities first). + * On failure, retry with sender capabilities appended. + */ + private applyCodecPreferences( + pc: RTCPeerConnection, + codec: VideoCodec, + preferredHevcProfileId?: 1 | 2, + ): void { + try { + const transceivers = pc.getTransceivers(); + const videoTransceiver = transceivers.find( + (t) => t.receiver.track.kind === "video", + ); + if (!videoTransceiver) { + this.log("setCodecPreferences: no video transceiver found, skipping"); + return; + } + + const receiverCaps = RTCRtpReceiver.getCapabilities("video")?.codecs; + if (!receiverCaps) { + this.log("setCodecPreferences: RTCRtpReceiver.getCapabilities returned null, skipping"); + return; + } + + const senderCaps = RTCRtpSender.getCapabilities?.("video")?.codecs ?? []; + + // Map our codec name to the MIME type used in WebRTC capabilities + const codecMimeMap: Record = { + H264: "video/H264", + H265: "video/H265", + AV1: "video/AV1", + VP9: "video/VP9", + VP8: "video/VP8", + }; + const preferredMime = codecMimeMap[codec]; + if (!preferredMime) { + this.log(`setCodecPreferences: unknown codec "${codec}", skipping`); + return; + } + + const preferred = receiverCaps.filter( + (c) => c.mimeType.toLowerCase() === preferredMime.toLowerCase(), + ); + + const auxiliary = receiverCaps.filter((c) => { + const mime = c.mimeType.toLowerCase(); + return mime.includes("rtx") || mime.includes("flexfec-03"); + }); + + if (preferred.length === 0) { + this.log(`setCodecPreferences: ${codec} (${preferredMime}) not in receiver capabilities, skipping`); + return; + } + + // H265 can be exposed with multiple profiles; prefer profile-id=1 first + // for maximum decoder compatibility (reduces macroblocking on some GPUs). + if (codec === "H265" && preferredHevcProfileId) { + preferred.sort((a, b) => { + const getScore = (c: RTCRtpCodec): number => { + const fmtp = (c.sdpFmtpLine ?? "").toLowerCase(); + const match = fmtp.match(/(?:^|;)\s*profile-id=(\d+)/); + const profile = match?.[1]; + if (profile === String(preferredHevcProfileId)) return 0; + if (!profile) return 1; + return 2; + }; + return getScore(a) - getScore(b); + }); + } + + let codecList = [...preferred, ...auxiliary]; + + try { + videoTransceiver.setCodecPreferences(codecList); + this.log( + `setCodecPreferences: set ${codec} (${preferred.length} preferred + ${auxiliary.length} auxiliary receiver codecs)`, + ); + } catch (e) { + this.log(`setCodecPreferences: receiver-only failed (${String(e)}), retrying with sender capabilities`); + try { + codecList = codecList.concat(senderCaps); + videoTransceiver.setCodecPreferences(codecList); + this.log( + `setCodecPreferences: retry succeeded with sender capabilities (+${senderCaps.length})`, + ); + } catch (retryErr) { + this.log(`setCodecPreferences: retry failed (${String(retryErr)}), falling back to SDP-only approach`); + } + } + } catch (e) { + this.log(`setCodecPreferences: failed (${String(e)}), falling back to SDP-only approach`); + } + } + + async handleOffer(offerSdp: string, session: SessionInfo, settings: OfferSettings): Promise { + this.cleanupPeerConnection(); + + this.log("=== handleOffer START ==="); + this.log(`Session: id=${session.sessionId}, status=${session.status}, serverIp=${session.serverIp}`); + this.log(`Signaling: server=${session.signalingServer}, url=${session.signalingUrl}`); + this.log(`MediaConnectionInfo: ${session.mediaConnectionInfo ? `ip=${session.mediaConnectionInfo.ip}, port=${session.mediaConnectionInfo.port}` : "NONE"}`); + this.log( + `Settings: codec=${settings.codec}, colorQuality=${settings.colorQuality}, resolution=${settings.resolution}, fps=${settings.fps}, maxBitrate=${settings.maxBitrateKbps}kbps`, + ); + this.log(`ICE servers: ${session.iceServers.length} (${session.iceServers.map(s => s.urls.join(",")).join(" | ")})`); + this.log(`Offer SDP length: ${offerSdp.length} chars`); + // Log full offer SDP for ICE debugging + this.log(`=== FULL OFFER SDP START ===`); + for (const line of offerSdp.split(/\r?\n/)) { + this.log(` SDP> ${line}`); + } + this.log(`=== FULL OFFER SDP END ===`); + + const negotiatedPartialReliable = parsePartialReliableThresholdMs(offerSdp); + this.partialReliableThresholdMs = negotiatedPartialReliable ?? GfnWebRtcClient.DEFAULT_PARTIAL_RELIABLE_THRESHOLD_MS; + this.log( + `Input channel policy: partial reliable threshold=${this.partialReliableThresholdMs}ms${negotiatedPartialReliable === null ? " (fallback)" : ""}`, + ); + + // Extract server region from session + this.serverRegion = session.signalingServer || session.streamingBaseUrl || ""; + // Clean up the region string (extract hostname or region name) + if (this.serverRegion) { + try { + const url = new URL(this.serverRegion); + this.serverRegion = url.hostname; + } catch { + // Keep as-is if not a valid URL + } + } + + const rtcConfig: RTCConfiguration = { + iceServers: toRtcIceServers(session.iceServers), + bundlePolicy: "max-bundle", + rtcpMuxPolicy: "require", + }; + + const pc = new RTCPeerConnection(rtcConfig); + this.pc = pc; + this.diagnostics.connectionState = pc.connectionState; + this.diagnostics.serverRegion = this.serverRegion; + this.diagnostics.gpuType = this.gpuType; + this.emitStats(); + + this.resetInputState(); + this.resetDiagnostics(); + this.createDataChannels(pc); + this.installInputCapture(this.options.videoElement); + this.setupStatsPolling(); + + pc.onicecandidate = (event) => { + if (!event.candidate) { + this.log("ICE gathering complete (null candidate)"); + return; + } + const payload = event.candidate.toJSON(); + if (!payload.candidate) { + return; + } + this.log(`Local ICE candidate: ${payload.candidate}`); + const candidate: IceCandidatePayload = { + candidate: payload.candidate, + sdpMid: payload.sdpMid, + sdpMLineIndex: payload.sdpMLineIndex, + usernameFragment: payload.usernameFragment, + }; + window.openNow.sendIceCandidate(candidate).catch((error) => { + this.log(`Failed to send local ICE candidate: ${String(error)}`); + }); + }; + + pc.onconnectionstatechange = () => { + this.diagnostics.connectionState = pc.connectionState; + this.emitStats(); + this.log(`Peer connection state: ${pc.connectionState}`); + }; + + pc.ondatachannel = (event) => { + const channel = event.channel; + this.log(`Remote data channel received: label=${channel.label}, ordered=${channel.ordered}`); + if (channel.label !== "control_channel") { + return; + } + + this.controlChannel = channel; + this.controlChannel.binaryType = "arraybuffer"; + this.controlChannel.onmessage = (msgEvent) => { + void this.onControlChannelMessage(msgEvent.data as string | Blob | ArrayBuffer); + }; + this.controlChannel.onclose = () => { + this.log("Control channel closed"); + if (this.controlChannel === channel) { + this.controlChannel = null; + } + }; + this.controlChannel.onerror = () => { + this.log("Control channel error"); + }; + }; + + pc.onicecandidateerror = (event: Event) => { + const e = event as RTCPeerConnectionIceErrorEvent; + const hostCandidate = "hostCandidate" in e + ? (e as RTCPeerConnectionIceErrorEvent & { hostCandidate?: string }).hostCandidate + : undefined; + this.log(`ICE candidate error: ${e.errorCode} ${e.errorText} (${e.url ?? "no url"}) hostCandidate=${hostCandidate ?? "?"}`); + }; + + pc.oniceconnectionstatechange = () => { + this.log(`ICE connection state: ${pc.iceConnectionState}`); + }; + + pc.onicegatheringstatechange = () => { + this.log(`ICE gathering state: ${pc.iceGatheringState}`); + }; + + pc.onsignalingstatechange = () => { + this.log(`Signaling state: ${pc.signalingState}`); + }; + + pc.ontrack = (event) => { + this.log(`Track received: kind=${event.track.kind}, id=${event.track.id}, readyState=${event.track.readyState}`); + this.attachTrack(event.track); + + // Configure low-latency jitter buffer for video and audio receivers + this.configureReceiverForLowLatency(event.receiver, event.track.kind); + }; + + // --- SDP Processing (matching Rust reference) --- + + // 1. Fix 0.0.0.0 in server's SDP offer with real server IP + // The GFN server sends c=IN IP4 0.0.0.0; replace with actual IP + const serverIpForSdp = session.mediaConnectionInfo?.ip || session.serverIp || ""; + let processedOffer = offerSdp; + if (serverIpForSdp) { + processedOffer = fixServerIp(processedOffer, serverIpForSdp); + this.log(`Fixed server IP in SDP offer: ${serverIpForSdp}`); + // Log any remaining 0.0.0.0 references after fix + const remaining = (processedOffer.match(/0\.0\.0\.0/g) ?? []).length; + if (remaining > 0) { + this.log(`Warning: ${remaining} occurrences of 0.0.0.0 still remain in SDP after fix`); + } + } + + // 2. Extract server's ice-ufrag BEFORE any modifications (needed for manual candidate injection) + const serverIceUfrag = extractIceUfragFromOffer(processedOffer); + this.log(`Server ICE ufrag: "${serverIceUfrag}"`); + + const preferredHevcProfileId = hevcPreferredProfileId(settings.colorQuality); + + // 3. Filter to preferred codec — but only if the browser actually supports it + let effectiveCodec = settings.codec; + const supported = this.getSupportedVideoCodecs(); + this.log(`Browser supported video codecs: ${supported.join(", ") || "unknown"}`); + + if (settings.codec === "H265") { + const hevcProfiles = this.getSupportedHevcProfiles(); + if (hevcProfiles.size > 0) { + this.log(`Browser HEVC profile-id support: ${Array.from(hevcProfiles).join(", ")}`); + } + + const hevcMaxLevels = this.getHevcMaxLevelsByProfile(); + if (hevcMaxLevels[1] || hevcMaxLevels[2]) { + this.log( + `Browser HEVC max level-id by profile: p1=${hevcMaxLevels[1] ?? "?"}, p2=${hevcMaxLevels[2] ?? "?"}`, + ); + const rewrittenLevel = rewriteH265LevelIdByProfile(processedOffer, hevcMaxLevels); + if (rewrittenLevel.replacements > 0) { + this.log( + `HEVC level compatibility: rewrote ${rewrittenLevel.replacements} fmtp lines to receiver max level-id`, + ); + processedOffer = rewrittenLevel.sdp; + } + } + + const tierFlagOneSupported = this.supportsHevcTierFlagOne(); + this.log(`Browser HEVC tier-flag=1 support: ${tierFlagOneSupported ? "yes" : "no"}`); + if (!tierFlagOneSupported) { + const rewritten = rewriteH265TierFlag(processedOffer, 0); + if (rewritten.replacements > 0) { + this.log( + `HEVC tier compatibility: rewrote ${rewritten.replacements} fmtp lines tier-flag=1 -> tier-flag=0`, + ); + processedOffer = rewritten.sdp; + } + } + if (hevcProfiles.size > 0 && !hevcProfiles.has(String(preferredHevcProfileId))) { + this.log( + `Warning: requested H265 profile-id=${preferredHevcProfileId} not reported in browser capabilities; forcing H265 anyway per user preference`, + ); + } + } + + if (supported.length > 0 && !supported.includes(settings.codec)) { + this.log(`Warning: ${settings.codec} not reported in browser codec list; forcing requested codec anyway`); + } + this.log(`Effective codec: ${effectiveCodec} (preferred HEVC profile-id=${preferredHevcProfileId})`); + const filteredOffer = preferCodec(processedOffer, effectiveCodec, { + preferHevcProfileId: preferredHevcProfileId, + }); + this.log(`Filtered offer SDP length: ${filteredOffer.length} chars`); + this.log("Setting remote description (offer)..."); + await pc.setRemoteDescription({ type: "offer", sdp: filteredOffer }); + this.log("Remote description set successfully"); + await this.flushQueuedCandidates(); + + // 3b. Apply setCodecPreferences on the video transceiver to reinforce codec choice. + // This is the modern WebRTC API — more reliable than SDP munging alone. + // Must be called after setRemoteDescription (which creates the transceiver) + // but before createAnswer (which generates the answer SDP). + this.applyCodecPreferences(pc, effectiveCodec, preferredHevcProfileId); + + // 4. Create answer, munge SDP, and set local description + this.log("Creating answer..."); + const answer = await pc.createAnswer(); + this.log(`Answer created, SDP length: ${answer.sdp?.length ?? 0} chars`); + + // Munge answer SDP: inject b=AS: bitrate limits and stereo=1 for opus + if (answer.sdp) { + answer.sdp = mungeAnswerSdp(answer.sdp, settings.maxBitrateKbps); + this.log(`Answer SDP munged (b=AS:${settings.maxBitrateKbps}, stereo=1)`); + } + + await pc.setLocalDescription(answer); + this.log("Local description set, waiting for ICE gathering..."); + + const finalSdp = await this.waitForIceGathering(pc, 5000); + this.log(`ICE gathering done, final SDP length: ${finalSdp.length} chars`); + + // Debug negotiated video codec/fmtp lines from local answer SDP + { + const lines = finalSdp.split(/\r?\n/); + let inVideo = false; + const negotiatedVideoLines: string[] = []; + let hasNegotiatedH265 = false; + for (const line of lines) { + if (line.startsWith("m=video")) { + inVideo = true; + negotiatedVideoLines.push(line); + continue; + } + if (line.startsWith("m=") && inVideo) { + break; + } + if (inVideo && (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:") || line.startsWith("a=rtcp-fb:"))) { + negotiatedVideoLines.push(line); + if (line.startsWith("a=rtpmap:") && /\sH(?:265|EVC)\//i.test(line)) { + hasNegotiatedH265 = true; + } + } + } + if (negotiatedVideoLines.length > 0) { + this.log("Negotiated local video SDP lines:"); + for (const l of negotiatedVideoLines) { + this.log(` SDP< ${l}`); + } + } + + if (effectiveCodec === "H265" && !hasNegotiatedH265) { + throw new Error("H265 requested but not negotiated in local SDP (no H265 rtpmap in answer)"); + } + } + + const credentials = extractIceCredentials(finalSdp); + this.log(`Extracted ICE credentials: ufrag=${credentials.ufrag}, pwd=${credentials.pwd.slice(0, 8)}...`); + const { width, height } = parseResolution(settings.resolution); + + const nvstSdp = buildNvstSdp({ + width, + height, + fps: settings.fps, + maxBitrateKbps: settings.maxBitrateKbps, + partialReliableThresholdMs: this.partialReliableThresholdMs, + codec: effectiveCodec, + colorQuality: settings.colorQuality, + credentials, + }); + + await window.openNow.sendAnswer({ + sdp: finalSdp, + nvstSdp, + }); + this.log("Sent SDP answer and nvstSdp"); + + // 5. Inject manual ICE candidate from mediaConnectionInfo AFTER answer is sent + // (matches Rust reference ordering — full SDP exchange completes first) + // GFN servers use ice-lite and may not trickle candidates via signaling. + // The actual media endpoint comes from the session's connectionInfo array. + if (session.mediaConnectionInfo) { + const mci = session.mediaConnectionInfo; + const rawIp = extractPublicIp(mci.ip); + if (rawIp && mci.port > 0) { + const candidateStr = `candidate:1 1 udp 2130706431 ${rawIp} ${mci.port} typ host`; + this.log(`Injecting manual ICE candidate: ${rawIp}:${mci.port}`); + + // Try sdpMid "0" first, then "1", "2", "3" (matching Rust fallback) + const mids = ["0", "1", "2", "3"]; + let injected = false; + for (const mid of mids) { + try { + await pc.addIceCandidate({ + candidate: candidateStr, + sdpMid: mid, + sdpMLineIndex: parseInt(mid, 10), + usernameFragment: serverIceUfrag || undefined, + }); + this.log(`Manual ICE candidate injected (sdpMid=${mid})`); + injected = true; + break; + } catch (error) { + this.log(`Manual ICE candidate failed for sdpMid=${mid}: ${String(error)}`); + } + } + if (!injected) { + this.log("Warning: Could not inject manual ICE candidate on any sdpMid"); + } + } else { + this.log(`Warning: mediaConnectionInfo present but no valid IP (ip=${mci.ip}, port=${mci.port})`); + } + } else { + this.log("No mediaConnectionInfo available — relying on trickle ICE only"); + } + + this.log("=== handleOffer COMPLETE — waiting for ICE connectivity and tracks ==="); + } + + async addRemoteCandidate(candidate: IceCandidatePayload): Promise { + this.log(`Remote ICE candidate received: ${candidate.candidate} (sdpMid=${candidate.sdpMid})`); + const init: RTCIceCandidateInit = { + candidate: candidate.candidate, + sdpMid: candidate.sdpMid ?? undefined, + sdpMLineIndex: candidate.sdpMLineIndex ?? undefined, + usernameFragment: candidate.usernameFragment ?? undefined, + }; + + if (!this.pc || !this.pc.remoteDescription) { + this.queuedCandidates.push(init); + return; + } + + await this.pc.addIceCandidate(init); + } + + dispose(): void { + this.cleanupPeerConnection(); + + for (const track of this.videoStream.getTracks()) { + this.videoStream.removeTrack(track); + } + for (const track of this.audioStream.getTracks()) { + this.audioStream.removeTrack(track); + } + } +} diff --git a/opennow-stable/src/renderer/src/main.tsx b/opennow-stable/src/renderer/src/main.tsx new file mode 100644 index 0000000..1c7a0a2 --- /dev/null +++ b/opennow-stable/src/renderer/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import { App } from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/opennow-stable/src/renderer/src/shortcuts.ts b/opennow-stable/src/renderer/src/shortcuts.ts new file mode 100644 index 0000000..e7fe18f --- /dev/null +++ b/opennow-stable/src/renderer/src/shortcuts.ts @@ -0,0 +1,212 @@ +export interface ParsedShortcut { + key: string; + ctrl: boolean; + alt: boolean; + shift: boolean; + meta: boolean; + valid: boolean; + canonical: string; +} + +function normalizeKeyToken(token: string): string | null { + const upper = token.toUpperCase(); + const alias: Record = { + ESC: "ESCAPE", + RETURN: "ENTER", + DEL: "DELETE", + INS: "INSERT", + PGUP: "PAGEUP", + PGDN: "PAGEDOWN", + SPACEBAR: "SPACE", + " ": "SPACE", + }; + + if (alias[upper]) { + return alias[upper]; + } + if (upper.length === 1) { + return upper; + } + if (/^F\d{1,2}$/.test(upper)) { + return upper; + } + if (upper.startsWith("ARROW")) { + return upper; + } + if (/^[A-Z0-9_]+$/.test(upper)) { + return upper; + } + return null; +} + +function normalizeEventKey(key: string): string { + const upper = key.toUpperCase(); + const alias: Record = { + ESC: "ESCAPE", + " ": "SPACE", + }; + if (alias[upper]) { + return alias[upper]; + } + return upper; +} + +function normalizeEventCode(code: string): string | null { + if (!code) return null; + const upper = code.toUpperCase(); + + if (upper.startsWith("KEY") && upper.length === 4) { + return upper.slice(3); + } + if (upper.startsWith("DIGIT") && upper.length === 6) { + return upper.slice(5); + } + if (upper.startsWith("NUMPAD")) { + return upper; + } + if (/^F\d{1,2}$/.test(upper)) { + return upper; + } + if (upper.startsWith("ARROW")) { + return upper; + } + if (upper === "SPACE") { + return "SPACE"; + } + if (upper === "ENTER" || upper === "NUMPADENTER") { + return "ENTER"; + } + if (/^[A-Z0-9_]+$/.test(upper)) { + return upper; + } + return null; +} + +function isKeyMatch(event: KeyboardEvent, shortcutKey: string): boolean { + const byKey = normalizeEventKey(event.key) === shortcutKey; + if (byKey) return true; + + const code = normalizeEventCode(event.code); + if (!code) return false; + + if (code === shortcutKey) return true; + if (shortcutKey.length === 1 && (code === `KEY${shortcutKey}` || code === `DIGIT${shortcutKey}`)) { + return true; + } + if (shortcutKey === "ENTER" && code === "NUMPADENTER") { + return true; + } + + return false; +} + +export function isShortcutMatch(event: KeyboardEvent, shortcut: ParsedShortcut): boolean { + if (!shortcut.valid) return false; + if (event.ctrlKey !== shortcut.ctrl) return false; + if (event.altKey !== shortcut.alt) return false; + if (event.shiftKey !== shortcut.shift) return false; + if (event.metaKey !== shortcut.meta) return false; + return isKeyMatch(event, shortcut.key); +} + +export function normalizeShortcut(raw: string): ParsedShortcut { + const tokens = raw + .split("+") + .map((part) => part.trim()) + .filter(Boolean); + + let ctrl = false; + let alt = false; + let shift = false; + let meta = false; + let keyToken: string | null = null; + + for (const token of tokens) { + const upper = token.toUpperCase(); + if (upper === "CTRL" || upper === "CONTROL") { + ctrl = true; + continue; + } + if (upper === "ALT" || upper === "OPTION") { + alt = true; + continue; + } + if (upper === "SHIFT") { + shift = true; + continue; + } + if (upper === "META" || upper === "CMD" || upper === "COMMAND") { + meta = true; + continue; + } + if (keyToken) { + return { + key: "", + ctrl, + alt, + shift, + meta, + valid: false, + canonical: raw.trim(), + }; + } + keyToken = token; + } + + if (!keyToken) { + return { + key: "", + ctrl, + alt, + shift, + meta, + valid: false, + canonical: raw.trim(), + }; + } + + const normalizedKey = normalizeKeyToken(keyToken); + if (!normalizedKey) { + return { + key: "", + ctrl, + alt, + shift, + meta, + valid: false, + canonical: raw.trim(), + }; + } + + const parts: string[] = []; + if (ctrl) parts.push("Ctrl"); + if (alt) parts.push("Alt"); + if (shift) parts.push("Shift"); + if (meta) parts.push("Meta"); + parts.push(normalizedKey); + + return { + key: normalizedKey, + ctrl, + alt, + shift, + meta, + valid: true, + canonical: parts.join("+"), + }; +} + +export function formatShortcutForDisplay(raw: string, isMac: boolean): string { + const parsed = normalizeShortcut(raw); + if (!parsed.valid) { + return raw; + } + + const parts: string[] = []; + if (parsed.ctrl) parts.push("Ctrl"); + if (parsed.alt) parts.push(isMac ? "Option" : "Alt"); + if (parsed.shift) parts.push("Shift"); + if (parsed.meta) parts.push(isMac ? "Cmd" : "Meta"); + parts.push(parsed.key); + return parts.join("+"); +} diff --git a/opennow-stable/src/renderer/src/styles.css b/opennow-stable/src/renderer/src/styles.css new file mode 100644 index 0000000..d1c8dbe --- /dev/null +++ b/opennow-stable/src/renderer/src/styles.css @@ -0,0 +1,2363 @@ +/* ============================================================ + OpenNOW — Unified Design System + Aesthetic: Premium cinematic dark gaming platform + Obsidian-charcoal base, warm neutral grays, green accents + ============================================================ */ + +/* ---------- Design Tokens ---------- */ +:root { + color-scheme: dark; + + /* Backgrounds — layered obsidian-charcoal */ + --bg-a: #0a0a0c; + --bg-b: #111114; + --bg-c: #19191e; + + /* Panels & cards */ + --panel: #131316; + --panel-border: rgba(255, 255, 255, 0.06); + --panel-border-solid: #2a2a30; + --card: #151518; + --card-hover: #1c1c20; + --card-selected: #1a2a22; + --chip: #1e1e23; + + /* Ink — text hierarchy */ + --ink: #ececef; + --ink-soft: #9a9aa3; + --ink-muted: #5c5c66; + + /* Accent — green */ + --accent: #58d98a; + --accent-hover: #6ae89a; + --accent-press: #44c477; + --accent-glow: rgba(88, 217, 138, 0.25); + --accent-surface: rgba(88, 217, 138, 0.08); + --accent-surface-strong: rgba(88, 217, 138, 0.14); + --accent-on: #062315; + + /* Semantic */ + --error: #f87171; + --error-bg: rgba(248, 113, 113, 0.08); + --error-border: rgba(248, 113, 113, 0.2); + --warning: #fbbf24; + --info: #60a5fa; + --success: #4ade80; + + /* Radii */ + --r-sm: 8px; + --r-md: 12px; + --r-lg: 16px; + --r-xl: 20px; + --r-2xl: 24px; + --r-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.55); + --shadow-glow: 0 0 20px var(--accent-glow); + + /* Transitions */ + --ease: cubic-bezier(0.4, 0, 0.2, 1); + --t-fast: 150ms var(--ease); + --t-normal: 200ms var(--ease); + --t-slow: 300ms var(--ease); + + /* Layout */ + --navbar-h: 48px; +} + +/* ---------- Reset & Base ---------- */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body, +#root { + width: 100%; + height: 100%; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--ink); + background: var(--bg-a); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ---------- Scrollbar ---------- */ +::-webkit-scrollbar { width: 5px; height: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--panel-border-solid); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); } + +/* ---------- Animations ---------- */ +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes float-a { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(-40px, 30px) scale(1.15); } +} +@keyframes float-b { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(30px, -25px) scale(1.08); } +} +@keyframes float-c { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(-20px, -30px) scale(1.12); } +} +@keyframes fade-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes fade-in-down { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes pulse-glow { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.08); opacity: 0.85; } +} + + +/* ====================================================== + APP SHELL + ====================================================== */ +.app-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background: + radial-gradient(900px 400px at 80% -5%, rgba(88, 217, 138, 0.03), transparent 70%), + linear-gradient(180deg, var(--bg-a), var(--bg-b)); +} + +.auth-refresh-notice { + position: fixed; + top: calc(var(--navbar-h) + 10px); + left: 50%; + transform: translateX(-50%); + z-index: 1200; + max-width: min(520px, calc(100vw - 24px)); + padding: 7px 12px; + border-radius: var(--r-full); + border: 1px solid var(--panel-border); + background: rgba(10, 10, 12, 0.95); + font-size: 0.74rem; + font-weight: 600; + letter-spacing: 0.01em; + box-shadow: var(--shadow-sm); + animation: fade-in 220ms var(--ease); +} + +.auth-refresh-notice--success { + color: var(--accent); + border-color: rgba(88, 217, 138, 0.35); + background: linear-gradient(180deg, rgba(88, 217, 138, 0.15), rgba(10, 10, 12, 0.95)); +} + +.auth-refresh-notice--warn { + color: #facc15; + border-color: rgba(250, 204, 21, 0.35); + background: linear-gradient(180deg, rgba(250, 204, 21, 0.15), rgba(10, 10, 12, 0.95)); +} + + +/* ====================================================== + NAVBAR + ====================================================== */ +.navbar { + position: fixed; + top: 0; left: 0; right: 0; + height: var(--navbar-h); + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0 16px; + background: rgba(10, 10, 12, 0.97); + border-bottom: 1px solid var(--panel-border); + z-index: 1000; + will-change: transform; +} + +.navbar-left { + display: flex; + align-items: center; + gap: 8px; +} + +.navbar-brand { + width: 28px; height: 28px; + border-radius: 7px; + background: linear-gradient(135deg, var(--accent), var(--accent-press)); + display: flex; align-items: center; justify-content: center; + color: var(--accent-on); + box-shadow: 0 2px 12px var(--accent-glow); + flex-shrink: 0; +} + +.navbar-logo-text { + font-size: 0.95rem; + font-weight: 700; + color: var(--ink); + letter-spacing: 0.01em; +} + +.navbar-nav { + display: flex; + align-items: center; + gap: 2px; + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +.navbar-subscription { + display: flex; + align-items: center; + gap: 6px; +} + +.navbar-subscription-chip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 9px; + border-radius: var(--r-full); + border: 1px solid var(--panel-border); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); + color: var(--ink-soft); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.01em; + white-space: nowrap; + font-family: inherit; + cursor: pointer; + transition: border-color var(--t-fast), background var(--t-fast), color var(--t-fast); +} + +.navbar-subscription-chip:hover { + border-color: color-mix(in srgb, var(--accent) 35%, var(--panel-border)); + background: linear-gradient(180deg, rgba(88, 217, 138, 0.12), rgba(88, 217, 138, 0.04)); + color: var(--ink); +} + +.navbar-subscription-chip:focus-visible { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-surface); +} + +.navbar-subscription-chip svg { + color: var(--accent); + flex-shrink: 0; +} + +.navbar-subscription-chip--good { + border-color: color-mix(in srgb, var(--accent) 45%, var(--panel-border)); + background: linear-gradient(180deg, rgba(88, 217, 138, 0.14), rgba(88, 217, 138, 0.05)); +} + +.navbar-subscription-chip--warn { + border-color: rgba(251, 191, 36, 0.35); + background: linear-gradient(180deg, rgba(251, 191, 36, 0.18), rgba(251, 191, 36, 0.06)); +} + +.navbar-subscription-chip--warn svg { + color: var(--warning); +} + +.navbar-subscription-chip--critical { + border-color: rgba(248, 113, 113, 0.45); + background: linear-gradient(180deg, rgba(248, 113, 113, 0.2), rgba(248, 113, 113, 0.08)); +} + +.navbar-subscription-chip--critical svg { + color: var(--error); +} + +.navbar-link { + display: flex; align-items: center; gap: 6px; + padding: 6px 12px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--ink-muted); + font-size: 0.8rem; font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: color var(--t-fast), background var(--t-fast); + outline: none; +} +.navbar-link:hover { color: var(--ink-soft); background: rgba(255, 255, 255, 0.04); } +.navbar-link.active { color: var(--ink); background: rgba(255, 255, 255, 0.07); font-weight: 600; } + +.navbar-right { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + position: relative; + z-index: 2; +} + +.navbar-session-resume { + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 7px; + width: 210px; + min-width: 210px; + padding: 6px 10px; + border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--panel-border)); + border-radius: var(--r-full); + background: + linear-gradient(160deg, rgba(88, 217, 138, 0.26), rgba(88, 217, 138, 0.08)), + linear-gradient(180deg, rgba(15, 24, 19, 0.95), rgba(10, 12, 11, 0.92)); + box-shadow: + 0 6px 20px rgba(88, 217, 138, 0.16), + 0 0 0 1px rgba(88, 217, 138, 0.14) inset; + color: #d7ffe8; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.01em; + font-family: inherit; + cursor: pointer; + white-space: nowrap; + transition: + transform var(--t-fast), + border-color var(--t-fast), + box-shadow var(--t-fast), + filter var(--t-fast); + overflow: hidden; +} + +.navbar-session-resume:hover:not(:disabled) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--accent) 75%, var(--panel-border)); + box-shadow: + 0 10px 28px rgba(88, 217, 138, 0.24), + 0 0 0 1px rgba(88, 217, 138, 0.24) inset; +} + +.navbar-session-resume:focus-visible { + outline: none; + box-shadow: + 0 0 0 2px rgba(88, 217, 138, 0.24), + 0 10px 28px rgba(88, 217, 138, 0.24); +} + +.navbar-session-resume:disabled { + opacity: 0.55; + cursor: not-allowed; + filter: grayscale(0.1); +} + +.navbar-session-resume-text { + color: #e9fff2; + min-width: 46px; + text-align: left; + flex: 0 0 auto; +} + +.navbar-session-resume-game { + min-width: 0; + flex: 1 1 auto; + color: #d9fbe8; + font-size: 0.67rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.navbar-session-resume-spin { + animation: spin 1s linear infinite; +} + +.navbar-session-resume.is-loading { + animation: none; +} + +.navbar-user { + display: flex; align-items: center; gap: 7px; + padding: 4px 8px; + border-radius: 6px; +} +.navbar-avatar { + width: 24px; height: 24px; border-radius: 5px; + object-fit: cover; +} +.navbar-avatar-fallback { + width: 24px; height: 24px; border-radius: 5px; + background: var(--chip); + display: flex; align-items: center; justify-content: center; + color: var(--ink-muted); +} +.navbar-user-info { + display: flex; flex-direction: column; gap: 0; line-height: 1.2; +} +.navbar-username { + font-size: 0.78rem; font-weight: 500; color: var(--ink-soft); + max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.navbar-tier { + font-size: 0.6rem; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.06em; line-height: 1; +} +.navbar-tier.tier-ultimate { color: #ffd700; } +.navbar-tier.tier-priority { color: #cdaf95; } +.navbar-tier.tier-free { color: var(--ink-muted); } +.navbar-logout { + display: flex; align-items: center; justify-content: center; + width: 32px; height: 32px; + border-radius: 6px; border: none; background: transparent; + color: var(--ink-muted); cursor: pointer; + transition: color var(--t-fast), background var(--t-fast); outline: none; + font-family: inherit; +} +.navbar-logout:hover { background: var(--error-bg); color: var(--error); } + +.navbar-guest { + display: flex; align-items: center; gap: 5px; + padding: 5px 10px; + background: var(--chip); + border-radius: 6px; + color: var(--ink-muted); font-size: 0.78rem; +} + +.navbar-modal-backdrop { + position: fixed; + inset: 0; + z-index: 3200; + display: flex; + align-items: center; + justify-content: center; + padding: 18px; + background: rgba(8, 9, 11, 0.6); + backdrop-filter: blur(6px); +} + +.navbar-modal { + width: min(760px, calc(100vw - 40px)); + max-height: calc(100vh - 70px); + border-radius: 12px; + border: 1px solid var(--panel-border); + background: var(--panel); + box-shadow: var(--shadow-md); + overflow: hidden; + animation: fade-in 130ms var(--ease); + display: flex; + flex-direction: column; +} + +.navbar-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 14px 16px; + border-bottom: 1px solid var(--panel-border); +} + +.navbar-modal-header h3 { + margin: 0; + font-size: 0.95rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.navbar-modal-close { + width: 26px; + height: 26px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--ink-muted); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background var(--t-fast), color var(--t-fast); +} + +.navbar-modal-close:hover { + background: var(--chip); + color: var(--ink-soft); +} + +.navbar-modal-body { + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 12px; + overflow: auto; +} + +.navbar-modal-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + border-bottom: 1px solid var(--panel-border); + padding-bottom: 6px; +} + +.navbar-modal-row:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.navbar-modal-row span { + color: var(--ink-muted); + font-size: 0.9rem; +} + +.navbar-modal-row strong { + color: var(--ink); + font-size: 0.95rem; + font-weight: 600; + text-align: right; +} + +.navbar-meter { + border: 1px solid var(--panel-border); + border-radius: 9px; + background: var(--card); + padding: 12px; +} + +.navbar-meter-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + gap: 10px; +} + +.navbar-meter-head span { + color: var(--ink-muted); + font-size: 0.88rem; + font-weight: 600; +} + +.navbar-meter-head strong { + color: var(--ink-soft); + font-size: 0.86rem; + letter-spacing: 0.02em; +} + +.navbar-meter-track { + height: 9px; + border-radius: 999px; + background: var(--bg-c); + overflow: hidden; +} + +.navbar-meter-fill { + display: block; + height: 100%; + border-radius: inherit; + transition: width var(--t-slow); +} + +.navbar-meter-fill--good { + background: linear-gradient(90deg, #44c477, #58d98a); +} + +.navbar-meter-fill--warn { + background: linear-gradient(90deg, #e6b64c, #f0c664); +} + +.navbar-meter-fill--critical { + background: linear-gradient(90deg, #d95f5f, #e77f7f); +} + +.navbar-meter-legend { + display: flex; + justify-content: space-between; + gap: 12px; + margin-top: 7px; + color: var(--ink-muted); + font-size: 0.84rem; +} + +@media (max-width: 1220px) { + .navbar-subscription { + display: none; + } +} + +@media (max-width: 980px) { + .navbar-nav { + position: static; + transform: none; + margin-left: 20px; + } + + .navbar-session-resume { + width: 170px; + min-width: 170px; + } +} + + +/* ====================================================== + MAIN CONTENT — offset for fixed navbar + ====================================================== */ +.main-content { + flex: 1; + overflow: auto; + padding: 20px; + margin-top: var(--navbar-h); + will-change: scroll-position; +} + + +/* ====================================================== + LOGIN SCREEN + ====================================================== */ +.login-screen { + position: fixed; inset: 0; + display: flex; + align-items: center; justify-content: center; + overflow: hidden; + background: var(--bg-a); +} + +/* Animated background */ +.login-bg { + position: absolute; inset: 0; + overflow: hidden; pointer-events: none; +} +.login-bg-orb { + position: absolute; + border-radius: 50%; + filter: blur(40px); + opacity: 0.35; +} +.login-bg-orb--1 { + width: 500px; height: 500px; + top: -180px; right: -80px; + background: radial-gradient(circle, var(--accent) 0%, transparent 70%); + animation: float-a 22s ease-in-out infinite; +} +.login-bg-orb--2 { + width: 400px; height: 400px; + bottom: -120px; left: -80px; + background: radial-gradient(circle, #4a90d9 0%, transparent 70%); + animation: float-b 28s ease-in-out infinite; +} +.login-bg-orb--3 { + width: 300px; height: 300px; + top: 40%; left: 50%; + background: radial-gradient(circle, rgba(88, 217, 138, 0.5) 0%, transparent 70%); + animation: float-c 18s ease-in-out infinite; +} +.login-bg-noise { + position: absolute; inset: 0; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 256px 256px; + opacity: 0.4; +} + +/* Login content */ +.login-content { + position: relative; z-index: 1; + display: flex; flex-direction: column; + align-items: center; gap: 28px; + animation: fade-in 500ms var(--ease); +} + +.login-brand { + display: flex; align-items: center; gap: 10px; +} +.login-brand-mark { + width: 36px; height: 36px; + border-radius: 9px; + background: linear-gradient(135deg, var(--accent), var(--accent-press)); + display: flex; align-items: center; justify-content: center; + color: var(--accent-on); + box-shadow: 0 4px 20px var(--accent-glow); +} +.login-brand-name { + font-size: 1.4rem; + font-weight: 700; + color: var(--ink); + letter-spacing: 0.01em; +} + +/* Login card */ +.login-card { + width: 100%; max-width: 380px; + padding: 32px; + background: rgba(21, 21, 24, 0.92); + border: 1px solid var(--panel-border); + border-radius: var(--r-xl); + box-shadow: var(--shadow-lg); +} + +.login-card-header { + text-align: center; margin-bottom: 28px; +} +.login-card-header h1 { + font-size: 1.35rem; font-weight: 700; + color: var(--ink); margin: 0 0 6px 0; +} +.login-card-header p { + font-size: 0.85rem; color: var(--ink-muted); margin: 0; +} + +/* Error */ +.login-error { + display: flex; align-items: center; gap: 8px; + padding: 10px 14px; margin-bottom: 20px; + background: var(--error-bg); + border: 1px solid var(--error-border); + border-radius: var(--r-sm); + color: var(--error); font-size: 0.8rem; font-weight: 500; +} +.login-error-dot { + width: 6px; height: 6px; + background: var(--error); + border-radius: 50%; + flex-shrink: 0; +} + +.login-status { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + margin-bottom: 20px; + background: rgba(96, 165, 250, 0.08); + border: 1px solid rgba(96, 165, 250, 0.2); + border-radius: var(--r-sm); + color: #93c5fd; + font-size: 0.8rem; + font-weight: 500; +} + +.login-status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + background: var(--info); + animation: pulse-glow 1.7s ease-in-out infinite; +} + +/* Field */ +.login-field { + position: relative; margin-bottom: 20px; +} +.login-label { + display: block; font-size: 0.75rem; font-weight: 600; + color: var(--ink-muted); text-transform: uppercase; + letter-spacing: 0.06em; margin-bottom: 6px; +} + +/* Custom select */ +.login-select { + width: 100%; + display: flex; align-items: center; justify-content: space-between; + padding: 11px 14px; + background: var(--bg-a); + border: 1px solid var(--panel-border-solid); + border-radius: var(--r-sm); + color: var(--ink); font-size: 0.88rem; font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: border-color var(--t-normal), box-shadow var(--t-normal); + outline: none; +} +.login-select:hover:not(:disabled) { border-color: #444; } +.login-select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-surface); } +.login-select.open { border-color: var(--accent); border-radius: var(--r-sm) var(--r-sm) 0 0; } +.login-select:disabled { opacity: 0.5; cursor: not-allowed; } +.login-select-text { + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.login-select-chevron { + color: var(--ink-muted); flex-shrink: 0; + transition: transform var(--t-normal); +} +.login-select-chevron.rotated { transform: rotate(180deg); color: var(--accent); } + +/* Dropdown */ +.login-dropdown { + position: absolute; top: 100%; left: 0; right: 0; + max-height: 200px; overflow-y: auto; + background: var(--bg-a); + border: 1px solid var(--accent); border-top: none; + border-radius: 0 0 var(--r-sm) var(--r-sm); + z-index: 10; + box-shadow: 0 10px 36px rgba(0, 0, 0, 0.5); + animation: fade-in-down 120ms var(--ease); +} +.login-dropdown-item { + width: 100%; + display: flex; align-items: center; justify-content: space-between; + padding: 10px 14px; + background: transparent; border: none; + color: var(--ink); font-size: 0.85rem; font-weight: 500; + font-family: inherit; + cursor: pointer; transition: background var(--t-fast); + text-align: left; +} +.login-dropdown-item:hover { background: var(--accent-surface); } +.login-dropdown-item.selected { background: var(--accent-surface-strong); color: var(--accent); } + +/* Login button */ +.login-button { + width: 100%; + display: flex; align-items: center; justify-content: center; gap: 8px; + padding: 13px 20px; + background: linear-gradient(135deg, var(--accent), var(--accent-press)); + border: none; border-radius: var(--r-sm); + color: var(--accent-on); font-size: 0.92rem; font-weight: 700; + font-family: inherit; + cursor: pointer; + transition: transform var(--t-normal), box-shadow var(--t-normal), opacity var(--t-normal); + box-shadow: 0 4px 16px var(--accent-glow); +} +.login-button:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 6px 24px rgba(88, 217, 138, 0.35); +} +.login-button:active:not(:disabled) { transform: translateY(0); } +.login-button:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-glow), 0 4px 16px var(--accent-glow); } +.login-button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } +.login-button.loading { cursor: wait; } + +.login-spinner { + width: 16px; height: 16px; + border: 2px solid rgba(6, 35, 21, 0.3); + border-top-color: var(--accent-on); + border-radius: 50%; + animation: spin 0.8s linear infinite; + display: inline-block; +} + +.login-footer { + font-size: 0.75rem; color: var(--ink-muted); margin: 0; +} + + +/* ====================================================== + HOME PAGE + ====================================================== */ +.home-page { + display: flex; + flex-direction: column; + height: 100%; + max-width: 1600px; + margin: 0 auto; + gap: 16px; + overflow: hidden; +} + +/* Toolbar: tabs | search | count */ +.home-toolbar { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.home-tabs { + display: flex; + gap: 2px; + background: var(--card); + border: 1px solid var(--panel-border); + border-radius: var(--r-sm); + padding: 3px; +} + +.home-tab { + display: flex; align-items: center; gap: 5px; + padding: 6px 12px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--ink-muted); + font-size: 0.8rem; font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: color var(--t-fast), background var(--t-fast); + outline: none; + white-space: nowrap; +} +.home-tab:hover:not(:disabled) { color: var(--ink-soft); background: rgba(255, 255, 255, 0.04); } +.home-tab.active { color: var(--accent-on); background: var(--accent); } +.home-tab:disabled { opacity: 0.5; cursor: not-allowed; } + +.home-search { + flex: 1; + position: relative; + max-width: 340px; +} +.home-search-icon { + position: absolute; left: 11px; top: 50%; transform: translateY(-50%); + color: var(--ink-muted); pointer-events: none; +} +.home-search-input { + width: 100%; + padding: 7px 12px 7px 34px; + border-radius: var(--r-sm); + border: 1px solid var(--panel-border); + background: var(--card); + color: var(--ink); + font-size: 0.82rem; font-family: inherit; + outline: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); +} +.home-search-input::placeholder { color: var(--ink-muted); } +.home-search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-surface); } + +.home-count { + font-size: 0.78rem; color: var(--ink-muted); font-weight: 500; + white-space: nowrap; margin-left: auto; +} + +/* Grid area */ +.home-grid-area { + flex: 1; + overflow-y: auto; + min-height: 0; + padding-right: 2px; + will-change: scroll-position; +} + +/* Empty / Loading states */ +.home-empty-state { + display: flex; flex-direction: column; + align-items: center; justify-content: center; + height: 100%; min-height: 260px; + gap: 12px; color: var(--ink-soft); text-align: center; +} +.home-empty-state h3 { font-size: 1.05rem; color: var(--ink); margin: 0; } +.home-empty-state p { font-size: 0.82rem; margin: 0; } +.home-empty-icon { color: var(--panel-border-solid); opacity: 0.5; } +.home-spinner { animation: spin 1s linear infinite; color: var(--accent); } + + +/* ====================================================== + GAME GRID (shared by Home + Library) + ====================================================== */ +.game-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; + padding-bottom: 16px; +} + +@media (min-width: 1600px) { .game-grid { grid-template-columns: repeat(6, 1fr); } } +@media (min-width: 1280px) and (max-width: 1599px) { .game-grid { grid-template-columns: repeat(5, 1fr); } } +@media (min-width: 1024px) and (max-width: 1279px) { .game-grid { grid-template-columns: repeat(4, 1fr); } } +@media (min-width: 768px) and (max-width: 1023px) { .game-grid { grid-template-columns: repeat(3, 1fr); } } +@media (max-width: 767px) { .game-grid { grid-template-columns: repeat(2, 1fr); } } + + +/* ====================================================== + GAME CARD + ====================================================== */ +.game-card { + position: relative; + display: flex; flex-direction: column; + background: var(--card); + border: 1px solid var(--panel-border); + border-radius: var(--r-md); + overflow: hidden; cursor: pointer; + transition: transform var(--t-normal), border-color var(--t-normal); + contain: layout style; + transform: translateZ(0); + backface-visibility: hidden; + will-change: transform; +} + +.game-card::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + opacity: 0; + transition: opacity var(--t-normal); + background: radial-gradient(120% 70% at 50% 0%, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0) 70%); +} + +.game-card:hover { + transform: translateY(-2px) scale(1.01); + border-color: rgba(255, 255, 255, 0.1); +} + +.game-card:hover::after { + opacity: 1; +} + +.game-card.selected { + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent-glow); +} + +/* Image wrapper — 16:9 landscape for GFN cover art */ +.game-card-image-wrapper { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; + background: var(--bg-c); + backface-visibility: hidden; +} +.game-card-image { + width: calc(100% + 2px); height: calc(100% + 2px); + margin: -1px; + object-fit: cover; + transition: transform var(--t-normal); +} +.game-card:hover .game-card-image { transform: scale(1.03); } + +.game-card-image-placeholder { + width: 100%; height: 100%; + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, var(--bg-c), var(--panel)); + color: var(--ink-muted); +} + +/* Play overlay */ +.game-card-overlay { + position: absolute; inset: 0; + display: flex; align-items: center; justify-content: center; + opacity: 0; + transition: opacity var(--t-normal); +} +.game-card:hover .game-card-overlay { opacity: 1; } + +.game-card-gradient { + position: absolute; inset: 0; + background: linear-gradient(to bottom, transparent 20%, rgba(10, 10, 12, 0.8) 100%); +} +.game-card-play-button { + position: relative; z-index: 1; + width: 44px; height: 44px; + border-radius: 50%; + background: var(--accent); border: none; + display: flex; align-items: center; justify-content: center; + cursor: pointer; + transition: transform var(--t-fast), background var(--t-fast); + box-shadow: 0 4px 16px var(--accent-glow); + color: var(--accent-on); +} +.game-card-play-button:hover { transform: scale(1.1); background: var(--accent-press); } + +/* Card info (bottom section) */ +.game-card-info { + padding: 10px 12px; + display: flex; flex-direction: column; gap: 6px; +} +.game-card-title { + margin: 0; + font-size: 0.8rem; font-weight: 600; color: var(--ink); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + line-height: 1.3; +} + +/* Store icon chips */ +.game-card-stores { + display: flex; + gap: 3px; + flex-wrap: wrap; +} +.game-card-store-chip { + display: flex; align-items: center; justify-content: center; + width: 22px; height: 22px; + background: var(--chip); + border-radius: 4px; + color: var(--ink-muted); + transition: color var(--t-fast), background var(--t-fast); +} +.game-card-store-chip:hover { + color: var(--ink-soft); + background: var(--panel-border-solid); +} +.store-svg { display: block; flex-shrink: 0; } + + +/* ====================================================== + LIBRARY PAGE + ====================================================== */ +.library-page { + display: flex; flex-direction: column; + height: 100%; max-width: 1600px; + margin: 0 auto; gap: 16px; overflow: hidden; +} + +.library-toolbar { + display: flex; align-items: center; gap: 12px; + flex-shrink: 0; +} +.library-title { + display: flex; align-items: center; gap: 8px; +} +.library-title-icon { color: var(--accent); flex-shrink: 0; } +.library-title h1 { font-size: 1.1rem; font-weight: 700; margin: 0; white-space: nowrap; } + +.library-search { + flex: 1; position: relative; max-width: 340px; +} +.library-search-icon { + position: absolute; left: 11px; top: 50%; transform: translateY(-50%); + color: var(--ink-muted); pointer-events: none; +} +.library-search-input { + width: 100%; + padding: 7px 12px 7px 34px; + border-radius: var(--r-sm); + border: 1px solid var(--panel-border); + background: var(--card); + color: var(--ink); + font-size: 0.82rem; font-family: inherit; outline: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); +} +.library-search-input::placeholder { color: var(--ink-muted); } +.library-search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-surface); } + +.library-count { + font-size: 0.78rem; color: var(--ink-muted); font-weight: 500; + white-space: nowrap; margin-left: auto; +} + +.library-grid-area { + flex: 1; overflow-y: auto; min-height: 0; padding-right: 2px; + will-change: scroll-position; +} + +.library-game-wrapper { + display: flex; flex-direction: column; gap: 4px; +} +.library-last-played { + display: flex; align-items: center; gap: 5px; + padding: 0 4px; + font-size: 0.7rem; color: var(--ink-muted); +} + +.library-empty-state { + display: flex; flex-direction: column; + align-items: center; justify-content: center; + height: 100%; min-height: 260px; + gap: 12px; color: var(--ink-soft); text-align: center; +} +.library-empty-state h3 { font-size: 1.05rem; color: var(--ink); margin: 0; } +.library-empty-state p { font-size: 0.82rem; margin: 0; max-width: 340px; } +.library-empty-icon { color: var(--panel-border-solid); opacity: 0.5; } +.library-spinner { animation: spin 1s linear infinite; color: var(--accent); } + + +/* ====================================================== + SETTINGS PAGE + ====================================================== */ +.settings-page { + max-width: 760px; + margin: 0 auto; + display: flex; flex-direction: column; gap: 18px; +} + +.settings-header { + display: flex; align-items: center; gap: 12px; + color: var(--ink); +} +.settings-header svg { width: 24px; height: 24px; } +.settings-header h1 { font-size: 1.35rem; font-weight: 700; margin: 0; flex: 1; } + +.settings-saved { + display: flex; align-items: center; gap: 5px; + padding: 6px 14px; + background: var(--accent-surface); + border-radius: var(--r-full); + font-size: 0.84rem; font-weight: 600; + color: var(--accent); + opacity: 0; transform: translateY(-3px); + transition: opacity var(--t-normal), transform var(--t-normal); pointer-events: none; +} +.settings-saved.visible { opacity: 1; transform: translateY(0); } + +/* Sections */ +.settings-sections { + display: flex; flex-direction: column; gap: 14px; +} + +.settings-section { + background: var(--card); + border: 1px solid var(--panel-border); + border-radius: var(--r-md); + padding: 18px 20px; + transition: border-color var(--t-normal); +} +.settings-section:hover { border-color: rgba(255, 255, 255, 0.08); } + +.settings-section-header { + display: flex; align-items: center; gap: 10px; + margin-bottom: 16px; padding-bottom: 12px; + border-bottom: 1px solid var(--panel-border); + color: var(--accent); +} +.settings-section-header svg { width: 20px; height: 20px; } +.settings-section-header h2 { + font-size: 1rem; font-weight: 600; color: var(--ink); margin: 0; +} + +/* Rows */ +.settings-rows { + display: flex; flex-direction: column; gap: 14px; +} +.settings-row { + display: flex; align-items: center; justify-content: space-between; gap: 14px; +} +.settings-row--column { + flex-direction: column; align-items: stretch; +} +.settings-row-top { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 8px; +} +.settings-shortcut-actions { + display: flex; + align-items: center; + gap: 8px; +} +.settings-shortcut-reset-btn { + padding: 5px 10px; + border: 1px solid var(--panel-border-solid); + border-radius: 6px; + background: var(--bg-a); + color: var(--ink-soft); + font-size: 0.74rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast); +} +.settings-shortcut-reset-btn:hover:not(:disabled) { + color: var(--accent); + border-color: rgba(88, 217, 138, 0.35); + background: var(--accent-surface); +} +.settings-shortcut-reset-btn:disabled { + opacity: 0.5; + cursor: default; +} + +.settings-label { + font-size: 0.92rem; color: var(--ink-soft); font-weight: 500; + flex-shrink: 0; cursor: default; +} + +/* Text input for resolution / FPS */ +.settings-text-input { + padding: 7px 12px; + background: var(--bg-a); + border: 1px solid var(--panel-border-solid); + border-radius: 6px; + color: var(--ink); + font-size: 0.82rem; font-family: inherit; outline: none; + min-width: 120px; + transition: border-color var(--t-fast), box-shadow var(--t-fast); +} +.settings-text-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-surface); } +.settings-text-input.error { border-color: var(--error); box-shadow: 0 0 0 2px var(--error-bg); } +.settings-text-input--narrow { min-width: 70px; max-width: 90px; } + +.settings-input-group { + display: flex; align-items: center; gap: 8px; +} +.settings-input-hint { + font-size: 0.82rem; color: var(--error); font-weight: 500; +} + +.settings-subtle-hint { + font-size: 0.8rem; + color: var(--ink-muted); + font-weight: 500; +} +.settings-shortcut-hint { + font-size: 0.72rem; + color: var(--ink-muted); +} +.settings-shortcut-grid { + display: grid; + gap: 8px; +} +.settings-shortcut-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.settings-shortcut-label { + font-size: 0.77rem; + color: var(--ink-soft); +} +.settings-shortcut-input { + min-width: 185px; + text-align: right; +} + +/* Chip row (presets) */ +.settings-chip-row { + display: flex; flex-wrap: wrap; gap: 4px; +} +.settings-chip { + display: flex; align-items: center; gap: 4px; + padding: 7px 12px; + border-radius: 6px; + border: 1px solid var(--panel-border); + background: var(--chip); + color: var(--ink-muted); + font-size: 0.84rem; font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast); + outline: none; + white-space: nowrap; +} +.settings-chip:hover { background: var(--panel-border-solid); color: var(--ink-soft); border-color: var(--panel-border-solid); } +.settings-chip.active { + background: var(--accent-surface-strong); color: var(--accent); + border-color: rgba(88, 217, 138, 0.25); +} + +/* Tier indicator on preset chips */ +.settings-chip-tier { + font-size: 0.62rem; font-weight: 800; + padding: 2px 4px; border-radius: 3px; + line-height: 1; text-transform: uppercase; + letter-spacing: 0.04em; +} +.settings-chip-tier.free { color: var(--ink-muted); background: rgba(255, 255, 255, 0.05); } +.settings-chip-tier.priority { color: #cdaf95; background: rgba(205, 175, 149, 0.1); } +.settings-chip-tier.ultimate { color: #ffd700; background: rgba(255, 215, 0, 0.1); } +.settings-chip.active .settings-chip-tier { opacity: 0.8; } + +/* Preset groups (aspect ratio grouped resolutions) */ +.settings-preset-groups { + display: flex; flex-direction: column; gap: 10px; +} +.settings-preset-group { + display: flex; flex-direction: column; gap: 5px; +} +.settings-preset-group-label { + font-size: 0.74rem; font-weight: 700; + color: var(--ink-muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +/* Loading spinner next to label */ +.settings-loading-icon { + display: inline-block; + margin-left: 6px; + vertical-align: middle; + color: var(--ink-muted); + animation: spin 1s linear infinite; +} + +/* Value badge */ +.settings-value-badge { + padding: 4px 12px; + background: var(--chip); + border-radius: 5px; + font-size: 0.86rem; font-weight: 700; + color: var(--accent); + white-space: nowrap; +} + +/* Slider */ +.settings-slider { + width: 100%; height: 6px; + background: var(--chip); + border-radius: 2px; + outline: none; + -webkit-appearance: none; + cursor: pointer; +} +.settings-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; height: 16px; + background: var(--accent); + border-radius: 50%; + cursor: pointer; + transition: transform var(--t-fast); +} +.settings-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } + +/* Toggle */ +.settings-toggle { + position: relative; + display: inline-block; + width: 44px; height: 24px; + cursor: pointer; +} +.settings-toggle input { opacity: 0; width: 0; height: 0; } +.settings-toggle-track { + position: absolute; inset: 0; + background: var(--chip); + border-radius: 10px; + transition: background var(--t-normal); +} +.settings-toggle-track::before { + content: ""; position: absolute; + top: 3px; left: 3px; + width: 18px; height: 18px; + background: white; border-radius: 50%; + transition: transform var(--t-normal); +} +.settings-toggle input:checked + .settings-toggle-track { background: var(--accent); } +.settings-toggle input:checked + .settings-toggle-track::before { transform: translateX(20px); } + +/* Placeholder */ +.settings-placeholder { + padding: 14px; text-align: center; + font-size: 0.92rem; color: var(--ink-muted); font-style: italic; +} + +/* Region selector */ +.region-selector { + position: relative; +} +.region-selected { + width: 100%; + display: flex; align-items: center; justify-content: space-between; + padding: 11px 14px; + background: var(--bg-a); + border: 1px solid var(--panel-border-solid); + border-radius: var(--r-sm); + color: var(--ink); font-size: 0.92rem; font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: border-color var(--t-fast), box-shadow var(--t-fast); + outline: none; +} +.region-selected:hover { border-color: #444; } +.region-selected.open { border-color: var(--accent); border-radius: var(--r-sm) var(--r-sm) 0 0; } +.region-selected-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.region-chevron { + color: var(--ink-muted); + transition: transform var(--t-fast); + flex-shrink: 0; +} +.region-chevron.flipped { transform: rotate(180deg); color: var(--accent); } + +.region-dropdown { + position: absolute; top: 100%; left: 0; right: 0; + background: var(--bg-a); + border: 1px solid var(--accent); border-top: none; + border-radius: 0 0 var(--r-sm) var(--r-sm); + z-index: 20; + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); + animation: fade-in-down 120ms var(--ease); +} +.region-dropdown-search { + display: flex; align-items: center; gap: 6px; + padding: 10px 12px; + border-bottom: 1px solid var(--panel-border); + position: relative; +} +.region-dropdown-search-icon { + color: var(--ink-muted); flex-shrink: 0; +} +.region-dropdown-search-input { + flex: 1; + padding: 5px 0; + background: transparent; border: none; + color: var(--ink); font-size: 0.9rem; font-family: inherit; + outline: none; +} +.region-dropdown-search-input::placeholder { color: var(--ink-muted); } +.region-dropdown-clear { + display: flex; align-items: center; justify-content: center; + width: 20px; height: 20px; + border-radius: 4px; border: none; background: var(--chip); + color: var(--ink-muted); cursor: pointer; + transition: color var(--t-fast), background var(--t-fast); +} +.region-dropdown-clear:hover { background: var(--panel-border-solid); color: var(--ink); } + +.region-dropdown-list { + max-height: 200px; overflow-y: auto; +} +.region-dropdown-item { + width: 100%; + display: flex; align-items: center; gap: 8px; + padding: 10px 14px; + background: transparent; border: none; + color: var(--ink); font-size: 0.9rem; font-weight: 500; + font-family: inherit; + cursor: pointer; text-align: left; + transition: background var(--t-fast); +} +.region-dropdown-item:hover { background: var(--accent-surface); } +.region-dropdown-item.active { background: var(--accent-surface-strong); color: var(--accent); } +.region-dropdown-item span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.region-check { color: var(--accent); flex-shrink: 0; } + +.region-dropdown-empty { + padding: 14px; text-align: center; + font-size: 0.9rem; color: var(--ink-muted); +} + +/* Footer */ +.settings-footer { + display: flex; justify-content: flex-end; +} +.settings-save-btn { + display: flex; align-items: center; gap: 7px; + padding: 11px 20px; + background: linear-gradient(135deg, var(--accent), var(--accent-press)); + border: none; border-radius: var(--r-sm); + color: var(--accent-on); font-size: 0.92rem; font-weight: 700; + font-family: inherit; + cursor: pointer; + transition: transform var(--t-normal), box-shadow var(--t-normal); + box-shadow: 0 4px 16px var(--accent-glow); +} +.settings-save-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px var(--accent-glow); } +.settings-save-btn:active { transform: translateY(0); } + +/* Codec Diagnostics */ +.codec-test-btn { + display: flex; align-items: center; gap: 8px; + padding: 9px 18px; + background: var(--bg-c); + border: 1px solid var(--panel-border-solid); + border-radius: 6px; + color: var(--ink-soft); + font-size: 0.88rem; font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast); + white-space: nowrap; + flex-shrink: 0; +} +.codec-test-btn:hover { background: var(--panel-border-solid); color: var(--ink); border-color: var(--accent); } +.codec-test-btn:disabled { opacity: 0.6; cursor: not-allowed; } + +.codec-test-description { + font-size: 0.9rem; + line-height: 1.4; + color: var(--ink-soft); + flex: 1 1 320px; + min-width: 0; + flex-shrink: 1; + white-space: normal; +} + +.codec-test-row { + align-items: flex-start; + flex-wrap: wrap; +} + +.codec-test-row .codec-test-btn { + margin-left: auto; +} + +@media (max-width: 820px) { + .codec-test-row .codec-test-btn { + margin-left: 0; + width: 100%; + justify-content: center; + } +} + +.codec-results { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 12px; +} + +.codec-result-card { + background: var(--bg-a); + border: 1px solid var(--panel-border); + border-radius: 10px; + padding: 16px 18px; + display: flex; flex-direction: column; gap: 12px; + transition: border-color var(--t-fast); +} +.codec-result-card:hover { border-color: rgba(255, 255, 255, 0.1); } + +.codec-result-header { + display: flex; align-items: center; justify-content: space-between; gap: 10px; +} + +.codec-result-name { + font-size: 1.1rem; font-weight: 700; color: var(--ink); + letter-spacing: 0.02em; +} + +.codec-result-badge { + font-size: 0.72rem; font-weight: 700; + padding: 3px 8px; border-radius: 5px; + text-transform: uppercase; letter-spacing: 0.04em; + white-space: nowrap; +} +.codec-result-badge.supported { + color: var(--success); background: rgba(74, 222, 128, 0.1); + border: 1px solid rgba(74, 222, 128, 0.2); +} +.codec-result-badge.unsupported { + color: var(--ink-muted); background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--panel-border); +} + +.codec-result-rows { + display: flex; flex-direction: column; gap: 8px; +} + +.codec-result-row { + display: flex; align-items: center; gap: 10px; + font-size: 0.88rem; +} + +.codec-result-direction { + color: var(--ink-muted); font-weight: 600; + min-width: 54px; + text-transform: uppercase; + font-size: 0.78rem; + letter-spacing: 0.04em; +} + +.codec-result-status { + font-weight: 700; font-size: 0.82rem; + padding: 2px 8px; border-radius: 5px; + min-width: 38px; text-align: center; +} +.codec-result-status.hw { + color: var(--accent); background: var(--accent-surface); +} +.codec-result-status.sw { + color: var(--warning); background: rgba(251, 191, 36, 0.1); +} +.codec-result-status.none { + color: var(--error); background: var(--error-bg); +} + +.codec-result-via { + color: var(--ink-muted); font-size: 0.84rem; + flex: 1; +} + +.codec-result-profiles { + border-top: 1px solid var(--panel-border); + padding-top: 10px; + display: flex; flex-direction: column; gap: 6px; +} + +.codec-result-profiles-label { + font-size: 0.72rem; font-weight: 700; + color: var(--ink-muted); text-transform: uppercase; + letter-spacing: 0.05em; +} + +.codec-result-profiles-list { + display: flex; flex-wrap: wrap; gap: 5px; +} + +.codec-result-profile { + font-size: 0.74rem; + padding: 2px 7px; border-radius: 4px; + background: rgba(255, 255, 255, 0.04); + color: var(--ink-muted); + font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace; + word-break: break-all; +} + + +/* ====================================================== + STREAM VIEW (.sv-) + ====================================================== */ +.sv { + position: fixed; inset: 0; + background: #000; z-index: 1000; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; +} +.sv-video { + width: 100%; height: 100%; + object-fit: contain; display: block; + position: relative; z-index: 1; + outline: none; +} +.sv-video:focus, +.sv-video:focus-visible { + outline: none; + box-shadow: none; +} +.sv-empty { + position: absolute; inset: 0; z-index: 0; + display: flex; align-items: center; justify-content: center; + pointer-events: none; +} +.sv-empty-grad { + position: absolute; inset: 0; + background: linear-gradient(135deg, #0a0a0c 0%, #19191e 50%, #0a0a0c 100%); +} + +/* Connecting overlay (inside StreamView) */ +.sv-connect { + position: absolute; inset: 0; + display: flex; align-items: center; justify-content: center; z-index: 10; +} +.sv-connect-inner { + display: flex; flex-direction: column; align-items: center; gap: 14px; + animation: fade-in 300ms var(--ease); +} +.sv-connect-spin { color: var(--accent); animation: spin 1s linear infinite; } +.sv-connect-title { font-size: 1.05rem; font-weight: 600; color: var(--ink); margin: 0; } +.sv-connect-sub { font-size: 0.82rem; color: var(--ink-soft); margin: 0; } + +.sv-session-clock { + position: fixed; + top: 14px; + left: 50%; + transform: translateX(-50%); + z-index: 1001; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 12px; + border-radius: var(--r-md); + border: 1px solid var(--panel-border); + background: rgba(10, 10, 12, 0.9); + color: var(--ink); + font-size: 0.74rem; + font-weight: 700; + letter-spacing: 0.01em; + font-variant-numeric: tabular-nums; + backdrop-filter: blur(4px); +} +.sv-session-clock svg { + color: var(--accent); +} + +.sv-time-warning { + position: fixed; + top: 54px; + left: 50%; + transform: translateX(-50%); + z-index: 1002; + display: inline-flex; + align-items: center; + gap: 7px; + max-width: min(86vw, 540px); + padding: 8px 12px; + border-radius: var(--r-md); + border: 1px solid var(--panel-border); + background: rgba(10, 10, 12, 0.93); + color: var(--ink); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.01em; + animation: fade-in 140ms var(--ease); + backdrop-filter: blur(5px); +} +.sv-time-warning--warn { + border-color: color-mix(in srgb, var(--warning) 55%, var(--panel-border)); + color: #f8e5b0; +} +.sv-time-warning--warn svg { + color: var(--warning); +} +.sv-time-warning--critical { + border-color: color-mix(in srgb, var(--error) 65%, var(--panel-border)); + color: #ffd0d0; +} +.sv-time-warning--critical svg { + color: var(--error); +} + +/* Stats HUD (StreamView inline stats) */ +.sv-stats { + position: fixed; top: 14px; right: 14px; z-index: 1001; + display: flex; flex-direction: column; gap: 5px; + padding: 8px 10px; + background: rgba(10, 10, 12, 0.9); + border: 1px solid var(--panel-border); border-radius: var(--r-md); + font-size: 0.7rem; min-width: 240px; max-width: 320px; font-variant-numeric: tabular-nums; + backdrop-filter: blur(4px); +} +.sv-stats-head { + display: flex; align-items: center; justify-content: space-between; gap: 8px; + font-weight: 700; color: var(--ink); font-size: 0.75rem; + line-height: 1.1; +} +.sv-stats-primary { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sv-stats-wait { color: var(--ink-muted); font-style: italic; font-weight: 500; } +.sv-stats-live { + display: inline-flex; align-items: center; justify-content: center; + min-width: 40px; + padding: 1px 7px; + border-radius: 999px; + font-size: 0.62rem; + letter-spacing: 0.03em; + border: 1px solid var(--panel-border); + color: var(--ink-soft); + background: rgba(255, 255, 255, 0.03); +} +.sv-stats-live.is-live { + color: var(--success); + border-color: color-mix(in srgb, var(--success) 45%, var(--panel-border)); + background: color-mix(in srgb, var(--success) 12%, transparent); +} +.sv-stats-live.is-pending { + color: var(--warning); + border-color: color-mix(in srgb, var(--warning) 40%, var(--panel-border)); + background: color-mix(in srgb, var(--warning) 10%, transparent); +} +.sv-stats-sub { + display: flex; align-items: center; justify-content: space-between; gap: 8px; + padding-top: 1px; +} +.sv-stats-sub-left { + display: inline-flex; align-items: center; gap: 6px; + color: var(--ink-soft); +} +.sv-stats-sub-right { + color: var(--ink); + font-weight: 600; +} +.sv-stats-hdr { + padding: 1px 5px; background: var(--accent-surface); + border-radius: 3px; font-size: 0.6rem; font-weight: 700; + color: var(--accent); text-transform: uppercase; +} +.sv-stats-metrics { + display: flex; align-items: center; gap: 4px; + flex-wrap: wrap; +} +.sv-stats-chip { + display: inline-flex; align-items: center; gap: 5px; + padding: 2px 5px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--panel-border); + border-radius: 4px; + font-size: 0.64rem; + color: var(--ink-muted); +} +.sv-stats-chip-val { + font-weight: 700; + color: var(--ink-soft); +} +.sv-stats-foot { + font-size: 0.64rem; + color: var(--ink-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Controller indicator */ +.sv-ctrl { + position: fixed; top: 14px; left: 14px; + display: flex; align-items: center; gap: 6px; + padding: 7px 11px; + background: rgba(10, 10, 12, 0.92); + border: 1px solid var(--panel-border); border-radius: var(--r-md); + z-index: 1001; color: var(--accent); +} +.sv-ctrl-n { font-size: 0.75rem; font-weight: 700; color: var(--ink); } + +.sv-afk { + position: fixed; top: 14px; left: 14px; + display: inline-flex; align-items: center; gap: 7px; + padding: 7px 11px; + background: rgba(10, 10, 12, 0.92); + border: 1px solid var(--panel-border); border-radius: var(--r-md); + z-index: 1001; color: var(--success); +} +.sv-afk--stacked { + top: 56px; +} +.sv-afk-dot { + width: 8px; height: 8px; border-radius: 999px; + background: var(--success); + box-shadow: 0 0 8px rgba(88, 217, 138, 0.8); +} +.sv-afk-label { + font-size: 0.72rem; font-weight: 700; color: var(--ink); + letter-spacing: 0.03em; +} + +.sv-esc-hold-backdrop { + position: fixed; inset: 0; + z-index: 1200; + pointer-events: none; + background: rgba(8, 9, 10, 0.24); + backdrop-filter: blur(7px) saturate(110%); + animation: fade-in 140ms var(--ease); +} +.sv-esc-hold { + position: fixed; top: 20%; left: 50%; transform: translateX(-50%); + z-index: 1201; + width: min(520px, calc(100vw - 40px)); + padding: 16px 18px; + background: rgba(10, 10, 12, 0.92); + border: 1px solid var(--panel-border); + border-radius: calc(var(--r-md) + 2px); + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.45); + animation: fade-in 180ms var(--ease), float-a 8s ease-in-out infinite; +} +.sv-esc-hold-title { + font-size: 1.02rem; + font-weight: 800; + letter-spacing: 0.01em; + color: var(--ink); +} +.sv-esc-hold-head { + display: flex; align-items: center; justify-content: space-between; gap: 8px; + margin-top: 6px; + font-size: 0.82rem; color: var(--ink-soft); font-weight: 700; +} +.sv-esc-hold-track { + margin-top: 10px; + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.08); +} +.sv-esc-hold-fill { + display: block; + width: 100%; + height: 100%; + transform-origin: left center; + transform: scaleX(0); + transition: transform 50ms linear; + background: linear-gradient(90deg, #e6b64c 0%, #f07b3f 55%, #f05050 100%); +} + +.sv-exit { + position: fixed; + inset: 0; + z-index: 1300; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} +.sv-exit-backdrop { + position: absolute; + inset: 0; + border: none; + background: + radial-gradient(circle at 20% 20%, rgba(88, 217, 138, 0.08), transparent 42%), + radial-gradient(circle at 82% 78%, rgba(239, 68, 68, 0.12), transparent 45%), + rgba(5, 6, 7, 0.58); + backdrop-filter: blur(14px) saturate(130%); + cursor: pointer; +} +.sv-exit-card { + position: relative; + z-index: 1; + width: min(520px, calc(100vw - 40px)); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: + linear-gradient(148deg, rgba(88, 217, 138, 0.08) 0%, rgba(88, 217, 138, 0.02) 24%, transparent 55%), + linear-gradient(180deg, rgba(14, 16, 18, 0.98), rgba(10, 11, 13, 0.96)); + box-shadow: + 0 24px 64px rgba(0, 0, 0, 0.55), + 0 0 0 1px rgba(88, 217, 138, 0.16) inset; + padding: 20px 20px 16px; + animation: fade-in 170ms var(--ease), float-a 10s ease-in-out infinite; +} +.sv-exit-kicker { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.67rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); +} +.sv-exit-title { + margin: 6px 0 0; + font-size: 1.34rem; + font-weight: 800; + color: var(--ink); + letter-spacing: 0.01em; +} +.sv-exit-text { + margin: 11px 0 0; + font-size: 0.96rem; + color: var(--ink-soft); + line-height: 1.45; +} +.sv-exit-text strong { + color: var(--ink); +} +.sv-exit-subtext { + margin: 6px 0 0; + font-size: 0.82rem; + color: var(--ink-muted); +} +.sv-exit-actions { + margin-top: 15px; + display: flex; + gap: 10px; +} +.sv-exit-btn { + flex: 1; + border-radius: 10px; + padding: 10px 12px; + border: 1px solid var(--panel-border); + background: rgba(255, 255, 255, 0.03); + color: var(--ink); + font-size: 0.83rem; + font-weight: 700; + font-family: inherit; + letter-spacing: 0.01em; + cursor: pointer; + transition: transform var(--t-fast), border-color var(--t-fast), background var(--t-fast), color var(--t-fast); +} +.sv-exit-btn:hover { + transform: translateY(-1px); +} +.sv-exit-btn:active { + transform: translateY(0); +} +.sv-exit-btn-cancel:hover { + border-color: var(--panel-border-solid); + background: rgba(255, 255, 255, 0.07); +} +.sv-exit-btn-confirm { + background: linear-gradient(140deg, rgba(239, 68, 68, 0.26), rgba(239, 68, 68, 0.38)); + border-color: rgba(239, 68, 68, 0.5); + color: #ffeaea; +} +.sv-exit-btn-confirm:hover { + border-color: rgba(239, 68, 68, 0.7); + background: linear-gradient(140deg, rgba(239, 68, 68, 0.35), rgba(239, 68, 68, 0.52)); +} +.sv-exit-hint { + margin-top: 12px; + font-size: 0.73rem; + color: var(--ink-muted); +} +.sv-exit-hint kbd { + display: inline-block; + margin: 0 2px; + padding: 2px 6px; + border-radius: 5px; + border: 1px solid var(--panel-border-solid); + background: rgba(255, 255, 255, 0.04); + color: var(--ink-soft); + font-size: 0.69rem; +} + +/* Fullscreen button */ +.sv-fs { + position: fixed; bottom: 18px; right: 18px; z-index: 1001; + width: 38px; height: 38px; + border-radius: var(--r-sm); + border: 1px solid var(--panel-border); + background: rgba(10, 10, 12, 0.9); + color: var(--ink-muted); cursor: pointer; + display: flex; align-items: center; justify-content: center; + transition: opacity var(--t-fast), background var(--t-fast), border-color var(--t-fast), color var(--t-fast); opacity: 0.5; +} +.sv-fs:hover { opacity: 1; background: rgba(10, 10, 12, 0.95); border-color: var(--accent); color: var(--accent); } + +/* End session button */ +.sv-end { + position: fixed; bottom: 18px; right: 64px; z-index: 1001; + width: 38px; height: 38px; + border-radius: var(--r-sm); + border: 1px solid var(--panel-border); + background: rgba(10, 10, 12, 0.9); + color: var(--ink-muted); cursor: pointer; + display: flex; align-items: center; justify-content: center; + transition: opacity var(--t-fast), background var(--t-fast), border-color var(--t-fast), color var(--t-fast); opacity: 0.5; +} +.sv-end:hover { opacity: 1; background: rgba(180, 30, 30, 0.9); border-color: var(--error); color: #fff; } + +/* Keyboard hints */ +.sv-hints { + position: fixed; bottom: 18px; left: 18px; z-index: 1001; + display: flex; flex-direction: column; gap: 4px; + padding: 8px 11px; + background: rgba(10, 10, 12, 0.9); + border: 1px solid var(--panel-border); border-radius: var(--r-md); + animation: fade-in 300ms var(--ease); opacity: 0.65; +} +.sv-hint { display: flex; align-items: center; gap: 6px; font-size: 0.7rem; color: var(--ink-muted); } +.sv-hint kbd { + padding: 2px 5px; background: var(--chip); + border: 1px solid var(--panel-border-solid); border-radius: 3px; + font-size: 0.65rem; font-family: inherit; color: var(--ink-soft); +} + +/* Game title toast */ +.sv-title-bar { + position: fixed; bottom: 68px; left: 50%; transform: translateX(-50%); z-index: 1001; + padding: 7px 18px; + background: rgba(10, 10, 12, 0.9); + border: 1px solid var(--panel-border); border-radius: var(--r-sm); + font-size: 0.88rem; font-weight: 600; color: var(--ink); + white-space: nowrap; animation: fade-in 400ms var(--ease); +} + +@media (max-width: 640px) { + .sv-session-clock { + top: 10px; + padding: 6px 10px; + font-size: 0.7rem; + } + .sv-time-warning { + top: 46px; + max-width: calc(100vw - 24px); + padding: 7px 10px; + font-size: 0.68rem; + } + .sv-exit { + align-items: flex-end; + padding: 14px; + } + .sv-exit-card { + width: 100%; + border-radius: 14px; + padding: 16px 16px 14px; + } + .sv-exit-title { + font-size: 1.12rem; + } + .sv-exit-actions { + flex-direction: column; + } +} + + +/* ====================================================== + STREAM LOADING (.sload-) + ====================================================== */ +.sload { + position: fixed; inset: 0; z-index: 1000; + display: flex; align-items: center; justify-content: center; padding: 24px; +} +.sload-backdrop { + position: absolute; inset: 0; + background: linear-gradient(135deg, rgba(10, 10, 12, 0.98), rgba(19, 19, 22, 0.99), rgba(10, 10, 12, 0.98)); +} +.sload-glow { + position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); + width: 400px; height: 400px; + background: radial-gradient(circle, rgba(88, 217, 138, 0.06) 0%, transparent 70%); + pointer-events: none; + animation: float-a 16s ease-in-out infinite; +} +.sload.sload--error .sload-glow { + background: radial-gradient(circle, rgba(248, 113, 113, 0.14) 0%, transparent 72%); +} +.sload-content { + position: relative; z-index: 1; + display: flex; flex-direction: column; align-items: center; gap: 32px; + max-width: 440px; width: 100%; + animation: fade-in 350ms var(--ease); +} + +/* Game info */ +.sload-game { display: flex; align-items: center; gap: 16px; text-align: left; } +.sload-cover { + width: 68px; height: 68px; border-radius: var(--r-md); + overflow: hidden; flex-shrink: 0; + box-shadow: var(--shadow-md); + position: relative; +} +.sload-cover-img { width: 100%; height: 100%; object-fit: cover; display: block; } +.sload-cover-empty { + width: 100%; height: 100%; + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, var(--bg-c), var(--panel)); + color: var(--ink-soft); +} +.sload-cover-shine { + position: absolute; inset: 0; + background: linear-gradient(135deg, transparent 40%, rgba(255, 255, 255, 0.06) 50%, transparent 60%); + pointer-events: none; +} + +.sload-game-meta { display: flex; flex-direction: column; gap: 3px; } +.sload-label { + font-size: 0.7rem; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); +} +.sload-title { + font-size: 1.3rem; font-weight: 700; color: var(--ink); line-height: 1.25; margin: 0; + max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} + +/* Progress steps */ +.sload-steps { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; } +.sload-step { + display: flex; flex-direction: column; align-items: center; gap: 7px; + flex: 1; position: relative; +} +.sload-step-dot { + width: 38px; height: 38px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + background: var(--bg-c); + border: 2px solid var(--panel-border-solid); + color: var(--ink-muted); + transition: background var(--t-slow), border-color var(--t-slow), color var(--t-slow), transform var(--t-slow); +} +.sload-step.active .sload-step-dot { + background: linear-gradient(135deg, var(--accent), var(--accent-press)); + border-color: var(--accent); color: var(--accent-on); + animation: pulse-glow 2s ease-in-out infinite; +} +.sload-step.completed .sload-step-dot { + background: rgba(88, 217, 138, 0.1); + border-color: var(--accent); color: var(--accent); +} +.sload-step.pending .sload-step-dot { opacity: 0.35; } +.sload-step.failed .sload-step-dot { + background: linear-gradient(135deg, rgba(248, 113, 113, 0.28), rgba(239, 68, 68, 0.32)); + border-color: var(--error); + color: #ffe3e3; + animation: pulse-glow 2s ease-in-out infinite; +} + +.sload-step-name { + font-size: 0.7rem; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.05em; + color: var(--ink-muted); transition: color var(--t-slow); +} +.sload-step.active .sload-step-name { color: var(--ink); } +.sload-step.completed .sload-step-name { color: var(--accent); } +.sload-step.failed .sload-step-name { color: var(--error); } + +.sload-step-line { + position: absolute; top: 19px; + left: calc(50% + 23px); width: calc(100% - 38px); + height: 2px; background: var(--panel-border-solid); overflow: hidden; +} +.sload-step-line-fill { + height: 100%; width: 0%; + background: linear-gradient(90deg, var(--accent), var(--accent-press)); + transition: width 500ms var(--ease); +} +.sload-step-line.failed .sload-step-line-fill { + width: 100%; + background: linear-gradient(90deg, #f87171, #ef4444); +} +.sload-step.completed .sload-step-line-fill { width: 100%; } +.sload-step.active .sload-step-line-fill { width: 50%; } + +/* Status */ +.sload-status { display: flex; flex-direction: column; align-items: center; gap: 12px; text-align: center; } +.sload-spin { color: var(--accent); animation: spin 1s linear infinite; } +.sload-error-icon { color: var(--error); } +.sload-status-text { display: flex; flex-direction: column; gap: 5px; } +.sload-message { margin: 0; font-size: 0.95rem; font-weight: 500; color: var(--ink); } +.sload-queue { margin: 0; font-size: 0.82rem; color: var(--ink-soft); } +.sload-queue-num { color: var(--accent); font-weight: 700; font-size: 1rem; } +.sload-wait { color: var(--ink-muted); } +.sload-status--error .sload-message { color: #ffdada; } +.sload-error-title { margin: 0; font-size: 0.9rem; font-weight: 700; color: #ffd5d5; } +.sload-error-desc { margin: 0; font-size: 0.8rem; color: #f8b4b4; max-width: 360px; line-height: 1.4; } +.sload-error-code { + margin: 0; + font-size: 0.72rem; + color: #fca5a5; + font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + letter-spacing: 0.01em; +} +.sload.sload--error .sload-queue, +.sload.sload--error .sload-queue-num, +.sload.sload--error .sload-wait { + color: #f8b4b4; +} + +/* Cancel */ +.sload-cancel { + display: flex; align-items: center; gap: 6px; + padding: 8px 16px; border-radius: var(--r-sm); + border: 1px solid var(--panel-border-solid); + background: var(--bg-c); + color: var(--ink-muted); font-size: 0.8rem; font-weight: 600; + font-family: inherit; + cursor: pointer; transition: transform var(--t-normal), background var(--t-normal), border-color var(--t-normal), color var(--t-normal); +} +.sload-cancel:hover { + background: var(--card-hover); + border-color: var(--ink-muted); color: var(--ink); + transform: translateY(-1px); +} +.sload-cancel:active { transform: translateY(0); } + + +/* ====================================================== + STATS OVERLAY COMPONENT (.sovl-) + ====================================================== */ +.sovl { + position: fixed; top: 14px; right: 14px; z-index: 1001; +} +.sovl-body { + display: flex; flex-wrap: wrap; gap: 5px; + padding: 8px; + background: rgba(10, 10, 12, 0.94); + border: 1px solid var(--panel-border); border-radius: var(--r-md); + max-width: 260px; +} +.sovl-pill { + display: flex; align-items: center; gap: 4px; + padding: 4px 7px; background: var(--card); + border-radius: 6px; font-size: 0.7rem; color: var(--ink-soft); +} +.sovl-icon { width: 13px; height: 13px; opacity: 0.65; flex-shrink: 0; } +.sovl-icon--ok { color: var(--accent); opacity: 1; } +.sovl-val { font-weight: 600; color: var(--ink); } +.sovl-badge { + padding: 1px 5px; background: var(--accent-surface); + border-radius: 3px; font-size: 0.62rem; font-weight: 700; color: var(--accent); +} +.sovl-badge--hdr { background: rgba(251, 191, 36, 0.1); color: var(--warning); } +.sovl-connecting { padding: 6px 10px; font-size: 0.75rem; color: var(--ink-muted); } +.sovl-pill--warn { background: rgba(251, 191, 36, 0.08); color: var(--warning); } +.sovl-pill--warn .sovl-val { color: var(--warning); } +.sovl-region { + width: 100%; margin-top: 3px; padding-top: 5px; + border-top: 1px solid var(--panel-border); + font-size: 0.65rem; color: var(--ink-muted); text-align: center; +} + + +/* ====================================================== + REDUCED MOTION + ====================================================== */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + /* Keep loading indicator visibly active even with reduced motion. */ + .sload-spin { + animation-name: spin !important; + animation-duration: 1s !important; + animation-timing-function: linear !important; + animation-iteration-count: infinite !important; + } +} diff --git a/opennow-stable/src/renderer/src/vite-env.d.ts b/opennow-stable/src/renderer/src/vite-env.d.ts new file mode 100644 index 0000000..84e627a --- /dev/null +++ b/opennow-stable/src/renderer/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +import type { OpenNowApi } from "@shared/gfn"; + +declare global { + interface Window { + openNow: OpenNowApi; + } +} + +export {}; diff --git a/opennow-stable/src/shared/gfn.ts b/opennow-stable/src/shared/gfn.ts new file mode 100644 index 0000000..2962970 --- /dev/null +++ b/opennow-stable/src/shared/gfn.ts @@ -0,0 +1,321 @@ +export type VideoCodec = "H264" | "H265" | "AV1"; +export type VideoAccelerationPreference = "auto" | "hardware" | "software"; + +/** Color quality (bit depth + chroma subsampling), matching Rust ColorQuality enum */ +export type ColorQuality = "8bit_420" | "8bit_444" | "10bit_420" | "10bit_444"; + +/** Helper: get CloudMatch bitDepth value (0 = 8-bit SDR, 10 = 10-bit HDR capable) */ +export function colorQualityBitDepth(cq: ColorQuality): number { + return cq.startsWith("10bit") ? 10 : 0; +} + +/** Helper: get CloudMatch chromaFormat value (0 = 4:2:0, 2 = 4:4:4) */ +export function colorQualityChromaFormat(cq: ColorQuality): number { + return cq.endsWith("444") ? 2 : 0; +} + +/** Helper: does this color quality mode require HEVC or AV1? */ +export function colorQualityRequiresHevc(cq: ColorQuality): boolean { + return cq !== "8bit_420"; +} + +/** Helper: is this a 10-bit (HDR-capable) mode? */ +export function colorQualityIs10Bit(cq: ColorQuality): boolean { + return cq.startsWith("10bit"); +} + +export interface Settings { + resolution: string; + fps: number; + maxBitrateMbps: number; + codec: VideoCodec; + decoderPreference: VideoAccelerationPreference; + encoderPreference: VideoAccelerationPreference; + colorQuality: ColorQuality; + region: string; + clipboardPaste: boolean; + mouseSensitivity: number; + shortcutToggleStats: string; + shortcutTogglePointerLock: string; + shortcutStopStream: string; + shortcutToggleAntiAfk: string; + windowWidth: number; + windowHeight: number; +} + +export interface LoginProvider { + idpId: string; + code: string; + displayName: string; + streamingServiceUrl: string; + priority: number; +} + +export interface AuthTokens { + accessToken: string; + refreshToken?: string; + idToken?: string; + expiresAt: number; +} + +export interface AuthUser { + userId: string; + displayName: string; + email?: string; + avatarUrl?: string; + membershipTier: string; +} + +export interface EntitledResolution { + width: number; + height: number; + fps: number; +} + +export interface StorageAddon { + type: "PERMANENT_STORAGE"; + sizeGb?: number; + usedGb?: number; + regionName?: string; + regionCode?: string; +} + +export interface SubscriptionInfo { + membershipTier: string; + subscriptionType?: string; + subscriptionSubType?: string; + allottedHours: number; + purchasedHours: number; + rolledOverHours: number; + usedHours: number; + remainingHours: number; + totalHours: number; + firstEntitlementStartDateTime?: string; + serverRegionId?: string; + currentSpanStartDateTime?: string; + currentSpanEndDateTime?: string; + notifyUserWhenTimeRemainingInMinutes?: number; + notifyUserOnSessionWhenRemainingTimeInMinutes?: number; + state?: string; + isGamePlayAllowed?: boolean; + isUnlimited: boolean; + storageAddon?: StorageAddon; + entitledResolutions: EntitledResolution[]; +} + +export interface AuthSession { + provider: LoginProvider; + tokens: AuthTokens; + user: AuthUser; +} + +export interface AuthLoginRequest { + providerIdpId?: string; +} + +export interface AuthSessionRequest { + forceRefresh?: boolean; +} + +export type AuthRefreshOutcome = "not_attempted" | "refreshed" | "failed" | "missing_refresh_token"; + +export interface AuthRefreshStatus { + attempted: boolean; + forced: boolean; + outcome: AuthRefreshOutcome; + message: string; + error?: string; +} + +export interface AuthSessionResult { + session: AuthSession | null; + refresh: AuthRefreshStatus; +} + +export interface RegionsFetchRequest { + token?: string; +} + +export interface StreamRegion { + name: string; + url: string; +} + +export interface GamesFetchRequest { + token?: string; + providerStreamingBaseUrl?: string; +} + +export interface ResolveLaunchIdRequest { + token?: string; + providerStreamingBaseUrl?: string; + appIdOrUuid: string; +} + +export interface SubscriptionFetchRequest { + token?: string; + providerStreamingBaseUrl?: string; + userId: string; +} + +export interface GameVariant { + id: string; + store: string; + supportedControls: string[]; +} + +export interface GameInfo { + id: string; + uuid?: string; + launchAppId?: string; + title: string; + description?: string; + imageUrl?: string; + playType?: string; + membershipTierLabel?: string; + selectedVariantIndex: number; + variants: GameVariant[]; +} + +export interface StreamSettings { + resolution: string; + fps: number; + maxBitrateMbps: number; + codec: VideoCodec; + colorQuality: ColorQuality; +} + +export interface SessionCreateRequest { + token?: string; + streamingBaseUrl?: string; + appId: string; + internalTitle: string; + accountLinked?: boolean; + zone: string; + settings: StreamSettings; +} + +export interface SessionPollRequest { + token?: string; + streamingBaseUrl?: string; + serverIp?: string; + zone: string; + sessionId: string; +} + +export interface SessionStopRequest { + token?: string; + streamingBaseUrl?: string; + serverIp?: string; + zone: string; + sessionId: string; +} + +export interface IceServer { + urls: string[]; + username?: string; + credential?: string; +} + +export interface MediaConnectionInfo { + ip: string; + port: number; +} + +export interface SessionInfo { + sessionId: string; + status: number; + zone: string; + streamingBaseUrl?: string; + serverIp: string; + signalingServer: string; + signalingUrl: string; + gpuType?: string; + iceServers: IceServer[]; + mediaConnectionInfo?: MediaConnectionInfo; +} + +/** Information about an active session from getActiveSessions */ +export interface ActiveSessionInfo { + sessionId: string; + appId: number; + gpuType?: string; + status: number; + serverIp?: string; + signalingUrl?: string; + resolution?: string; + fps?: number; +} + +/** Request to claim/resume an existing session */ +export interface SessionClaimRequest { + token?: string; + streamingBaseUrl?: string; + sessionId: string; + serverIp: string; + appId?: string; + settings?: StreamSettings; +} + +export interface SignalingConnectRequest { + sessionId: string; + signalingServer: string; + signalingUrl?: string; +} + +export interface IceCandidatePayload { + candidate: string; + sdpMid?: string | null; + sdpMLineIndex?: number | null; + usernameFragment?: string | null; +} + +export interface SendAnswerRequest { + sdp: string; + nvstSdp?: string; +} + +export type MainToRendererSignalingEvent = + | { type: "connected" } + | { type: "disconnected"; reason: string } + | { type: "offer"; sdp: string } + | { type: "remote-ice"; candidate: IceCandidatePayload } + | { type: "error"; message: string } + | { type: "log"; message: string }; + +/** Dialog result for session conflict resolution */ +export type SessionConflictChoice = "resume" | "new" | "cancel"; + +export interface OpenNowApi { + getAuthSession(input?: AuthSessionRequest): Promise; + getLoginProviders(): Promise; + getRegions(input?: RegionsFetchRequest): Promise; + login(input: AuthLoginRequest): Promise; + logout(): Promise; + fetchSubscription(input: SubscriptionFetchRequest): Promise; + fetchMainGames(input: GamesFetchRequest): Promise; + fetchLibraryGames(input: GamesFetchRequest): Promise; + fetchPublicGames(): Promise; + resolveLaunchAppId(input: ResolveLaunchIdRequest): Promise; + createSession(input: SessionCreateRequest): Promise; + pollSession(input: SessionPollRequest): Promise; + stopSession(input: SessionStopRequest): Promise; + /** Get list of active sessions (status 2 or 3) */ + getActiveSessions(token?: string, streamingBaseUrl?: string): Promise; + /** Claim/resume an existing session */ + claimSession(input: SessionClaimRequest): Promise; + /** Show dialog asking user how to handle session conflict */ + showSessionConflictDialog(): Promise; + connectSignaling(input: SignalingConnectRequest): Promise; + disconnectSignaling(): Promise; + sendAnswer(input: SendAnswerRequest): Promise; + sendIceCandidate(input: IceCandidatePayload): Promise; + onSignalingEvent(listener: (event: MainToRendererSignalingEvent) => void): () => void; + /** Listen for F11 fullscreen toggle from main process */ + onToggleFullscreen(listener: () => void): () => void; + toggleFullscreen(): Promise; + togglePointerLock(): Promise; + getSettings(): Promise; + setSetting(key: K, value: Settings[K]): Promise; + resetSettings(): Promise; +} diff --git a/opennow-stable/src/shared/ipc.ts b/opennow-stable/src/shared/ipc.ts new file mode 100644 index 0000000..8a55824 --- /dev/null +++ b/opennow-stable/src/shared/ipc.ts @@ -0,0 +1,30 @@ +export const IPC_CHANNELS = { + AUTH_GET_SESSION: "auth:get-session", + AUTH_GET_PROVIDERS: "auth:get-providers", + AUTH_GET_REGIONS: "auth:get-regions", + AUTH_LOGIN: "auth:login", + AUTH_LOGOUT: "auth:logout", + SUBSCRIPTION_FETCH: "subscription:fetch", + GAMES_FETCH_MAIN: "games:fetch-main", + GAMES_FETCH_LIBRARY: "games:fetch-library", + GAMES_FETCH_PUBLIC: "games:fetch-public", + GAMES_RESOLVE_LAUNCH_ID: "games:resolve-launch-id", + CREATE_SESSION: "gfn:create-session", + POLL_SESSION: "gfn:poll-session", + STOP_SESSION: "gfn:stop-session", + GET_ACTIVE_SESSIONS: "gfn:get-active-sessions", + CLAIM_SESSION: "gfn:claim-session", + SESSION_CONFLICT_DIALOG: "gfn:session-conflict-dialog", + CONNECT_SIGNALING: "gfn:connect-signaling", + DISCONNECT_SIGNALING: "gfn:disconnect-signaling", + SEND_ANSWER: "gfn:send-answer", + SEND_ICE_CANDIDATE: "gfn:send-ice-candidate", + SIGNALING_EVENT: "gfn:signaling-event", + TOGGLE_FULLSCREEN: "window:toggle-fullscreen", + TOGGLE_POINTER_LOCK: "window:toggle-pointer-lock", + SETTINGS_GET: "settings:get", + SETTINGS_SET: "settings:set", + SETTINGS_RESET: "settings:reset", +} as const; + +export type IpcChannel = (typeof IPC_CHANNELS)[keyof typeof IPC_CHANNELS]; diff --git a/opennow-stable/tsconfig.json b/opennow-stable/tsconfig.json new file mode 100644 index 0000000..6eec53e --- /dev/null +++ b/opennow-stable/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "baseUrl": ".", + "paths": { + "@shared/*": [ + "src/shared/*" + ] + }, + "strict": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "types": [ + "vite/client" + ], + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "src/renderer/src", + "src/shared" + ] +} diff --git a/opennow-stable/tsconfig.node.json b/opennow-stable/tsconfig.node.json new file mode 100644 index 0000000..bbfc2a9 --- /dev/null +++ b/opennow-stable/tsconfig.node.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "baseUrl": ".", + "paths": { + "@shared/*": [ + "src/shared/*" + ] + }, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "types": [ + "node", + "electron-vite/node" + ], + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "electron.vite.config.ts", + "src/main", + "src/preload", + "src/shared" + ] +} diff --git a/opennow-streamer/Cargo.lock b/opennow-streamer/Cargo.lock deleted file mode 100644 index d4181a9..0000000 --- a/opennow-streamer/Cargo.lock +++ /dev/null @@ -1,7148 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - -[[package]] -name = "accesskit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.10.0", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "android-activity" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" -dependencies = [ - "android-properties", - "bitflags 2.10.0", - "cc", - "cesu8", - "jni", - "jni-sys", - "libc", - "log", - "ndk 0.9.0", - "ndk-context", - "ndk-sys 0.6.0+11769913", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.3", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - -[[package]] -name = "arc-swap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[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 = "ash-window" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" -dependencies = [ - "ash", - "raw-window-handle", - "raw-window-metal", -] - -[[package]] -name = "asn1-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" -dependencies = [ - "asn1-rs-derive 0.4.0", - "asn1-rs-impl 0.1.0", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 1.0.69", -] - -[[package]] -name = "asn1-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" -dependencies = [ - "asn1-rs-derive 0.5.1", - "asn1-rs-impl 0.2.0", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure 0.12.6", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", - "synstructure 0.13.2", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "async-compression" -version = "0.4.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atomic_refcell" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.112", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.10.0", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" -dependencies = [ - "bitflags 2.10.0", - "polling", - "rustix 1.1.3", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.3", - "rustix 1.1.3", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "ccm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" -dependencies = [ - "aead", - "cipher", - "ctr", - "subtle", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-expr" -version = "0.20.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21be0e1ce6cdb2ee7fff840f922fb04ead349e5cfb1e750b769132d44ce04720" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation 0.1.2", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types 0.5.0", - "libc", - "objc", -] - -[[package]] -name = "cocoa" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" -dependencies = [ - "bitflags 2.10.0", - "block", - "cocoa-foundation 0.2.1", - "core-foundation 0.10.1", - "core-graphics 0.24.0", - "foreign-types 0.5.0", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" -dependencies = [ - "bitflags 2.10.0", - "block", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "color" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compression-codecs" -version = "0.4.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" -dependencies = [ - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "libc", -] - -[[package]] -name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk 0.8.0", - "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.54.0", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" -dependencies = [ - "asn1-rs 0.5.2", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs 0.6.2", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ecolor" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" -dependencies = [ - "bytemuck", - "emath", -] - -[[package]] -name = "egui" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" -dependencies = [ - "accesskit", - "ahash", - "bitflags 2.10.0", - "emath", - "epaint", - "log", - "nohash-hasher", - "profiling", - "smallvec", - "unicode-segmentation", -] - -[[package]] -name = "egui-wgpu" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "epaint", - "log", - "profiling", - "thiserror 2.0.17", - "type-map", - "web-time", - "wgpu", -] - -[[package]] -name = "egui-winit" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" -dependencies = [ - "arboard", - "bytemuck", - "egui", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "profiling", - "raw-window-handle", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "emath" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "epaint" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" -dependencies = [ - "ahash", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot", - "profiling", - "self_cell", - "skrifa", - "vello_cpu", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.33.3" -source = "git+https://github.com/zortos293/egui?branch=wgpu-28#10d32ba834ad158a1f0d0d55762da0fc78c2eb56" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - -[[package]] -name = "euclid" -version = "0.22.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" -dependencies = [ - "num-traits", -] - -[[package]] -name = "evdev" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6055a93a963297befb0f4f6e18f314aec9767a4bbe88b151126df2433610a7" -dependencies = [ - "bitvec", - "cfg-if", - "libc", - "nix 0.23.2", - "thiserror 1.0.69", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fearless_simd" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "ffmpeg-next" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40" -dependencies = [ - "bitflags 2.10.0", - "ffmpeg-sys-next", - "libc", -] - -[[package]] -name = "ffmpeg-sys-next" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bca20aa4ee774fe384c2490096c122b0b23cf524a9910add0686691003d797b" -dependencies = [ - "bindgen", - "cc", - "libc", - "num_cpus", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "font-types" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "g29" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb2c86bc924b0adf8dd32db37e0b3cb57714adcad5c584c4fe9734ac8f5a1cd" -dependencies = [ - "hidapi", -] - -[[package]] -name = "generator" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result 0.4.1", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.3", - "windows-link", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gilrs" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb2c998745a3c1ac90f64f4f7b3a54219fd3612d7705e7798212935641ed18f" -dependencies = [ - "fnv", - "gilrs-core", - "log", - "uuid", - "vec_map", -] - -[[package]] -name = "gilrs-core" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be11a71ac3564f6965839e2ed275bf4fcf5ce16d80d396e1dfdb7b2d80bd587e" -dependencies = [ - "core-foundation 0.10.1", - "inotify", - "io-kit-sys", - "js-sys", - "libc", - "libudev-sys", - "log", - "nix 0.30.1", - "uuid", - "vec_map", - "wasm-bindgen", - "web-sys", - "windows 0.62.2", -] - -[[package]] -name = "gio-sys" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys 0.59.0", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glib" -version = "0.20.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" -dependencies = [ - "bitflags 2.10.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", -] - -[[package]] -name = "glib-macros" -version = "0.20.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "glib-sys" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "glow" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gobject-sys" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gpu-allocator" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" -dependencies = [ - "ash", - "hashbrown 0.16.1", - "log", - "presser", - "thiserror 2.0.17", - "windows 0.62.2", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.10.0", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "gstreamer" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8757a87f3706560037a01a9f06a59fcc7bdb0864744dcf73546606e60c4316e1" -dependencies = [ - "cfg-if", - "futures-channel", - "futures-core", - "futures-util", - "glib", - "gstreamer-sys", - "itertools 0.14.0", - "libc", - "muldiv", - "num-integer", - "num-rational", - "once_cell", - "option-operations", - "paste", - "pin-project-lite", - "smallvec", - "thiserror 2.0.17", -] - -[[package]] -name = "gstreamer-app" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9a883eb21aebcf1289158225c05f7aea5da6ecf71fa7f0ff1ce4d25baf004e" -dependencies = [ - "futures-core", - "futures-sink", - "glib", - "gstreamer", - "gstreamer-app-sys", - "gstreamer-base", - "libc", -] - -[[package]] -name = "gstreamer-app-sys" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f7ef838306fe51852d503a14dc79ac42de005a59008a05098de3ecdaf05455" -dependencies = [ - "glib-sys", - "gstreamer-base-sys", - "gstreamer-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-base" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f19a74fd04ffdcb847dd322640f2cf520897129d00a7bcb92fd62a63f3e27404" -dependencies = [ - "atomic_refcell", - "cfg-if", - "glib", - "gstreamer", - "gstreamer-base-sys", - "libc", -] - -[[package]] -name = "gstreamer-base-sys" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f2fb0037b6d3c5b51f60dea11e667910f33be222308ca5a101450018a09840" -dependencies = [ - "glib-sys", - "gobject-sys", - "gstreamer-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-sys" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feea73b4d92dbf9c24a203c9cd0bcc740d584f6b5960d5faf359febf288919b2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-video" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1318b599d77ca4f7702ecbdeac1672d6304cb16b7e5752fabb3ee8260449a666" -dependencies = [ - "cfg-if", - "futures-channel", - "glib", - "gstreamer", - "gstreamer-base", - "gstreamer-video-sys", - "libc", - "once_cell", - "thiserror 2.0.17", -] - -[[package]] -name = "gstreamer-video-sys" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a70f0947f12d253b9de9bc3fd92f981e4d025336c18389c7f08cdf388a99f5c" -dependencies = [ - "glib-sys", - "gobject-sys", - "gstreamer-base-sys", - "gstreamer-sys", - "libc", - "system-deps", -] - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "hidapi" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565dd4c730b8f8b2c0fb36df6be12e5470ae10895ddcc4e9dcfbfb495de202b0" -dependencies = [ - "cc", - "cfg-if", - "libc", - "pkg-config", - "windows-sys 0.48.0", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" -dependencies = [ - "bytemuck", - "byteorder-lite", - "image-webp", - "moxcms", - "num-traits", - "png", - "tiff", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.10.0", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "interceptor" -version = "0.12.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "async-trait", - "bytes", - "log", - "portable-atomic", - "rand 0.8.5", - "rtcp", - "rtp", - "thiserror 1.0.69", - "tokio", - "waitgroup", - "webrtc-srtp", - "webrtc-util", -] - -[[package]] -name = "io-kit-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" -dependencies = [ - "core-foundation-sys", - "mach2", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jiff" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "kurbo" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.179" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" - -[[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 = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.7.0", -] - -[[package]] -name = "libudev-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "linebender_resource_handle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.10.0", - "block", - "core-graphics-types 0.2.0", - "foreign-types 0.5.0", - "log", - "objc", - "paste", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "moxcms" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "muldiv" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" - -[[package]] -name = "naga" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.16.1", - "hexf-parse", - "indexmap", - "libm", - "log", - "num-traits", - "once_cell", - "rustc-hash 1.1.0", - "spirv", - "thiserror 2.0.17", - "unicode-ident", -] - -[[package]] -name = "nasm-rs" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f676553b60ccbb76f41f9ae8f2428dac3f259ff8f1c2468a174778d06a1af9" -dependencies = [ - "log", -] - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.1.6", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys 0.6.0+11769913", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", - "pin-utils", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation 0.2.2", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-graphics", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2 0.6.3", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] - -[[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.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.10.0", - "block2", - "dispatch", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - -[[package]] -name = "oid-registry" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" -dependencies = [ - "asn1-rs 0.6.2", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "openh264" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1af3a4d35290ba7a46d1ce69cb13ae740a2d72cc2ee00abee3c84bed3dbe5d" -dependencies = [ - "openh264-sys2", - "wide", -] - -[[package]] -name = "openh264-sys2" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a77c1e18503537113d77b1b1d05274e81fa9f44843c06be2d735adb19f7c9d" -dependencies = [ - "cc", - "nasm-rs", - "walkdir", -] - -[[package]] -name = "opennow-streamer" -version = "0.1.0" -dependencies = [ - "anyhow", - "arboard", - "ash", - "ash-window", - "base64 0.22.1", - "block", - "bytemuck", - "bytes", - "chrono", - "cocoa 0.26.1", - "core-foundation 0.10.1", - "core-graphics 0.24.0", - "cpal", - "dirs", - "egui", - "egui-wgpu", - "egui-winit", - "env_logger", - "evdev", - "ffmpeg-next", - "foreign-types 0.5.0", - "futures-util", - "g29", - "gilrs", - "gstreamer", - "gstreamer-app", - "gstreamer-video", - "hex", - "http", - "image", - "lazy_static", - "libc", - "libloading", - "log", - "metal", - "native-tls", - "objc", - "once_cell", - "open", - "openh264", - "parking_lot", - "pollster", - "rand 0.8.5", - "regex", - "reqwest", - "serde", - "serde_json", - "sha2", - "thiserror 2.0.17", - "tokio", - "tokio-native-tls", - "tokio-tungstenite", - "tracing", - "tracing-log", - "tracing-subscriber", - "tracing-tracy", - "urlencoding", - "uuid", - "webrtc", - "webrtc-util", - "wgpu", - "wgpu-hal", - "windows 0.62.2", - "windows-numerics", - "winit", - "x11", -] - -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-probe" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "option-operations" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" -dependencies = [ - "paste", -] - -[[package]] -name = "orbclient" -version = "0.3.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "ordered-float" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" -dependencies = [ - "num-traits", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "peniko" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c76095c9a636173600478e0373218c7b955335048c2bcd12dc6a79657649d8" -dependencies = [ - "bytemuck", - "color", - "kurbo", - "linebender_resource_handle", - "smallvec", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.10.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" - -[[package]] -name = "pxfm" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.1", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "range-alloc" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "raw-window-metal" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" -dependencies = [ - "cocoa 0.25.0", - "core-graphics 0.23.2", - "objc", - "raw-window-handle", -] - -[[package]] -name = "rcgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "x509-parser", - "yasna", -] - -[[package]] -name = "read-fonts" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" -dependencies = [ - "bytemuck", - "font-types", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rtcp" -version = "0.11.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "bytes", - "thiserror 1.0.69", - "webrtc-util", -] - -[[package]] -name = "rtp" -version = "0.11.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "bytes", - "portable-atomic", - "rand 0.8.5", - "serde", - "thiserror 1.0.69", - "webrtc-util", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe 0.2.0", - "rustls-pki-types", - "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit 0.19.2", - "tiny-skia", -] - -[[package]] -name = "sdp" -version = "0.6.2" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "rand 0.8.5", - "substring", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "serde_json" -version = "1.0.148" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.10.0", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.3", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.3", - "thiserror 2.0.17", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" -dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - -[[package]] -name = "stun" -version = "0.6.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "base64 0.21.7", - "crc", - "lazy_static", - "md-5", - "rand 0.8.5", - "ring", - "subtle", - "thiserror 1.0.69", - "tokio", - "url", - "webrtc-util", -] - -[[package]] -name = "substring" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" -dependencies = [ - "autocfg", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.112" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "system-deps" -version = "7.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg 0.4.21", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.9.10+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "async-compression", - "bitflags 2.10.0", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "iri-string", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "tracing-tracy" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eaa1852afa96e0fe9e44caa53dc0bd2d9d05e0f2611ce09f97f8677af56e4ba" -dependencies = [ - "tracing-core", - "tracing-subscriber", - "tracy-client", -] - -[[package]] -name = "tracy-client" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f6fc3baeac5d86ab90c772e9e30620fc653bf1864295029921a15ef478e6a5" -dependencies = [ - "loom", - "once_cell", - "tracy-client-sys", -] - -[[package]] -name = "tracy-client-sys" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" -dependencies = [ - "cc", - "windows-targets 0.52.6", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - -[[package]] -name = "tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.8.5", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 1.0.69", - "utf-8", -] - -[[package]] -name = "turn" -version = "0.8.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "async-trait", - "base64 0.21.7", - "futures", - "log", - "md-5", - "portable-atomic", - "rand 0.8.5", - "ring", - "stun", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "webrtc-util", -] - -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.1", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "vello_common" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a235ba928b3109ad9e7696270edb09445a52ae1c7c08e6d31a19b1cdd6cbc24a" -dependencies = [ - "bytemuck", - "fearless_simd", - "log", - "peniko", - "skrifa", - "smallvec", -] - -[[package]] -name = "vello_cpu" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0bd1fcf9c1814f17a491e07113623d44e3ec1125a9f3401f5e047d6d326da21" -dependencies = [ - "bytemuck", - "vello_common", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "waitgroup" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" -dependencies = [ - "atomic-waker", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.112", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wayland-backend" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.3", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" -dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.3", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.10.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" -dependencies = [ - "rustix 1.1.3", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webbrowser" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" -dependencies = [ - "core-foundation 0.10.1", - "jni", - "log", - "ndk-context", - "objc2 0.6.3", - "objc2-foundation 0.3.2", - "url", - "web-sys", -] - -[[package]] -name = "webpki-roots" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webrtc" -version = "0.11.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "arc-swap", - "async-trait", - "bytes", - "cfg-if", - "hex", - "interceptor", - "lazy_static", - "log", - "portable-atomic", - "rand 0.8.5", - "rcgen", - "regex", - "ring", - "rtcp", - "rtp", - "rustls", - "sdp", - "serde", - "serde_json", - "sha2", - "smol_str", - "stun", - "thiserror 1.0.69", - "time", - "tokio", - "turn", - "url", - "waitgroup", - "webrtc-data", - "webrtc-dtls", - "webrtc-ice", - "webrtc-mdns", - "webrtc-media", - "webrtc-sctp", - "webrtc-srtp", - "webrtc-util", -] - -[[package]] -name = "webrtc-data" -version = "0.9.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "bytes", - "log", - "portable-atomic", - "thiserror 1.0.69", - "tokio", - "webrtc-sctp", - "webrtc-util", -] - -[[package]] -name = "webrtc-dtls" -version = "0.10.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "aes", - "aes-gcm", - "async-trait", - "bincode", - "byteorder", - "cbc", - "ccm", - "der-parser 8.2.0", - "hkdf", - "hmac", - "log", - "p256", - "p384", - "portable-atomic", - "rand 0.8.5", - "rand_core 0.6.4", - "rcgen", - "ring", - "rustls", - "sec1", - "serde", - "sha1", - "sha2", - "subtle", - "thiserror 1.0.69", - "tokio", - "webrtc-util", - "x25519-dalek", - "x509-parser", -] - -[[package]] -name = "webrtc-ice" -version = "0.11.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "arc-swap", - "async-trait", - "crc", - "log", - "portable-atomic", - "rand 0.8.5", - "serde", - "serde_json", - "stun", - "thiserror 1.0.69", - "tokio", - "turn", - "url", - "uuid", - "waitgroup", - "webrtc-mdns", - "webrtc-util", -] - -[[package]] -name = "webrtc-mdns" -version = "0.7.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "log", - "socket2 0.5.10", - "thiserror 1.0.69", - "tokio", - "webrtc-util", -] - -[[package]] -name = "webrtc-media" -version = "0.8.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "byteorder", - "bytes", - "rand 0.8.5", - "rtp", - "thiserror 1.0.69", -] - -[[package]] -name = "webrtc-sctp" -version = "0.10.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "arc-swap", - "async-trait", - "bytes", - "crc", - "log", - "portable-atomic", - "rand 0.8.5", - "thiserror 1.0.69", - "tokio", - "webrtc-util", -] - -[[package]] -name = "webrtc-srtp" -version = "0.13.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "aead", - "aes", - "aes-gcm", - "byteorder", - "bytes", - "ctr", - "hmac", - "log", - "rtcp", - "rtp", - "sha1", - "subtle", - "thiserror 1.0.69", - "tokio", - "webrtc-util", -] - -[[package]] -name = "webrtc-util" -version = "0.9.0" -source = "git+https://github.com/zortos293/webrtc-rs-gfn?branch=gfn-ssrc-fix#822de5f07547ee9e103492012ae45681faa60bd4" -dependencies = [ - "async-trait", - "bitflags 1.3.2", - "bytes", - "ipnet", - "lazy_static", - "libc", - "log", - "nix 0.26.4", - "portable-atomic", - "rand 0.8.5", - "thiserror 1.0.69", - "tokio", - "winapi", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "wgpu" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "arrayvec", - "bitflags 2.10.0", - "bytemuck", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "js-sys", - "log", - "naga", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.10.0", - "bytemuck", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.17", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-apple" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.10.0", - "block", - "bytemuck", - "cfg-if", - "cfg_aliases", - "core-graphics-types 0.2.0", - "glow", - "glutin_wgl_sys", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys 0.6.0+11769913", - "objc", - "once_cell", - "ordered-float", - "parking_lot", - "portable-atomic", - "portable-atomic-util", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.17", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows 0.62.2", - "windows-core 0.62.2", -] - -[[package]] -name = "wgpu-types" -version = "28.0.0" -source = "git+https://github.com/gfx-rs/wgpu?tag=v28.0.0#3f02781bb5a0a1fe1922ea36c9bdacf9792abcbc" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "js-sys", - "log", - "web-sys", -] - -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core 0.62.2", - "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 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result 0.4.1", - "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 0.62.2", - "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 2.0.112", -] - -[[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 2.0.112", -] - -[[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 0.62.2", - "windows-link", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.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-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winit" -version = "0.30.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" -dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.10.0", - "block2", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk 0.9.0", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit 0.19.2", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.3", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs 0.6.2", - "data-encoding", - "der-parser 9.0.0", - "lazy_static", - "nom", - "oid-registry", - "ring", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - -[[package]] -name = "xcursor" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.10.0", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", - "synstructure 0.13.2", -] - -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", - "synstructure 0.13.2", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "zmij" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-core" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" - -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" -dependencies = [ - "zune-core 0.5.0", -] diff --git a/opennow-streamer/src/webrtc/signaling.rs b/opennow-streamer/src/webrtc/signaling.rs deleted file mode 100644 index 7d90fab..0000000 --- a/opennow-streamer/src/webrtc/signaling.rs +++ /dev/null @@ -1,363 +0,0 @@ -//! GFN WebSocket Signaling Protocol -//! -//! WebSocket-based signaling for WebRTC connection setup. - -use std::sync::Arc; -use tokio::sync::{mpsc, Mutex}; -use tokio_tungstenite::tungstenite::Message; -use futures_util::{StreamExt, SinkExt}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use anyhow::{Result, Context}; -use log::{info, debug, warn, error}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; - -/// Generate WebSocket key for handshake -fn generate_ws_key() -> String { - let random_bytes: [u8; 16] = rand::random(); - STANDARD.encode(random_bytes) -} - -/// Peer info sent to server -#[derive(Debug, Serialize, Deserialize)] -pub struct PeerInfo { - pub browser: String, - #[serde(rename = "browserVersion")] - pub browser_version: String, - pub connected: bool, - pub id: u32, - pub name: String, - pub peer_role: u32, - pub resolution: String, - pub version: u32, -} - -/// Message from signaling server -#[derive(Debug, Deserialize)] -pub struct SignalingMessage { - pub ackid: Option, - pub ack: Option, - pub hb: Option, - pub peer_info: Option, - pub peer_msg: Option, -} - -/// Peer-to-peer message wrapper -#[derive(Debug, Deserialize)] -pub struct PeerMessage { - pub from: u32, - pub to: u32, - pub msg: String, -} - -/// ICE candidate message -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IceCandidate { - pub candidate: String, - #[serde(rename = "sdpMid")] - pub sdp_mid: Option, - #[serde(rename = "sdpMLineIndex")] - pub sdp_mline_index: Option, -} - -/// Events emitted by the signaling client -#[derive(Debug)] -pub enum SignalingEvent { - Connected, - SdpOffer(String), - IceCandidate(IceCandidate), - Disconnected(String), - Error(String), -} - -/// GFN Signaling Client -pub struct GfnSignaling { - server_ip: String, - session_id: String, - peer_id: u32, - peer_name: String, - ack_counter: Arc>, - event_tx: mpsc::Sender, - message_tx: Option>, -} - -impl GfnSignaling { - pub fn new( - server_ip: String, - session_id: String, - event_tx: mpsc::Sender, - ) -> Self { - let peer_id = 2; // Client is always peer 2 - let random_suffix: u64 = rand::random::() % 10_000_000_000; - let peer_name = format!("peer-{}", random_suffix); - - Self { - server_ip, - session_id, - peer_id, - peer_name, - ack_counter: Arc::new(Mutex::new(0)), - event_tx, - message_tx: None, - } - } - - /// Connect to the signaling server - pub async fn connect(&mut self) -> Result<()> { - let url = format!( - "wss://{}/nvst/sign_in?peer_id={}&version=2", - self.server_ip, self.peer_name - ); - let subprotocol = format!("x-nv-sessionid.{}", self.session_id); - - info!("Connecting to signaling: {}", url); - info!("Using subprotocol: {}", subprotocol); - - // Use TLS connector that accepts self-signed certs - let tls_connector = native_tls::TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build() - .context("Failed to build TLS connector")?; - - // Connect TCP first - let host = self.server_ip.split(':').next().unwrap_or(&self.server_ip); - let port = 443; - let addr = format!("{}:{}", host, port); - - info!("Connecting TCP to: {}", addr); - let tcp_stream = tokio::net::TcpStream::connect(&addr).await - .context("TCP connection failed")?; - - info!("TCP connected, starting TLS handshake..."); - let tls_stream = tokio_native_tls::TlsConnector::from(tls_connector) - .connect(host, tcp_stream) - .await - .context("TLS handshake failed")?; - - info!("TLS connected, starting WebSocket handshake..."); - - let ws_key = generate_ws_key(); - - let request = http::Request::builder() - .uri(&url) - .header("Host", &self.server_ip) - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .header("Sec-WebSocket-Version", "13") - .header("Sec-WebSocket-Key", &ws_key) - .header("Sec-WebSocket-Protocol", &subprotocol) - .header("Origin", "https://play.geforcenow.com") - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0") - .body(()) - .context("Failed to build request")?; - - let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig { - max_message_size: Some(64 << 20), - max_frame_size: Some(16 << 20), - accept_unmasked_frames: false, - ..Default::default() - }; - - let (ws_stream, response) = tokio_tungstenite::client_async_with_config( - request, - tls_stream, - Some(ws_config), - ) - .await - .map_err(|e| { - error!("WebSocket handshake error: {:?}", e); - anyhow::anyhow!("WebSocket handshake failed: {}", e) - })?; - - info!("Connected! Response: {:?}", response.status()); - - let (mut write, mut read) = ws_stream.split(); - - // Channel for sending messages - let (msg_tx, mut msg_rx) = mpsc::channel::(64); - self.message_tx = Some(msg_tx.clone()); - - // Send initial peer_info - let peer_info = self.create_peer_info(); - let peer_info_msg = json!({ - "ackid": self.next_ack_id().await, - "peer_info": peer_info - }); - write.send(Message::Text(peer_info_msg.to_string())).await?; - info!("Sent peer_info"); - - let event_tx = self.event_tx.clone(); - let peer_id = self.peer_id; - - // Spawn message sender task - tokio::spawn(async move { - while let Some(msg) = msg_rx.recv().await { - if let Err(e) = write.send(msg).await { - error!("Failed to send message: {}", e); - break; - } - } - }); - - // Spawn message receiver task - let msg_tx_clone = msg_tx.clone(); - let event_tx_clone = event_tx.clone(); - tokio::spawn(async move { - while let Some(msg_result) = read.next().await { - match msg_result { - Ok(Message::Text(text)) => { - info!("Received: {}", &text[..text.len().min(1000)]); - - if let Ok(msg) = serde_json::from_str::(&text) { - // Send ACK for messages with ackid - if let Some(ackid) = msg.ackid { - if msg.peer_info.as_ref().map(|p| p.id) != Some(peer_id) { - let ack = json!({ "ack": ackid }); - let _ = msg_tx_clone.send(Message::Text(ack.to_string())).await; - } - } - - // Handle heartbeat - if msg.hb.is_some() { - let hb = json!({ "hb": 1 }); - let _ = msg_tx_clone.send(Message::Text(hb.to_string())).await; - continue; - } - - // Handle peer messages - if let Some(peer_msg) = msg.peer_msg { - if let Ok(inner) = serde_json::from_str::(&peer_msg.msg) { - // SDP Offer - if inner.get("type").and_then(|t| t.as_str()) == Some("offer") { - if let Some(sdp) = inner.get("sdp").and_then(|s| s.as_str()) { - info!("Received SDP offer, length: {}", sdp.len()); - // Log full SDP for debugging (color space info, codec params) - for line in sdp.lines() { - debug!("SDP: {}", line); - } - let _ = event_tx_clone.send(SignalingEvent::SdpOffer(sdp.to_string())).await; - } - } - // ICE Candidate - else if inner.get("candidate").is_some() { - if let Ok(candidate) = serde_json::from_value::(inner) { - info!("Received ICE candidate: {}", candidate.candidate); - let _ = event_tx_clone.send(SignalingEvent::IceCandidate(candidate)).await; - } - } - } - } - } - } - Ok(Message::Close(frame)) => { - warn!("WebSocket closed: {:?}", frame); - let _ = event_tx_clone.send(SignalingEvent::Disconnected( - frame.map(|f| f.reason.to_string()).unwrap_or_default() - )).await; - break; - } - Err(e) => { - error!("WebSocket error: {}", e); - let _ = event_tx_clone.send(SignalingEvent::Error(e.to_string())).await; - break; - } - _ => {} - } - } - }); - - // Notify connected - self.event_tx.send(SignalingEvent::Connected).await?; - - // Start heartbeat task - let hb_tx = msg_tx.clone(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(5)); - loop { - interval.tick().await; - let hb = json!({ "hb": 1 }); - if hb_tx.send(Message::Text(hb.to_string())).await.is_err() { - break; - } - } - }); - - Ok(()) - } - - /// Send SDP answer to server - pub async fn send_answer(&self, sdp: &str, nvst_sdp: Option<&str>) -> Result<()> { - let msg_tx = self.message_tx.as_ref().context("Not connected")?; - - let mut answer = json!({ - "type": "answer", - "sdp": sdp - }); - - if let Some(nvst) = nvst_sdp { - // Try to parse as JSON object (for nvstSdp wrapper), otherwise treat as string - if let Ok(val) = serde_json::from_str::(nvst) { - answer["nvstSdp"] = val; - } else { - answer["nvstSdp"] = json!(nvst); - } - } - - let peer_msg = json!({ - "peer_msg": { - "from": self.peer_id, - "to": 1, - "msg": answer.to_string() - }, - "ackid": self.next_ack_id().await - }); - - msg_tx.send(Message::Text(peer_msg.to_string())).await?; - info!("Sent SDP answer"); - Ok(()) - } - - /// Send ICE candidate to server - pub async fn send_ice_candidate(&self, candidate: &str, sdp_mid: Option<&str>, sdp_mline_index: Option) -> Result<()> { - let msg_tx = self.message_tx.as_ref().context("Not connected")?; - - let ice = json!({ - "candidate": candidate, - "sdpMid": sdp_mid, - "sdpMLineIndex": sdp_mline_index - }); - - let peer_msg = json!({ - "peer_msg": { - "from": self.peer_id, - "to": 1, - "msg": ice.to_string() - }, - "ackid": self.next_ack_id().await - }); - - msg_tx.send(Message::Text(peer_msg.to_string())).await?; - info!("Sent ICE candidate: {}", candidate); - Ok(()) - } - - fn create_peer_info(&self) -> PeerInfo { - PeerInfo { - browser: "Chrome".to_string(), - browser_version: "131".to_string(), - connected: true, - id: self.peer_id, - name: self.peer_name.clone(), - peer_role: 0, - resolution: "1920x1080".to_string(), - version: 2, - } - } - - async fn next_ack_id(&self) -> u32 { - let mut counter = self.ack_counter.lock().await; - *counter += 1; - *counter - } -} diff --git a/package.json b/package.json index ac74280..ef11008 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,12 @@ { - "name": "opennow-streamer", - "version": "0.1.0", - "description": "OpenNOW - Native GeForce NOW streaming client built from the ground up", + "name": "opennow-workspace", + "private": true, + "description": "OpenNOW Electron workspace", "scripts": { - "build": "cd opennow-streamer && cargo build --release", - "dev": "cd opennow-streamer && cargo run", - "test": "cd opennow-streamer && cargo test" - }, - "repository": { - "type": "git", - "url": "https://github.com/zortos293/GFNClient.git" - }, - "author": "zortos293", - "license": "MIT" + "dev": "npm --prefix opennow-stable run dev", + "build": "npm --prefix opennow-stable run build", + "typecheck": "npm --prefix opennow-stable run typecheck", + "dist": "npm --prefix opennow-stable run dist", + "dist:signed": "npm --prefix opennow-stable run dist:signed" + } } diff --git a/tmp.js b/tmp.js new file mode 100644 index 0000000..2cdbe81 --- /dev/null +++ b/tmp.js @@ -0,0 +1 @@ +console.log('x')