tangled
alpha
login
or
join now
mmatt.net
/
browser-stream
0
fork
atom
this repo has no description
0
fork
atom
overview
issues
pulls
pipelines
init
mmatt.net
3 weeks ago
df928ba5
+4655
27 changed files
expand all
collapse all
unified
split
.dockerignore
.env.example
.github
workflows
build-and-release.yml
docker-publish.yml
.gitignore
Cargo.lock
Cargo.toml
Dockerfile
README.md
docker
entrypoint.sh
docker-compose.yml
scripts
fetch-sidecars.ps1
fetch-sidecars.sh
src
chromium.rs
cli.rs
encoder.rs
error.rs
frame.rs
lib.rs
main.rs
retry.rs
rtmp.rs
tests
cli.rs
ffmpeg_cmd.rs
frame.rs
retry.rs
rtmp.rs
+10
.dockerignore
···
0
0
0
0
0
0
0
0
0
0
···
1
+
.git
2
+
.github
3
+
target
4
+
docker-compose*.yml
5
+
*.log
6
+
.env
7
+
.env.*
8
+
README.md
9
+
scripts
10
+
tests
+21
.env.example
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
WEBSITE_URL=https://example.com
2
+
3
+
# Option A: full output URL
4
+
OUTPUT=rtmp://live.example.com/app/mystream
5
+
6
+
# Option B: split URL + key (used when OUTPUT is empty)
7
+
RTMP_URL=rtmp://live.example.com/app
8
+
STREAM_KEY=mystream
9
+
10
+
WIDTH=1920
11
+
HEIGHT=1080
12
+
FPS=30
13
+
BITRATE_KBPS=4500
14
+
KEYINT_SEC=1
15
+
X264_OPTS=bframes=0
16
+
RETRIES=5
17
+
RETRY_BACKOFF_MS=1000
18
+
STARTUP_DELAY_MS=2000
19
+
FRAME_TIMEOUT_MS=30000
20
+
NO_AUDIO=0
21
+
VERBOSE=0
+109
.github/workflows/build-and-release.yml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
name: Build and Release
2
+
3
+
on:
4
+
pull_request:
5
+
push:
6
+
branches:
7
+
- main
8
+
tags:
9
+
- "v*"
10
+
workflow_dispatch:
11
+
12
+
permissions:
13
+
contents: write
14
+
15
+
jobs:
16
+
build:
17
+
name: ${{ matrix.name }}
18
+
runs-on: ${{ matrix.os }}
19
+
strategy:
20
+
fail-fast: false
21
+
matrix:
22
+
include:
23
+
- name: macOS arm64
24
+
os: macos-14
25
+
target: aarch64-apple-darwin
26
+
artifact_id: macos-arm64
27
+
archive_ext: tar.gz
28
+
bin_name: browser-stream
29
+
- name: Linux x86_64
30
+
os: ubuntu-24.04
31
+
target: x86_64-unknown-linux-gnu
32
+
artifact_id: linux-x86_64
33
+
archive_ext: tar.gz
34
+
bin_name: browser-stream
35
+
- name: Windows x86_64
36
+
os: windows-2022
37
+
target: x86_64-pc-windows-msvc
38
+
artifact_id: windows-x86_64
39
+
archive_ext: zip
40
+
bin_name: browser-stream.exe
41
+
42
+
steps:
43
+
- name: Checkout
44
+
uses: actions/checkout@v4
45
+
46
+
- name: Setup Rust
47
+
uses: dtolnay/rust-toolchain@stable
48
+
with:
49
+
targets: ${{ matrix.target }}
50
+
51
+
- name: Build release binary
52
+
run: cargo build --release --target ${{ matrix.target }}
53
+
54
+
- name: Install sidecar tooling (Linux)
55
+
if: runner.os == 'Linux'
56
+
run: |
57
+
sudo apt-get update
58
+
sudo apt-get install -y jq unzip
59
+
60
+
- name: Install sidecar tooling (macOS)
61
+
if: runner.os == 'macOS'
62
+
run: brew install jq unzip
63
+
64
+
- name: Fetch sidecars (macOS/Linux)
65
+
if: runner.os != 'Windows'
66
+
run: ./scripts/fetch-sidecars.sh --destination dist/sidecar
67
+
68
+
- name: Fetch sidecars (Windows)
69
+
if: runner.os == 'Windows'
70
+
shell: pwsh
71
+
run: ./scripts/fetch-sidecars.ps1 -Destination dist/sidecar
72
+
73
+
- name: Package archive (macOS/Linux)
74
+
if: runner.os != 'Windows'
75
+
run: |
76
+
set -euo pipefail
77
+
ARCHIVE_ROOT="browser-stream-${{ matrix.artifact_id }}"
78
+
PACKAGE_ROOT="dist/${ARCHIVE_ROOT}"
79
+
80
+
mkdir -p "$PACKAGE_ROOT/bin" "$PACKAGE_ROOT/sidecar"
81
+
cp "target/${{ matrix.target }}/release/${{ matrix.bin_name }}" "$PACKAGE_ROOT/bin/${{ matrix.bin_name }}"
82
+
cp -R dist/sidecar/. "$PACKAGE_ROOT/sidecar/"
83
+
84
+
tar -czf "dist/${ARCHIVE_ROOT}.tar.gz" -C dist "${ARCHIVE_ROOT}"
85
+
86
+
- name: Package archive (Windows)
87
+
if: runner.os == 'Windows'
88
+
shell: pwsh
89
+
run: |
90
+
$archiveRoot = "browser-stream-${{ matrix.artifact_id }}"
91
+
$packageRoot = "dist/$archiveRoot"
92
+
93
+
New-Item -ItemType Directory -Force -Path "$packageRoot/bin" | Out-Null
94
+
Copy-Item "target/${{ matrix.target }}/release/${{ matrix.bin_name }}" "$packageRoot/bin/${{ matrix.bin_name }}" -Force
95
+
Copy-Item "dist/sidecar" "$packageRoot/sidecar" -Recurse -Force
96
+
97
+
Compress-Archive -Path $packageRoot -DestinationPath "dist/$archiveRoot.zip" -Force
98
+
99
+
- name: Upload workflow artifact
100
+
uses: actions/upload-artifact@v4
101
+
with:
102
+
name: browser-stream-${{ matrix.artifact_id }}
103
+
path: dist/browser-stream-${{ matrix.artifact_id }}.${{ matrix.archive_ext }}
104
+
105
+
- name: Publish release asset
106
+
if: startsWith(github.ref, 'refs/tags/')
107
+
uses: softprops/action-gh-release@v2
108
+
with:
109
+
files: dist/browser-stream-${{ matrix.artifact_id }}.${{ matrix.archive_ext }}
+71
.github/workflows/docker-publish.yml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
name: Docker Publish
2
+
3
+
on:
4
+
pull_request:
5
+
push:
6
+
branches:
7
+
- main
8
+
tags:
9
+
- "v*"
10
+
workflow_dispatch:
11
+
12
+
permissions:
13
+
contents: read
14
+
packages: write
15
+
16
+
jobs:
17
+
docker:
18
+
name: Build and Publish GHCR Image (${{ matrix.variant }})
19
+
runs-on: ubuntu-latest
20
+
strategy:
21
+
fail-fast: false
22
+
matrix:
23
+
include:
24
+
- variant: slim
25
+
target: slim
26
+
platforms: linux/amd64,linux/arm64
27
+
- variant: full
28
+
target: full
29
+
platforms: linux/amd64
30
+
31
+
steps:
32
+
- name: Checkout
33
+
uses: actions/checkout@v4
34
+
35
+
- name: Set up QEMU
36
+
uses: docker/setup-qemu-action@v3
37
+
38
+
- name: Set up Docker Buildx
39
+
uses: docker/setup-buildx-action@v3
40
+
41
+
- name: Log in to GHCR
42
+
if: github.event_name != 'pull_request'
43
+
uses: docker/login-action@v3
44
+
with:
45
+
registry: ghcr.io
46
+
username: ${{ github.actor }}
47
+
password: ${{ secrets.GITHUB_TOKEN }}
48
+
49
+
- name: Generate Docker metadata
50
+
id: meta
51
+
uses: docker/metadata-action@v5
52
+
with:
53
+
images: ghcr.io/${{ github.repository }}
54
+
tags: |
55
+
type=ref,event=branch,suffix=-${{ matrix.variant }}
56
+
type=ref,event=tag,suffix=-${{ matrix.variant }}
57
+
type=sha,prefix=sha-,suffix=-${{ matrix.variant }}
58
+
type=raw,value=latest-${{ matrix.variant }},enable={{is_default_branch}}
59
+
60
+
- name: Build and push
61
+
uses: docker/build-push-action@v6
62
+
with:
63
+
context: .
64
+
file: Dockerfile
65
+
target: ${{ matrix.target }}
66
+
platforms: ${{ matrix.platforms }}
67
+
push: ${{ github.event_name != 'pull_request' }}
68
+
tags: ${{ steps.meta.outputs.tags }}
69
+
labels: ${{ steps.meta.outputs.labels }}
70
+
cache-from: type=gha
71
+
cache-to: type=gha,mode=max
+1
.gitignore
···
0
···
1
+
/target
+2522
Cargo.lock
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
# This file is automatically @generated by Cargo.
2
+
# It is not intended for manual editing.
3
+
version = 4
4
+
5
+
[[package]]
6
+
name = "adler2"
7
+
version = "2.0.1"
8
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9
+
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
+
11
+
[[package]]
12
+
name = "aho-corasick"
13
+
version = "1.1.4"
14
+
source = "registry+https://github.com/rust-lang/crates.io-index"
15
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
16
+
dependencies = [
17
+
"memchr",
18
+
]
19
+
20
+
[[package]]
21
+
name = "aligned"
22
+
version = "0.4.3"
23
+
source = "registry+https://github.com/rust-lang/crates.io-index"
24
+
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
25
+
dependencies = [
26
+
"as-slice",
27
+
]
28
+
29
+
[[package]]
30
+
name = "aligned-vec"
31
+
version = "0.6.4"
32
+
source = "registry+https://github.com/rust-lang/crates.io-index"
33
+
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
34
+
dependencies = [
35
+
"equator",
36
+
]
37
+
38
+
[[package]]
39
+
name = "anstream"
40
+
version = "0.6.21"
41
+
source = "registry+https://github.com/rust-lang/crates.io-index"
42
+
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
43
+
dependencies = [
44
+
"anstyle",
45
+
"anstyle-parse",
46
+
"anstyle-query",
47
+
"anstyle-wincon",
48
+
"colorchoice",
49
+
"is_terminal_polyfill",
50
+
"utf8parse",
51
+
]
52
+
53
+
[[package]]
54
+
name = "anstyle"
55
+
version = "1.0.13"
56
+
source = "registry+https://github.com/rust-lang/crates.io-index"
57
+
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
58
+
59
+
[[package]]
60
+
name = "anstyle-parse"
61
+
version = "0.2.7"
62
+
source = "registry+https://github.com/rust-lang/crates.io-index"
63
+
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
64
+
dependencies = [
65
+
"utf8parse",
66
+
]
67
+
68
+
[[package]]
69
+
name = "anstyle-query"
70
+
version = "1.1.5"
71
+
source = "registry+https://github.com/rust-lang/crates.io-index"
72
+
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
73
+
dependencies = [
74
+
"windows-sys 0.61.2",
75
+
]
76
+
77
+
[[package]]
78
+
name = "anstyle-wincon"
79
+
version = "3.0.11"
80
+
source = "registry+https://github.com/rust-lang/crates.io-index"
81
+
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
82
+
dependencies = [
83
+
"anstyle",
84
+
"once_cell_polyfill",
85
+
"windows-sys 0.61.2",
86
+
]
87
+
88
+
[[package]]
89
+
name = "anyhow"
90
+
version = "1.0.101"
91
+
source = "registry+https://github.com/rust-lang/crates.io-index"
92
+
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
93
+
94
+
[[package]]
95
+
name = "arbitrary"
96
+
version = "1.4.2"
97
+
source = "registry+https://github.com/rust-lang/crates.io-index"
98
+
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
99
+
100
+
[[package]]
101
+
name = "arg_enum_proc_macro"
102
+
version = "0.3.4"
103
+
source = "registry+https://github.com/rust-lang/crates.io-index"
104
+
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
105
+
dependencies = [
106
+
"proc-macro2",
107
+
"quote",
108
+
"syn",
109
+
]
110
+
111
+
[[package]]
112
+
name = "arrayvec"
113
+
version = "0.7.6"
114
+
source = "registry+https://github.com/rust-lang/crates.io-index"
115
+
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
116
+
117
+
[[package]]
118
+
name = "as-slice"
119
+
version = "0.2.1"
120
+
source = "registry+https://github.com/rust-lang/crates.io-index"
121
+
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
122
+
dependencies = [
123
+
"stable_deref_trait",
124
+
]
125
+
126
+
[[package]]
127
+
name = "assert_matches"
128
+
version = "1.5.0"
129
+
source = "registry+https://github.com/rust-lang/crates.io-index"
130
+
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
131
+
132
+
[[package]]
133
+
name = "async-tungstenite"
134
+
version = "0.32.1"
135
+
source = "registry+https://github.com/rust-lang/crates.io-index"
136
+
checksum = "8acc405d38be14342132609f06f02acaf825ddccfe76c4824a69281e0458ebd4"
137
+
dependencies = [
138
+
"atomic-waker",
139
+
"futures-core",
140
+
"futures-io",
141
+
"futures-task",
142
+
"futures-util",
143
+
"log",
144
+
"pin-project-lite",
145
+
"tokio",
146
+
"tungstenite",
147
+
]
148
+
149
+
[[package]]
150
+
name = "atomic-waker"
151
+
version = "1.1.2"
152
+
source = "registry+https://github.com/rust-lang/crates.io-index"
153
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
154
+
155
+
[[package]]
156
+
name = "autocfg"
157
+
version = "1.5.0"
158
+
source = "registry+https://github.com/rust-lang/crates.io-index"
159
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
160
+
161
+
[[package]]
162
+
name = "av-scenechange"
163
+
version = "0.14.1"
164
+
source = "registry+https://github.com/rust-lang/crates.io-index"
165
+
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
166
+
dependencies = [
167
+
"aligned",
168
+
"anyhow",
169
+
"arg_enum_proc_macro",
170
+
"arrayvec",
171
+
"log",
172
+
"num-rational",
173
+
"num-traits",
174
+
"pastey",
175
+
"rayon",
176
+
"thiserror 2.0.18",
177
+
"v_frame",
178
+
"y4m",
179
+
]
180
+
181
+
[[package]]
182
+
name = "av1-grain"
183
+
version = "0.2.5"
184
+
source = "registry+https://github.com/rust-lang/crates.io-index"
185
+
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
186
+
dependencies = [
187
+
"anyhow",
188
+
"arrayvec",
189
+
"log",
190
+
"nom",
191
+
"num-rational",
192
+
"v_frame",
193
+
]
194
+
195
+
[[package]]
196
+
name = "avif-serialize"
197
+
version = "0.8.8"
198
+
source = "registry+https://github.com/rust-lang/crates.io-index"
199
+
checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
200
+
dependencies = [
201
+
"arrayvec",
202
+
]
203
+
204
+
[[package]]
205
+
name = "base64"
206
+
version = "0.22.1"
207
+
source = "registry+https://github.com/rust-lang/crates.io-index"
208
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
209
+
210
+
[[package]]
211
+
name = "bit_field"
212
+
version = "0.10.3"
213
+
source = "registry+https://github.com/rust-lang/crates.io-index"
214
+
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
215
+
216
+
[[package]]
217
+
name = "bitflags"
218
+
version = "2.11.0"
219
+
source = "registry+https://github.com/rust-lang/crates.io-index"
220
+
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
221
+
222
+
[[package]]
223
+
name = "bitstream-io"
224
+
version = "4.9.0"
225
+
source = "registry+https://github.com/rust-lang/crates.io-index"
226
+
checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
227
+
dependencies = [
228
+
"core2",
229
+
]
230
+
231
+
[[package]]
232
+
name = "block-buffer"
233
+
version = "0.10.4"
234
+
source = "registry+https://github.com/rust-lang/crates.io-index"
235
+
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
236
+
dependencies = [
237
+
"generic-array",
238
+
]
239
+
240
+
[[package]]
241
+
name = "browser-stream"
242
+
version = "0.1.0"
243
+
dependencies = [
244
+
"anyhow",
245
+
"assert_matches",
246
+
"base64",
247
+
"chromiumoxide",
248
+
"chromiumoxide_cdp",
249
+
"clap",
250
+
"futures",
251
+
"image",
252
+
"thiserror 2.0.18",
253
+
"tokio",
254
+
"tracing",
255
+
"tracing-subscriber",
256
+
"url",
257
+
]
258
+
259
+
[[package]]
260
+
name = "built"
261
+
version = "0.8.0"
262
+
source = "registry+https://github.com/rust-lang/crates.io-index"
263
+
checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
264
+
265
+
[[package]]
266
+
name = "bumpalo"
267
+
version = "3.20.1"
268
+
source = "registry+https://github.com/rust-lang/crates.io-index"
269
+
checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
270
+
271
+
[[package]]
272
+
name = "bytemuck"
273
+
version = "1.25.0"
274
+
source = "registry+https://github.com/rust-lang/crates.io-index"
275
+
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
276
+
277
+
[[package]]
278
+
name = "byteorder-lite"
279
+
version = "0.1.0"
280
+
source = "registry+https://github.com/rust-lang/crates.io-index"
281
+
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
282
+
283
+
[[package]]
284
+
name = "bytes"
285
+
version = "1.11.1"
286
+
source = "registry+https://github.com/rust-lang/crates.io-index"
287
+
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
288
+
dependencies = [
289
+
"serde",
290
+
]
291
+
292
+
[[package]]
293
+
name = "cc"
294
+
version = "1.2.56"
295
+
source = "registry+https://github.com/rust-lang/crates.io-index"
296
+
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
297
+
dependencies = [
298
+
"find-msvc-tools",
299
+
"jobserver",
300
+
"libc",
301
+
"shlex",
302
+
]
303
+
304
+
[[package]]
305
+
name = "cfg-if"
306
+
version = "1.0.4"
307
+
source = "registry+https://github.com/rust-lang/crates.io-index"
308
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
309
+
310
+
[[package]]
311
+
name = "chromiumoxide"
312
+
version = "0.8.0"
313
+
source = "registry+https://github.com/rust-lang/crates.io-index"
314
+
checksum = "6c18200611490f523adb497ddd4744d6d536e243f6add13e7eeeb1c05904fbb1"
315
+
dependencies = [
316
+
"async-tungstenite",
317
+
"base64",
318
+
"bytes",
319
+
"cfg-if",
320
+
"chromiumoxide_cdp",
321
+
"chromiumoxide_types",
322
+
"dunce",
323
+
"fnv",
324
+
"futures",
325
+
"futures-timer",
326
+
"pin-project-lite",
327
+
"reqwest",
328
+
"serde",
329
+
"serde_json",
330
+
"thiserror 1.0.69",
331
+
"tokio",
332
+
"tracing",
333
+
"url",
334
+
"which",
335
+
"windows-registry",
336
+
]
337
+
338
+
[[package]]
339
+
name = "chromiumoxide_cdp"
340
+
version = "0.8.0"
341
+
source = "registry+https://github.com/rust-lang/crates.io-index"
342
+
checksum = "b8f78027ced540595dcbaf9e2f3413cbe3708b839ff239d2858acaea73915dcb"
343
+
dependencies = [
344
+
"chromiumoxide_pdl",
345
+
"chromiumoxide_types",
346
+
"serde",
347
+
"serde_json",
348
+
]
349
+
350
+
[[package]]
351
+
name = "chromiumoxide_pdl"
352
+
version = "0.8.0"
353
+
source = "registry+https://github.com/rust-lang/crates.io-index"
354
+
checksum = "0d2c7b7c6b41a0de36d00a284e619017e0f4aec5c9bc8d90614b9e1687984f20"
355
+
dependencies = [
356
+
"chromiumoxide_types",
357
+
"either",
358
+
"heck 0.4.1",
359
+
"once_cell",
360
+
"proc-macro2",
361
+
"quote",
362
+
"regex",
363
+
"serde",
364
+
"serde_json",
365
+
]
366
+
367
+
[[package]]
368
+
name = "chromiumoxide_types"
369
+
version = "0.8.0"
370
+
source = "registry+https://github.com/rust-lang/crates.io-index"
371
+
checksum = "309ba8f378bbc093c93f06beb7bd4c5ceffdf14107ad99cacbbf063709926795"
372
+
dependencies = [
373
+
"serde",
374
+
"serde_json",
375
+
]
376
+
377
+
[[package]]
378
+
name = "clap"
379
+
version = "4.5.59"
380
+
source = "registry+https://github.com/rust-lang/crates.io-index"
381
+
checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499"
382
+
dependencies = [
383
+
"clap_builder",
384
+
"clap_derive",
385
+
]
386
+
387
+
[[package]]
388
+
name = "clap_builder"
389
+
version = "4.5.59"
390
+
source = "registry+https://github.com/rust-lang/crates.io-index"
391
+
checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24"
392
+
dependencies = [
393
+
"anstream",
394
+
"anstyle",
395
+
"clap_lex",
396
+
"strsim",
397
+
]
398
+
399
+
[[package]]
400
+
name = "clap_derive"
401
+
version = "4.5.55"
402
+
source = "registry+https://github.com/rust-lang/crates.io-index"
403
+
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
404
+
dependencies = [
405
+
"heck 0.5.0",
406
+
"proc-macro2",
407
+
"quote",
408
+
"syn",
409
+
]
410
+
411
+
[[package]]
412
+
name = "clap_lex"
413
+
version = "1.0.0"
414
+
source = "registry+https://github.com/rust-lang/crates.io-index"
415
+
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
416
+
417
+
[[package]]
418
+
name = "color_quant"
419
+
version = "1.1.0"
420
+
source = "registry+https://github.com/rust-lang/crates.io-index"
421
+
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
422
+
423
+
[[package]]
424
+
name = "colorchoice"
425
+
version = "1.0.4"
426
+
source = "registry+https://github.com/rust-lang/crates.io-index"
427
+
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
428
+
429
+
[[package]]
430
+
name = "core2"
431
+
version = "0.4.0"
432
+
source = "registry+https://github.com/rust-lang/crates.io-index"
433
+
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
434
+
dependencies = [
435
+
"memchr",
436
+
]
437
+
438
+
[[package]]
439
+
name = "cpufeatures"
440
+
version = "0.2.17"
441
+
source = "registry+https://github.com/rust-lang/crates.io-index"
442
+
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
443
+
dependencies = [
444
+
"libc",
445
+
]
446
+
447
+
[[package]]
448
+
name = "crc32fast"
449
+
version = "1.5.0"
450
+
source = "registry+https://github.com/rust-lang/crates.io-index"
451
+
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
452
+
dependencies = [
453
+
"cfg-if",
454
+
]
455
+
456
+
[[package]]
457
+
name = "crossbeam-deque"
458
+
version = "0.8.6"
459
+
source = "registry+https://github.com/rust-lang/crates.io-index"
460
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
461
+
dependencies = [
462
+
"crossbeam-epoch",
463
+
"crossbeam-utils",
464
+
]
465
+
466
+
[[package]]
467
+
name = "crossbeam-epoch"
468
+
version = "0.9.18"
469
+
source = "registry+https://github.com/rust-lang/crates.io-index"
470
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
471
+
dependencies = [
472
+
"crossbeam-utils",
473
+
]
474
+
475
+
[[package]]
476
+
name = "crossbeam-utils"
477
+
version = "0.8.21"
478
+
source = "registry+https://github.com/rust-lang/crates.io-index"
479
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
480
+
481
+
[[package]]
482
+
name = "crunchy"
483
+
version = "0.2.4"
484
+
source = "registry+https://github.com/rust-lang/crates.io-index"
485
+
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
486
+
487
+
[[package]]
488
+
name = "crypto-common"
489
+
version = "0.1.7"
490
+
source = "registry+https://github.com/rust-lang/crates.io-index"
491
+
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
492
+
dependencies = [
493
+
"generic-array",
494
+
"typenum",
495
+
]
496
+
497
+
[[package]]
498
+
name = "data-encoding"
499
+
version = "2.10.0"
500
+
source = "registry+https://github.com/rust-lang/crates.io-index"
501
+
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
502
+
503
+
[[package]]
504
+
name = "digest"
505
+
version = "0.10.7"
506
+
source = "registry+https://github.com/rust-lang/crates.io-index"
507
+
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
508
+
dependencies = [
509
+
"block-buffer",
510
+
"crypto-common",
511
+
]
512
+
513
+
[[package]]
514
+
name = "displaydoc"
515
+
version = "0.2.5"
516
+
source = "registry+https://github.com/rust-lang/crates.io-index"
517
+
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
518
+
dependencies = [
519
+
"proc-macro2",
520
+
"quote",
521
+
"syn",
522
+
]
523
+
524
+
[[package]]
525
+
name = "dunce"
526
+
version = "1.0.5"
527
+
source = "registry+https://github.com/rust-lang/crates.io-index"
528
+
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
529
+
530
+
[[package]]
531
+
name = "either"
532
+
version = "1.15.0"
533
+
source = "registry+https://github.com/rust-lang/crates.io-index"
534
+
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
535
+
536
+
[[package]]
537
+
name = "env_home"
538
+
version = "0.1.0"
539
+
source = "registry+https://github.com/rust-lang/crates.io-index"
540
+
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
541
+
542
+
[[package]]
543
+
name = "equator"
544
+
version = "0.4.2"
545
+
source = "registry+https://github.com/rust-lang/crates.io-index"
546
+
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
547
+
dependencies = [
548
+
"equator-macro",
549
+
]
550
+
551
+
[[package]]
552
+
name = "equator-macro"
553
+
version = "0.4.2"
554
+
source = "registry+https://github.com/rust-lang/crates.io-index"
555
+
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
556
+
dependencies = [
557
+
"proc-macro2",
558
+
"quote",
559
+
"syn",
560
+
]
561
+
562
+
[[package]]
563
+
name = "errno"
564
+
version = "0.3.14"
565
+
source = "registry+https://github.com/rust-lang/crates.io-index"
566
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
567
+
dependencies = [
568
+
"libc",
569
+
"windows-sys 0.61.2",
570
+
]
571
+
572
+
[[package]]
573
+
name = "exr"
574
+
version = "1.74.0"
575
+
source = "registry+https://github.com/rust-lang/crates.io-index"
576
+
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
577
+
dependencies = [
578
+
"bit_field",
579
+
"half",
580
+
"lebe",
581
+
"miniz_oxide",
582
+
"rayon-core",
583
+
"smallvec",
584
+
"zune-inflate",
585
+
]
586
+
587
+
[[package]]
588
+
name = "fax"
589
+
version = "0.2.6"
590
+
source = "registry+https://github.com/rust-lang/crates.io-index"
591
+
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
592
+
dependencies = [
593
+
"fax_derive",
594
+
]
595
+
596
+
[[package]]
597
+
name = "fax_derive"
598
+
version = "0.2.0"
599
+
source = "registry+https://github.com/rust-lang/crates.io-index"
600
+
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
601
+
dependencies = [
602
+
"proc-macro2",
603
+
"quote",
604
+
"syn",
605
+
]
606
+
607
+
[[package]]
608
+
name = "fdeflate"
609
+
version = "0.3.7"
610
+
source = "registry+https://github.com/rust-lang/crates.io-index"
611
+
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
612
+
dependencies = [
613
+
"simd-adler32",
614
+
]
615
+
616
+
[[package]]
617
+
name = "find-msvc-tools"
618
+
version = "0.1.9"
619
+
source = "registry+https://github.com/rust-lang/crates.io-index"
620
+
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
621
+
622
+
[[package]]
623
+
name = "flate2"
624
+
version = "1.1.9"
625
+
source = "registry+https://github.com/rust-lang/crates.io-index"
626
+
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
627
+
dependencies = [
628
+
"crc32fast",
629
+
"miniz_oxide",
630
+
]
631
+
632
+
[[package]]
633
+
name = "fnv"
634
+
version = "1.0.7"
635
+
source = "registry+https://github.com/rust-lang/crates.io-index"
636
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
637
+
638
+
[[package]]
639
+
name = "form_urlencoded"
640
+
version = "1.2.2"
641
+
source = "registry+https://github.com/rust-lang/crates.io-index"
642
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
643
+
dependencies = [
644
+
"percent-encoding",
645
+
]
646
+
647
+
[[package]]
648
+
name = "futures"
649
+
version = "0.3.32"
650
+
source = "registry+https://github.com/rust-lang/crates.io-index"
651
+
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
652
+
dependencies = [
653
+
"futures-channel",
654
+
"futures-core",
655
+
"futures-executor",
656
+
"futures-io",
657
+
"futures-sink",
658
+
"futures-task",
659
+
"futures-util",
660
+
]
661
+
662
+
[[package]]
663
+
name = "futures-channel"
664
+
version = "0.3.32"
665
+
source = "registry+https://github.com/rust-lang/crates.io-index"
666
+
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
667
+
dependencies = [
668
+
"futures-core",
669
+
"futures-sink",
670
+
]
671
+
672
+
[[package]]
673
+
name = "futures-core"
674
+
version = "0.3.32"
675
+
source = "registry+https://github.com/rust-lang/crates.io-index"
676
+
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
677
+
678
+
[[package]]
679
+
name = "futures-executor"
680
+
version = "0.3.32"
681
+
source = "registry+https://github.com/rust-lang/crates.io-index"
682
+
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
683
+
dependencies = [
684
+
"futures-core",
685
+
"futures-task",
686
+
"futures-util",
687
+
]
688
+
689
+
[[package]]
690
+
name = "futures-io"
691
+
version = "0.3.32"
692
+
source = "registry+https://github.com/rust-lang/crates.io-index"
693
+
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
694
+
695
+
[[package]]
696
+
name = "futures-macro"
697
+
version = "0.3.32"
698
+
source = "registry+https://github.com/rust-lang/crates.io-index"
699
+
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
700
+
dependencies = [
701
+
"proc-macro2",
702
+
"quote",
703
+
"syn",
704
+
]
705
+
706
+
[[package]]
707
+
name = "futures-sink"
708
+
version = "0.3.32"
709
+
source = "registry+https://github.com/rust-lang/crates.io-index"
710
+
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
711
+
712
+
[[package]]
713
+
name = "futures-task"
714
+
version = "0.3.32"
715
+
source = "registry+https://github.com/rust-lang/crates.io-index"
716
+
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
717
+
718
+
[[package]]
719
+
name = "futures-timer"
720
+
version = "3.0.3"
721
+
source = "registry+https://github.com/rust-lang/crates.io-index"
722
+
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
723
+
724
+
[[package]]
725
+
name = "futures-util"
726
+
version = "0.3.32"
727
+
source = "registry+https://github.com/rust-lang/crates.io-index"
728
+
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
729
+
dependencies = [
730
+
"futures-channel",
731
+
"futures-core",
732
+
"futures-io",
733
+
"futures-macro",
734
+
"futures-sink",
735
+
"futures-task",
736
+
"memchr",
737
+
"pin-project-lite",
738
+
"slab",
739
+
]
740
+
741
+
[[package]]
742
+
name = "generic-array"
743
+
version = "0.14.7"
744
+
source = "registry+https://github.com/rust-lang/crates.io-index"
745
+
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
746
+
dependencies = [
747
+
"typenum",
748
+
"version_check",
749
+
]
750
+
751
+
[[package]]
752
+
name = "getrandom"
753
+
version = "0.3.4"
754
+
source = "registry+https://github.com/rust-lang/crates.io-index"
755
+
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
756
+
dependencies = [
757
+
"cfg-if",
758
+
"libc",
759
+
"r-efi",
760
+
"wasip2",
761
+
]
762
+
763
+
[[package]]
764
+
name = "gif"
765
+
version = "0.14.1"
766
+
source = "registry+https://github.com/rust-lang/crates.io-index"
767
+
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
768
+
dependencies = [
769
+
"color_quant",
770
+
"weezl",
771
+
]
772
+
773
+
[[package]]
774
+
name = "half"
775
+
version = "2.7.1"
776
+
source = "registry+https://github.com/rust-lang/crates.io-index"
777
+
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
778
+
dependencies = [
779
+
"cfg-if",
780
+
"crunchy",
781
+
"zerocopy",
782
+
]
783
+
784
+
[[package]]
785
+
name = "heck"
786
+
version = "0.4.1"
787
+
source = "registry+https://github.com/rust-lang/crates.io-index"
788
+
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
789
+
790
+
[[package]]
791
+
name = "heck"
792
+
version = "0.5.0"
793
+
source = "registry+https://github.com/rust-lang/crates.io-index"
794
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
795
+
796
+
[[package]]
797
+
name = "http"
798
+
version = "1.4.0"
799
+
source = "registry+https://github.com/rust-lang/crates.io-index"
800
+
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
801
+
dependencies = [
802
+
"bytes",
803
+
"itoa",
804
+
]
805
+
806
+
[[package]]
807
+
name = "http-body"
808
+
version = "1.0.1"
809
+
source = "registry+https://github.com/rust-lang/crates.io-index"
810
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
811
+
dependencies = [
812
+
"bytes",
813
+
"http",
814
+
]
815
+
816
+
[[package]]
817
+
name = "http-body-util"
818
+
version = "0.1.3"
819
+
source = "registry+https://github.com/rust-lang/crates.io-index"
820
+
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
821
+
dependencies = [
822
+
"bytes",
823
+
"futures-core",
824
+
"http",
825
+
"http-body",
826
+
"pin-project-lite",
827
+
]
828
+
829
+
[[package]]
830
+
name = "httparse"
831
+
version = "1.10.1"
832
+
source = "registry+https://github.com/rust-lang/crates.io-index"
833
+
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
834
+
835
+
[[package]]
836
+
name = "hyper"
837
+
version = "1.8.1"
838
+
source = "registry+https://github.com/rust-lang/crates.io-index"
839
+
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
840
+
dependencies = [
841
+
"atomic-waker",
842
+
"bytes",
843
+
"futures-channel",
844
+
"futures-core",
845
+
"http",
846
+
"http-body",
847
+
"httparse",
848
+
"itoa",
849
+
"pin-project-lite",
850
+
"pin-utils",
851
+
"smallvec",
852
+
"tokio",
853
+
"want",
854
+
]
855
+
856
+
[[package]]
857
+
name = "hyper-util"
858
+
version = "0.1.20"
859
+
source = "registry+https://github.com/rust-lang/crates.io-index"
860
+
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
861
+
dependencies = [
862
+
"base64",
863
+
"bytes",
864
+
"futures-channel",
865
+
"futures-util",
866
+
"http",
867
+
"http-body",
868
+
"hyper",
869
+
"ipnet",
870
+
"libc",
871
+
"percent-encoding",
872
+
"pin-project-lite",
873
+
"socket2",
874
+
"tokio",
875
+
"tower-service",
876
+
"tracing",
877
+
]
878
+
879
+
[[package]]
880
+
name = "icu_collections"
881
+
version = "2.1.1"
882
+
source = "registry+https://github.com/rust-lang/crates.io-index"
883
+
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
884
+
dependencies = [
885
+
"displaydoc",
886
+
"potential_utf",
887
+
"yoke",
888
+
"zerofrom",
889
+
"zerovec",
890
+
]
891
+
892
+
[[package]]
893
+
name = "icu_locale_core"
894
+
version = "2.1.1"
895
+
source = "registry+https://github.com/rust-lang/crates.io-index"
896
+
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
897
+
dependencies = [
898
+
"displaydoc",
899
+
"litemap",
900
+
"tinystr",
901
+
"writeable",
902
+
"zerovec",
903
+
]
904
+
905
+
[[package]]
906
+
name = "icu_normalizer"
907
+
version = "2.1.1"
908
+
source = "registry+https://github.com/rust-lang/crates.io-index"
909
+
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
910
+
dependencies = [
911
+
"icu_collections",
912
+
"icu_normalizer_data",
913
+
"icu_properties",
914
+
"icu_provider",
915
+
"smallvec",
916
+
"zerovec",
917
+
]
918
+
919
+
[[package]]
920
+
name = "icu_normalizer_data"
921
+
version = "2.1.1"
922
+
source = "registry+https://github.com/rust-lang/crates.io-index"
923
+
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
924
+
925
+
[[package]]
926
+
name = "icu_properties"
927
+
version = "2.1.2"
928
+
source = "registry+https://github.com/rust-lang/crates.io-index"
929
+
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
930
+
dependencies = [
931
+
"icu_collections",
932
+
"icu_locale_core",
933
+
"icu_properties_data",
934
+
"icu_provider",
935
+
"zerotrie",
936
+
"zerovec",
937
+
]
938
+
939
+
[[package]]
940
+
name = "icu_properties_data"
941
+
version = "2.1.2"
942
+
source = "registry+https://github.com/rust-lang/crates.io-index"
943
+
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
944
+
945
+
[[package]]
946
+
name = "icu_provider"
947
+
version = "2.1.1"
948
+
source = "registry+https://github.com/rust-lang/crates.io-index"
949
+
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
950
+
dependencies = [
951
+
"displaydoc",
952
+
"icu_locale_core",
953
+
"writeable",
954
+
"yoke",
955
+
"zerofrom",
956
+
"zerotrie",
957
+
"zerovec",
958
+
]
959
+
960
+
[[package]]
961
+
name = "idna"
962
+
version = "1.1.0"
963
+
source = "registry+https://github.com/rust-lang/crates.io-index"
964
+
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
965
+
dependencies = [
966
+
"idna_adapter",
967
+
"smallvec",
968
+
"utf8_iter",
969
+
]
970
+
971
+
[[package]]
972
+
name = "idna_adapter"
973
+
version = "1.2.1"
974
+
source = "registry+https://github.com/rust-lang/crates.io-index"
975
+
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
976
+
dependencies = [
977
+
"icu_normalizer",
978
+
"icu_properties",
979
+
]
980
+
981
+
[[package]]
982
+
name = "image"
983
+
version = "0.25.9"
984
+
source = "registry+https://github.com/rust-lang/crates.io-index"
985
+
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
986
+
dependencies = [
987
+
"bytemuck",
988
+
"byteorder-lite",
989
+
"color_quant",
990
+
"exr",
991
+
"gif",
992
+
"image-webp",
993
+
"moxcms",
994
+
"num-traits",
995
+
"png",
996
+
"qoi",
997
+
"ravif",
998
+
"rayon",
999
+
"rgb",
1000
+
"tiff",
1001
+
"zune-core 0.5.1",
1002
+
"zune-jpeg 0.5.12",
1003
+
]
1004
+
1005
+
[[package]]
1006
+
name = "image-webp"
1007
+
version = "0.2.4"
1008
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1009
+
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
1010
+
dependencies = [
1011
+
"byteorder-lite",
1012
+
"quick-error",
1013
+
]
1014
+
1015
+
[[package]]
1016
+
name = "imgref"
1017
+
version = "1.12.0"
1018
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1019
+
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
1020
+
1021
+
[[package]]
1022
+
name = "interpolate_name"
1023
+
version = "0.2.4"
1024
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1025
+
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
1026
+
dependencies = [
1027
+
"proc-macro2",
1028
+
"quote",
1029
+
"syn",
1030
+
]
1031
+
1032
+
[[package]]
1033
+
name = "ipnet"
1034
+
version = "2.11.0"
1035
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1036
+
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
1037
+
1038
+
[[package]]
1039
+
name = "iri-string"
1040
+
version = "0.7.10"
1041
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1042
+
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
1043
+
dependencies = [
1044
+
"memchr",
1045
+
"serde",
1046
+
]
1047
+
1048
+
[[package]]
1049
+
name = "is_terminal_polyfill"
1050
+
version = "1.70.2"
1051
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1052
+
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
1053
+
1054
+
[[package]]
1055
+
name = "itertools"
1056
+
version = "0.14.0"
1057
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1058
+
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
1059
+
dependencies = [
1060
+
"either",
1061
+
]
1062
+
1063
+
[[package]]
1064
+
name = "itoa"
1065
+
version = "1.0.17"
1066
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1067
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
1068
+
1069
+
[[package]]
1070
+
name = "jobserver"
1071
+
version = "0.1.34"
1072
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1073
+
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
1074
+
dependencies = [
1075
+
"getrandom",
1076
+
"libc",
1077
+
]
1078
+
1079
+
[[package]]
1080
+
name = "js-sys"
1081
+
version = "0.3.85"
1082
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1083
+
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
1084
+
dependencies = [
1085
+
"once_cell",
1086
+
"wasm-bindgen",
1087
+
]
1088
+
1089
+
[[package]]
1090
+
name = "lazy_static"
1091
+
version = "1.5.0"
1092
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1093
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
1094
+
1095
+
[[package]]
1096
+
name = "lebe"
1097
+
version = "0.5.3"
1098
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1099
+
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
1100
+
1101
+
[[package]]
1102
+
name = "libc"
1103
+
version = "0.2.182"
1104
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1105
+
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
1106
+
1107
+
[[package]]
1108
+
name = "libfuzzer-sys"
1109
+
version = "0.4.12"
1110
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1111
+
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
1112
+
dependencies = [
1113
+
"arbitrary",
1114
+
"cc",
1115
+
]
1116
+
1117
+
[[package]]
1118
+
name = "linux-raw-sys"
1119
+
version = "0.11.0"
1120
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1121
+
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
1122
+
1123
+
[[package]]
1124
+
name = "litemap"
1125
+
version = "0.8.1"
1126
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1127
+
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
1128
+
1129
+
[[package]]
1130
+
name = "lock_api"
1131
+
version = "0.4.14"
1132
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1133
+
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
1134
+
dependencies = [
1135
+
"scopeguard",
1136
+
]
1137
+
1138
+
[[package]]
1139
+
name = "log"
1140
+
version = "0.4.29"
1141
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1142
+
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
1143
+
1144
+
[[package]]
1145
+
name = "loop9"
1146
+
version = "0.1.5"
1147
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1148
+
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
1149
+
dependencies = [
1150
+
"imgref",
1151
+
]
1152
+
1153
+
[[package]]
1154
+
name = "matchers"
1155
+
version = "0.2.0"
1156
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1157
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
1158
+
dependencies = [
1159
+
"regex-automata",
1160
+
]
1161
+
1162
+
[[package]]
1163
+
name = "maybe-rayon"
1164
+
version = "0.1.1"
1165
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1166
+
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
1167
+
dependencies = [
1168
+
"cfg-if",
1169
+
"rayon",
1170
+
]
1171
+
1172
+
[[package]]
1173
+
name = "memchr"
1174
+
version = "2.8.0"
1175
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1176
+
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
1177
+
1178
+
[[package]]
1179
+
name = "miniz_oxide"
1180
+
version = "0.8.9"
1181
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1182
+
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
1183
+
dependencies = [
1184
+
"adler2",
1185
+
"simd-adler32",
1186
+
]
1187
+
1188
+
[[package]]
1189
+
name = "mio"
1190
+
version = "1.1.1"
1191
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1192
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
1193
+
dependencies = [
1194
+
"libc",
1195
+
"wasi",
1196
+
"windows-sys 0.61.2",
1197
+
]
1198
+
1199
+
[[package]]
1200
+
name = "moxcms"
1201
+
version = "0.7.11"
1202
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1203
+
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
1204
+
dependencies = [
1205
+
"num-traits",
1206
+
"pxfm",
1207
+
]
1208
+
1209
+
[[package]]
1210
+
name = "new_debug_unreachable"
1211
+
version = "1.0.6"
1212
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1213
+
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
1214
+
1215
+
[[package]]
1216
+
name = "nom"
1217
+
version = "8.0.0"
1218
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1219
+
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
1220
+
dependencies = [
1221
+
"memchr",
1222
+
]
1223
+
1224
+
[[package]]
1225
+
name = "noop_proc_macro"
1226
+
version = "0.3.0"
1227
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1228
+
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
1229
+
1230
+
[[package]]
1231
+
name = "nu-ansi-term"
1232
+
version = "0.50.3"
1233
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1234
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
1235
+
dependencies = [
1236
+
"windows-sys 0.61.2",
1237
+
]
1238
+
1239
+
[[package]]
1240
+
name = "num-bigint"
1241
+
version = "0.4.6"
1242
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1243
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
1244
+
dependencies = [
1245
+
"num-integer",
1246
+
"num-traits",
1247
+
]
1248
+
1249
+
[[package]]
1250
+
name = "num-derive"
1251
+
version = "0.4.2"
1252
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1253
+
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
1254
+
dependencies = [
1255
+
"proc-macro2",
1256
+
"quote",
1257
+
"syn",
1258
+
]
1259
+
1260
+
[[package]]
1261
+
name = "num-integer"
1262
+
version = "0.1.46"
1263
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1264
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
1265
+
dependencies = [
1266
+
"num-traits",
1267
+
]
1268
+
1269
+
[[package]]
1270
+
name = "num-rational"
1271
+
version = "0.4.2"
1272
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1273
+
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
1274
+
dependencies = [
1275
+
"num-bigint",
1276
+
"num-integer",
1277
+
"num-traits",
1278
+
]
1279
+
1280
+
[[package]]
1281
+
name = "num-traits"
1282
+
version = "0.2.19"
1283
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1284
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1285
+
dependencies = [
1286
+
"autocfg",
1287
+
]
1288
+
1289
+
[[package]]
1290
+
name = "once_cell"
1291
+
version = "1.21.3"
1292
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1293
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
1294
+
1295
+
[[package]]
1296
+
name = "once_cell_polyfill"
1297
+
version = "1.70.2"
1298
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1299
+
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
1300
+
1301
+
[[package]]
1302
+
name = "parking_lot"
1303
+
version = "0.12.5"
1304
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1305
+
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
1306
+
dependencies = [
1307
+
"lock_api",
1308
+
"parking_lot_core",
1309
+
]
1310
+
1311
+
[[package]]
1312
+
name = "parking_lot_core"
1313
+
version = "0.9.12"
1314
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1315
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
1316
+
dependencies = [
1317
+
"cfg-if",
1318
+
"libc",
1319
+
"redox_syscall",
1320
+
"smallvec",
1321
+
"windows-link 0.2.1",
1322
+
]
1323
+
1324
+
[[package]]
1325
+
name = "paste"
1326
+
version = "1.0.15"
1327
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1328
+
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
1329
+
1330
+
[[package]]
1331
+
name = "pastey"
1332
+
version = "0.1.1"
1333
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1334
+
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
1335
+
1336
+
[[package]]
1337
+
name = "percent-encoding"
1338
+
version = "2.3.2"
1339
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1340
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1341
+
1342
+
[[package]]
1343
+
name = "pin-project-lite"
1344
+
version = "0.2.16"
1345
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1346
+
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
1347
+
1348
+
[[package]]
1349
+
name = "pin-utils"
1350
+
version = "0.1.0"
1351
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1352
+
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
1353
+
1354
+
[[package]]
1355
+
name = "png"
1356
+
version = "0.18.1"
1357
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1358
+
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
1359
+
dependencies = [
1360
+
"bitflags",
1361
+
"crc32fast",
1362
+
"fdeflate",
1363
+
"flate2",
1364
+
"miniz_oxide",
1365
+
]
1366
+
1367
+
[[package]]
1368
+
name = "potential_utf"
1369
+
version = "0.1.4"
1370
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1371
+
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
1372
+
dependencies = [
1373
+
"zerovec",
1374
+
]
1375
+
1376
+
[[package]]
1377
+
name = "ppv-lite86"
1378
+
version = "0.2.21"
1379
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1380
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
1381
+
dependencies = [
1382
+
"zerocopy",
1383
+
]
1384
+
1385
+
[[package]]
1386
+
name = "proc-macro2"
1387
+
version = "1.0.106"
1388
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1389
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
1390
+
dependencies = [
1391
+
"unicode-ident",
1392
+
]
1393
+
1394
+
[[package]]
1395
+
name = "profiling"
1396
+
version = "1.0.17"
1397
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1398
+
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
1399
+
dependencies = [
1400
+
"profiling-procmacros",
1401
+
]
1402
+
1403
+
[[package]]
1404
+
name = "profiling-procmacros"
1405
+
version = "1.0.17"
1406
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1407
+
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
1408
+
dependencies = [
1409
+
"quote",
1410
+
"syn",
1411
+
]
1412
+
1413
+
[[package]]
1414
+
name = "pxfm"
1415
+
version = "0.1.27"
1416
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1417
+
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
1418
+
dependencies = [
1419
+
"num-traits",
1420
+
]
1421
+
1422
+
[[package]]
1423
+
name = "qoi"
1424
+
version = "0.4.1"
1425
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1426
+
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
1427
+
dependencies = [
1428
+
"bytemuck",
1429
+
]
1430
+
1431
+
[[package]]
1432
+
name = "quick-error"
1433
+
version = "2.0.1"
1434
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1435
+
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
1436
+
1437
+
[[package]]
1438
+
name = "quote"
1439
+
version = "1.0.44"
1440
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1441
+
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
1442
+
dependencies = [
1443
+
"proc-macro2",
1444
+
]
1445
+
1446
+
[[package]]
1447
+
name = "r-efi"
1448
+
version = "5.3.0"
1449
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1450
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1451
+
1452
+
[[package]]
1453
+
name = "rand"
1454
+
version = "0.9.2"
1455
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1456
+
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
1457
+
dependencies = [
1458
+
"rand_chacha",
1459
+
"rand_core",
1460
+
]
1461
+
1462
+
[[package]]
1463
+
name = "rand_chacha"
1464
+
version = "0.9.0"
1465
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1466
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1467
+
dependencies = [
1468
+
"ppv-lite86",
1469
+
"rand_core",
1470
+
]
1471
+
1472
+
[[package]]
1473
+
name = "rand_core"
1474
+
version = "0.9.5"
1475
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1476
+
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
1477
+
dependencies = [
1478
+
"getrandom",
1479
+
]
1480
+
1481
+
[[package]]
1482
+
name = "rav1e"
1483
+
version = "0.8.1"
1484
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1485
+
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
1486
+
dependencies = [
1487
+
"aligned-vec",
1488
+
"arbitrary",
1489
+
"arg_enum_proc_macro",
1490
+
"arrayvec",
1491
+
"av-scenechange",
1492
+
"av1-grain",
1493
+
"bitstream-io",
1494
+
"built",
1495
+
"cfg-if",
1496
+
"interpolate_name",
1497
+
"itertools",
1498
+
"libc",
1499
+
"libfuzzer-sys",
1500
+
"log",
1501
+
"maybe-rayon",
1502
+
"new_debug_unreachable",
1503
+
"noop_proc_macro",
1504
+
"num-derive",
1505
+
"num-traits",
1506
+
"paste",
1507
+
"profiling",
1508
+
"rand",
1509
+
"rand_chacha",
1510
+
"simd_helpers",
1511
+
"thiserror 2.0.18",
1512
+
"v_frame",
1513
+
"wasm-bindgen",
1514
+
]
1515
+
1516
+
[[package]]
1517
+
name = "ravif"
1518
+
version = "0.12.0"
1519
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1520
+
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
1521
+
dependencies = [
1522
+
"avif-serialize",
1523
+
"imgref",
1524
+
"loop9",
1525
+
"quick-error",
1526
+
"rav1e",
1527
+
"rayon",
1528
+
"rgb",
1529
+
]
1530
+
1531
+
[[package]]
1532
+
name = "rayon"
1533
+
version = "1.11.0"
1534
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1535
+
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
1536
+
dependencies = [
1537
+
"either",
1538
+
"rayon-core",
1539
+
]
1540
+
1541
+
[[package]]
1542
+
name = "rayon-core"
1543
+
version = "1.13.0"
1544
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1545
+
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
1546
+
dependencies = [
1547
+
"crossbeam-deque",
1548
+
"crossbeam-utils",
1549
+
]
1550
+
1551
+
[[package]]
1552
+
name = "redox_syscall"
1553
+
version = "0.5.18"
1554
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1555
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
1556
+
dependencies = [
1557
+
"bitflags",
1558
+
]
1559
+
1560
+
[[package]]
1561
+
name = "regex"
1562
+
version = "1.12.3"
1563
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1564
+
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
1565
+
dependencies = [
1566
+
"aho-corasick",
1567
+
"memchr",
1568
+
"regex-automata",
1569
+
"regex-syntax",
1570
+
]
1571
+
1572
+
[[package]]
1573
+
name = "regex-automata"
1574
+
version = "0.4.14"
1575
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1576
+
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
1577
+
dependencies = [
1578
+
"aho-corasick",
1579
+
"memchr",
1580
+
"regex-syntax",
1581
+
]
1582
+
1583
+
[[package]]
1584
+
name = "regex-syntax"
1585
+
version = "0.8.9"
1586
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1587
+
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
1588
+
1589
+
[[package]]
1590
+
name = "reqwest"
1591
+
version = "0.12.28"
1592
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1593
+
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
1594
+
dependencies = [
1595
+
"base64",
1596
+
"bytes",
1597
+
"futures-core",
1598
+
"http",
1599
+
"http-body",
1600
+
"http-body-util",
1601
+
"hyper",
1602
+
"hyper-util",
1603
+
"js-sys",
1604
+
"log",
1605
+
"percent-encoding",
1606
+
"pin-project-lite",
1607
+
"serde",
1608
+
"serde_json",
1609
+
"serde_urlencoded",
1610
+
"sync_wrapper",
1611
+
"tokio",
1612
+
"tower",
1613
+
"tower-http",
1614
+
"tower-service",
1615
+
"url",
1616
+
"wasm-bindgen",
1617
+
"wasm-bindgen-futures",
1618
+
"web-sys",
1619
+
]
1620
+
1621
+
[[package]]
1622
+
name = "rgb"
1623
+
version = "0.8.52"
1624
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1625
+
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
1626
+
1627
+
[[package]]
1628
+
name = "rustix"
1629
+
version = "1.1.3"
1630
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1631
+
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
1632
+
dependencies = [
1633
+
"bitflags",
1634
+
"errno",
1635
+
"libc",
1636
+
"linux-raw-sys",
1637
+
"windows-sys 0.61.2",
1638
+
]
1639
+
1640
+
[[package]]
1641
+
name = "rustversion"
1642
+
version = "1.0.22"
1643
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1644
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
1645
+
1646
+
[[package]]
1647
+
name = "ryu"
1648
+
version = "1.0.23"
1649
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1650
+
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1651
+
1652
+
[[package]]
1653
+
name = "scopeguard"
1654
+
version = "1.2.0"
1655
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1656
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1657
+
1658
+
[[package]]
1659
+
name = "serde"
1660
+
version = "1.0.228"
1661
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1662
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
1663
+
dependencies = [
1664
+
"serde_core",
1665
+
"serde_derive",
1666
+
]
1667
+
1668
+
[[package]]
1669
+
name = "serde_core"
1670
+
version = "1.0.228"
1671
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1672
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
1673
+
dependencies = [
1674
+
"serde_derive",
1675
+
]
1676
+
1677
+
[[package]]
1678
+
name = "serde_derive"
1679
+
version = "1.0.228"
1680
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1681
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
1682
+
dependencies = [
1683
+
"proc-macro2",
1684
+
"quote",
1685
+
"syn",
1686
+
]
1687
+
1688
+
[[package]]
1689
+
name = "serde_json"
1690
+
version = "1.0.149"
1691
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1692
+
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
1693
+
dependencies = [
1694
+
"itoa",
1695
+
"memchr",
1696
+
"serde",
1697
+
"serde_core",
1698
+
"zmij",
1699
+
]
1700
+
1701
+
[[package]]
1702
+
name = "serde_urlencoded"
1703
+
version = "0.7.1"
1704
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1705
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1706
+
dependencies = [
1707
+
"form_urlencoded",
1708
+
"itoa",
1709
+
"ryu",
1710
+
"serde",
1711
+
]
1712
+
1713
+
[[package]]
1714
+
name = "sha1"
1715
+
version = "0.10.6"
1716
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1717
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
1718
+
dependencies = [
1719
+
"cfg-if",
1720
+
"cpufeatures",
1721
+
"digest",
1722
+
]
1723
+
1724
+
[[package]]
1725
+
name = "sharded-slab"
1726
+
version = "0.1.7"
1727
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1728
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
1729
+
dependencies = [
1730
+
"lazy_static",
1731
+
]
1732
+
1733
+
[[package]]
1734
+
name = "shlex"
1735
+
version = "1.3.0"
1736
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1737
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1738
+
1739
+
[[package]]
1740
+
name = "signal-hook-registry"
1741
+
version = "1.4.8"
1742
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1743
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
1744
+
dependencies = [
1745
+
"errno",
1746
+
"libc",
1747
+
]
1748
+
1749
+
[[package]]
1750
+
name = "simd-adler32"
1751
+
version = "0.3.8"
1752
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1753
+
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
1754
+
1755
+
[[package]]
1756
+
name = "simd_helpers"
1757
+
version = "0.1.0"
1758
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1759
+
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
1760
+
dependencies = [
1761
+
"quote",
1762
+
]
1763
+
1764
+
[[package]]
1765
+
name = "slab"
1766
+
version = "0.4.12"
1767
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1768
+
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1769
+
1770
+
[[package]]
1771
+
name = "smallvec"
1772
+
version = "1.15.1"
1773
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1774
+
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
1775
+
1776
+
[[package]]
1777
+
name = "socket2"
1778
+
version = "0.6.2"
1779
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1780
+
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
1781
+
dependencies = [
1782
+
"libc",
1783
+
"windows-sys 0.60.2",
1784
+
]
1785
+
1786
+
[[package]]
1787
+
name = "stable_deref_trait"
1788
+
version = "1.2.1"
1789
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1790
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1791
+
1792
+
[[package]]
1793
+
name = "strsim"
1794
+
version = "0.11.1"
1795
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1796
+
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1797
+
1798
+
[[package]]
1799
+
name = "syn"
1800
+
version = "2.0.116"
1801
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1802
+
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
1803
+
dependencies = [
1804
+
"proc-macro2",
1805
+
"quote",
1806
+
"unicode-ident",
1807
+
]
1808
+
1809
+
[[package]]
1810
+
name = "sync_wrapper"
1811
+
version = "1.0.2"
1812
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1813
+
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
1814
+
dependencies = [
1815
+
"futures-core",
1816
+
]
1817
+
1818
+
[[package]]
1819
+
name = "synstructure"
1820
+
version = "0.13.2"
1821
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1822
+
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1823
+
dependencies = [
1824
+
"proc-macro2",
1825
+
"quote",
1826
+
"syn",
1827
+
]
1828
+
1829
+
[[package]]
1830
+
name = "thiserror"
1831
+
version = "1.0.69"
1832
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1833
+
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1834
+
dependencies = [
1835
+
"thiserror-impl 1.0.69",
1836
+
]
1837
+
1838
+
[[package]]
1839
+
name = "thiserror"
1840
+
version = "2.0.18"
1841
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1842
+
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
1843
+
dependencies = [
1844
+
"thiserror-impl 2.0.18",
1845
+
]
1846
+
1847
+
[[package]]
1848
+
name = "thiserror-impl"
1849
+
version = "1.0.69"
1850
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1851
+
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1852
+
dependencies = [
1853
+
"proc-macro2",
1854
+
"quote",
1855
+
"syn",
1856
+
]
1857
+
1858
+
[[package]]
1859
+
name = "thiserror-impl"
1860
+
version = "2.0.18"
1861
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1862
+
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
1863
+
dependencies = [
1864
+
"proc-macro2",
1865
+
"quote",
1866
+
"syn",
1867
+
]
1868
+
1869
+
[[package]]
1870
+
name = "thread_local"
1871
+
version = "1.1.9"
1872
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1873
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
1874
+
dependencies = [
1875
+
"cfg-if",
1876
+
]
1877
+
1878
+
[[package]]
1879
+
name = "tiff"
1880
+
version = "0.10.3"
1881
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1882
+
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
1883
+
dependencies = [
1884
+
"fax",
1885
+
"flate2",
1886
+
"half",
1887
+
"quick-error",
1888
+
"weezl",
1889
+
"zune-jpeg 0.4.21",
1890
+
]
1891
+
1892
+
[[package]]
1893
+
name = "tinystr"
1894
+
version = "0.8.2"
1895
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1896
+
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
1897
+
dependencies = [
1898
+
"displaydoc",
1899
+
"zerovec",
1900
+
]
1901
+
1902
+
[[package]]
1903
+
name = "tokio"
1904
+
version = "1.49.0"
1905
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1906
+
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
1907
+
dependencies = [
1908
+
"bytes",
1909
+
"libc",
1910
+
"mio",
1911
+
"parking_lot",
1912
+
"pin-project-lite",
1913
+
"signal-hook-registry",
1914
+
"socket2",
1915
+
"tokio-macros",
1916
+
"windows-sys 0.61.2",
1917
+
]
1918
+
1919
+
[[package]]
1920
+
name = "tokio-macros"
1921
+
version = "2.6.0"
1922
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1923
+
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
1924
+
dependencies = [
1925
+
"proc-macro2",
1926
+
"quote",
1927
+
"syn",
1928
+
]
1929
+
1930
+
[[package]]
1931
+
name = "tower"
1932
+
version = "0.5.3"
1933
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1934
+
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
1935
+
dependencies = [
1936
+
"futures-core",
1937
+
"futures-util",
1938
+
"pin-project-lite",
1939
+
"sync_wrapper",
1940
+
"tokio",
1941
+
"tower-layer",
1942
+
"tower-service",
1943
+
]
1944
+
1945
+
[[package]]
1946
+
name = "tower-http"
1947
+
version = "0.6.8"
1948
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1949
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
1950
+
dependencies = [
1951
+
"bitflags",
1952
+
"bytes",
1953
+
"futures-util",
1954
+
"http",
1955
+
"http-body",
1956
+
"iri-string",
1957
+
"pin-project-lite",
1958
+
"tower",
1959
+
"tower-layer",
1960
+
"tower-service",
1961
+
]
1962
+
1963
+
[[package]]
1964
+
name = "tower-layer"
1965
+
version = "0.3.3"
1966
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1967
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1968
+
1969
+
[[package]]
1970
+
name = "tower-service"
1971
+
version = "0.3.3"
1972
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1973
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1974
+
1975
+
[[package]]
1976
+
name = "tracing"
1977
+
version = "0.1.44"
1978
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1979
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1980
+
dependencies = [
1981
+
"pin-project-lite",
1982
+
"tracing-attributes",
1983
+
"tracing-core",
1984
+
]
1985
+
1986
+
[[package]]
1987
+
name = "tracing-attributes"
1988
+
version = "0.1.31"
1989
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1990
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1991
+
dependencies = [
1992
+
"proc-macro2",
1993
+
"quote",
1994
+
"syn",
1995
+
]
1996
+
1997
+
[[package]]
1998
+
name = "tracing-core"
1999
+
version = "0.1.36"
2000
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2001
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
2002
+
dependencies = [
2003
+
"once_cell",
2004
+
"valuable",
2005
+
]
2006
+
2007
+
[[package]]
2008
+
name = "tracing-log"
2009
+
version = "0.2.0"
2010
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2011
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
2012
+
dependencies = [
2013
+
"log",
2014
+
"once_cell",
2015
+
"tracing-core",
2016
+
]
2017
+
2018
+
[[package]]
2019
+
name = "tracing-subscriber"
2020
+
version = "0.3.22"
2021
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2022
+
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
2023
+
dependencies = [
2024
+
"matchers",
2025
+
"nu-ansi-term",
2026
+
"once_cell",
2027
+
"regex-automata",
2028
+
"sharded-slab",
2029
+
"smallvec",
2030
+
"thread_local",
2031
+
"tracing",
2032
+
"tracing-core",
2033
+
"tracing-log",
2034
+
]
2035
+
2036
+
[[package]]
2037
+
name = "try-lock"
2038
+
version = "0.2.5"
2039
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2040
+
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
2041
+
2042
+
[[package]]
2043
+
name = "tungstenite"
2044
+
version = "0.28.0"
2045
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2046
+
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
2047
+
dependencies = [
2048
+
"bytes",
2049
+
"data-encoding",
2050
+
"http",
2051
+
"httparse",
2052
+
"log",
2053
+
"rand",
2054
+
"sha1",
2055
+
"thiserror 2.0.18",
2056
+
"utf-8",
2057
+
]
2058
+
2059
+
[[package]]
2060
+
name = "typenum"
2061
+
version = "1.19.0"
2062
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2063
+
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
2064
+
2065
+
[[package]]
2066
+
name = "unicode-ident"
2067
+
version = "1.0.24"
2068
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2069
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
2070
+
2071
+
[[package]]
2072
+
name = "url"
2073
+
version = "2.5.8"
2074
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2075
+
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
2076
+
dependencies = [
2077
+
"form_urlencoded",
2078
+
"idna",
2079
+
"percent-encoding",
2080
+
"serde",
2081
+
]
2082
+
2083
+
[[package]]
2084
+
name = "utf-8"
2085
+
version = "0.7.6"
2086
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2087
+
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
2088
+
2089
+
[[package]]
2090
+
name = "utf8_iter"
2091
+
version = "1.0.4"
2092
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2093
+
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
2094
+
2095
+
[[package]]
2096
+
name = "utf8parse"
2097
+
version = "0.2.2"
2098
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2099
+
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
2100
+
2101
+
[[package]]
2102
+
name = "v_frame"
2103
+
version = "0.3.9"
2104
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2105
+
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
2106
+
dependencies = [
2107
+
"aligned-vec",
2108
+
"num-traits",
2109
+
"wasm-bindgen",
2110
+
]
2111
+
2112
+
[[package]]
2113
+
name = "valuable"
2114
+
version = "0.1.1"
2115
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2116
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
2117
+
2118
+
[[package]]
2119
+
name = "version_check"
2120
+
version = "0.9.5"
2121
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2122
+
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2123
+
2124
+
[[package]]
2125
+
name = "want"
2126
+
version = "0.3.1"
2127
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2128
+
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
2129
+
dependencies = [
2130
+
"try-lock",
2131
+
]
2132
+
2133
+
[[package]]
2134
+
name = "wasi"
2135
+
version = "0.11.1+wasi-snapshot-preview1"
2136
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2137
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2138
+
2139
+
[[package]]
2140
+
name = "wasip2"
2141
+
version = "1.0.2+wasi-0.2.9"
2142
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2143
+
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
2144
+
dependencies = [
2145
+
"wit-bindgen",
2146
+
]
2147
+
2148
+
[[package]]
2149
+
name = "wasm-bindgen"
2150
+
version = "0.2.108"
2151
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2152
+
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
2153
+
dependencies = [
2154
+
"cfg-if",
2155
+
"once_cell",
2156
+
"rustversion",
2157
+
"wasm-bindgen-macro",
2158
+
"wasm-bindgen-shared",
2159
+
]
2160
+
2161
+
[[package]]
2162
+
name = "wasm-bindgen-futures"
2163
+
version = "0.4.58"
2164
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2165
+
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
2166
+
dependencies = [
2167
+
"cfg-if",
2168
+
"futures-util",
2169
+
"js-sys",
2170
+
"once_cell",
2171
+
"wasm-bindgen",
2172
+
"web-sys",
2173
+
]
2174
+
2175
+
[[package]]
2176
+
name = "wasm-bindgen-macro"
2177
+
version = "0.2.108"
2178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2179
+
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
2180
+
dependencies = [
2181
+
"quote",
2182
+
"wasm-bindgen-macro-support",
2183
+
]
2184
+
2185
+
[[package]]
2186
+
name = "wasm-bindgen-macro-support"
2187
+
version = "0.2.108"
2188
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2189
+
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
2190
+
dependencies = [
2191
+
"bumpalo",
2192
+
"proc-macro2",
2193
+
"quote",
2194
+
"syn",
2195
+
"wasm-bindgen-shared",
2196
+
]
2197
+
2198
+
[[package]]
2199
+
name = "wasm-bindgen-shared"
2200
+
version = "0.2.108"
2201
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2202
+
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
2203
+
dependencies = [
2204
+
"unicode-ident",
2205
+
]
2206
+
2207
+
[[package]]
2208
+
name = "web-sys"
2209
+
version = "0.3.85"
2210
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2211
+
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
2212
+
dependencies = [
2213
+
"js-sys",
2214
+
"wasm-bindgen",
2215
+
]
2216
+
2217
+
[[package]]
2218
+
name = "weezl"
2219
+
version = "0.1.12"
2220
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2221
+
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
2222
+
2223
+
[[package]]
2224
+
name = "which"
2225
+
version = "8.0.0"
2226
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2227
+
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
2228
+
dependencies = [
2229
+
"env_home",
2230
+
"rustix",
2231
+
"winsafe",
2232
+
]
2233
+
2234
+
[[package]]
2235
+
name = "windows-link"
2236
+
version = "0.1.3"
2237
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2238
+
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
2239
+
2240
+
[[package]]
2241
+
name = "windows-link"
2242
+
version = "0.2.1"
2243
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2244
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2245
+
2246
+
[[package]]
2247
+
name = "windows-registry"
2248
+
version = "0.5.3"
2249
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2250
+
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
2251
+
dependencies = [
2252
+
"windows-link 0.1.3",
2253
+
"windows-result",
2254
+
"windows-strings",
2255
+
]
2256
+
2257
+
[[package]]
2258
+
name = "windows-result"
2259
+
version = "0.3.4"
2260
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2261
+
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
2262
+
dependencies = [
2263
+
"windows-link 0.1.3",
2264
+
]
2265
+
2266
+
[[package]]
2267
+
name = "windows-strings"
2268
+
version = "0.4.2"
2269
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2270
+
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
2271
+
dependencies = [
2272
+
"windows-link 0.1.3",
2273
+
]
2274
+
2275
+
[[package]]
2276
+
name = "windows-sys"
2277
+
version = "0.60.2"
2278
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2279
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
2280
+
dependencies = [
2281
+
"windows-targets",
2282
+
]
2283
+
2284
+
[[package]]
2285
+
name = "windows-sys"
2286
+
version = "0.61.2"
2287
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2288
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
2289
+
dependencies = [
2290
+
"windows-link 0.2.1",
2291
+
]
2292
+
2293
+
[[package]]
2294
+
name = "windows-targets"
2295
+
version = "0.53.5"
2296
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2297
+
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
2298
+
dependencies = [
2299
+
"windows-link 0.2.1",
2300
+
"windows_aarch64_gnullvm",
2301
+
"windows_aarch64_msvc",
2302
+
"windows_i686_gnu",
2303
+
"windows_i686_gnullvm",
2304
+
"windows_i686_msvc",
2305
+
"windows_x86_64_gnu",
2306
+
"windows_x86_64_gnullvm",
2307
+
"windows_x86_64_msvc",
2308
+
]
2309
+
2310
+
[[package]]
2311
+
name = "windows_aarch64_gnullvm"
2312
+
version = "0.53.1"
2313
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2314
+
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
2315
+
2316
+
[[package]]
2317
+
name = "windows_aarch64_msvc"
2318
+
version = "0.53.1"
2319
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2320
+
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
2321
+
2322
+
[[package]]
2323
+
name = "windows_i686_gnu"
2324
+
version = "0.53.1"
2325
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2326
+
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
2327
+
2328
+
[[package]]
2329
+
name = "windows_i686_gnullvm"
2330
+
version = "0.53.1"
2331
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2332
+
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
2333
+
2334
+
[[package]]
2335
+
name = "windows_i686_msvc"
2336
+
version = "0.53.1"
2337
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2338
+
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
2339
+
2340
+
[[package]]
2341
+
name = "windows_x86_64_gnu"
2342
+
version = "0.53.1"
2343
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2344
+
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
2345
+
2346
+
[[package]]
2347
+
name = "windows_x86_64_gnullvm"
2348
+
version = "0.53.1"
2349
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2350
+
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
2351
+
2352
+
[[package]]
2353
+
name = "windows_x86_64_msvc"
2354
+
version = "0.53.1"
2355
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2356
+
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
2357
+
2358
+
[[package]]
2359
+
name = "winsafe"
2360
+
version = "0.0.19"
2361
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2362
+
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
2363
+
2364
+
[[package]]
2365
+
name = "wit-bindgen"
2366
+
version = "0.51.0"
2367
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2368
+
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
2369
+
2370
+
[[package]]
2371
+
name = "writeable"
2372
+
version = "0.6.2"
2373
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2374
+
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
2375
+
2376
+
[[package]]
2377
+
name = "y4m"
2378
+
version = "0.8.0"
2379
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2380
+
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
2381
+
2382
+
[[package]]
2383
+
name = "yoke"
2384
+
version = "0.8.1"
2385
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2386
+
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
2387
+
dependencies = [
2388
+
"stable_deref_trait",
2389
+
"yoke-derive",
2390
+
"zerofrom",
2391
+
]
2392
+
2393
+
[[package]]
2394
+
name = "yoke-derive"
2395
+
version = "0.8.1"
2396
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2397
+
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
2398
+
dependencies = [
2399
+
"proc-macro2",
2400
+
"quote",
2401
+
"syn",
2402
+
"synstructure",
2403
+
]
2404
+
2405
+
[[package]]
2406
+
name = "zerocopy"
2407
+
version = "0.8.39"
2408
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2409
+
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
2410
+
dependencies = [
2411
+
"zerocopy-derive",
2412
+
]
2413
+
2414
+
[[package]]
2415
+
name = "zerocopy-derive"
2416
+
version = "0.8.39"
2417
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2418
+
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
2419
+
dependencies = [
2420
+
"proc-macro2",
2421
+
"quote",
2422
+
"syn",
2423
+
]
2424
+
2425
+
[[package]]
2426
+
name = "zerofrom"
2427
+
version = "0.1.6"
2428
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2429
+
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
2430
+
dependencies = [
2431
+
"zerofrom-derive",
2432
+
]
2433
+
2434
+
[[package]]
2435
+
name = "zerofrom-derive"
2436
+
version = "0.1.6"
2437
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2438
+
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
2439
+
dependencies = [
2440
+
"proc-macro2",
2441
+
"quote",
2442
+
"syn",
2443
+
"synstructure",
2444
+
]
2445
+
2446
+
[[package]]
2447
+
name = "zerotrie"
2448
+
version = "0.2.3"
2449
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2450
+
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
2451
+
dependencies = [
2452
+
"displaydoc",
2453
+
"yoke",
2454
+
"zerofrom",
2455
+
]
2456
+
2457
+
[[package]]
2458
+
name = "zerovec"
2459
+
version = "0.11.5"
2460
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2461
+
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
2462
+
dependencies = [
2463
+
"yoke",
2464
+
"zerofrom",
2465
+
"zerovec-derive",
2466
+
]
2467
+
2468
+
[[package]]
2469
+
name = "zerovec-derive"
2470
+
version = "0.11.2"
2471
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2472
+
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
2473
+
dependencies = [
2474
+
"proc-macro2",
2475
+
"quote",
2476
+
"syn",
2477
+
]
2478
+
2479
+
[[package]]
2480
+
name = "zmij"
2481
+
version = "1.0.21"
2482
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2483
+
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
2484
+
2485
+
[[package]]
2486
+
name = "zune-core"
2487
+
version = "0.4.12"
2488
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2489
+
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
2490
+
2491
+
[[package]]
2492
+
name = "zune-core"
2493
+
version = "0.5.1"
2494
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2495
+
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
2496
+
2497
+
[[package]]
2498
+
name = "zune-inflate"
2499
+
version = "0.2.54"
2500
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2501
+
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
2502
+
dependencies = [
2503
+
"simd-adler32",
2504
+
]
2505
+
2506
+
[[package]]
2507
+
name = "zune-jpeg"
2508
+
version = "0.4.21"
2509
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2510
+
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
2511
+
dependencies = [
2512
+
"zune-core 0.4.12",
2513
+
]
2514
+
2515
+
[[package]]
2516
+
name = "zune-jpeg"
2517
+
version = "0.5.12"
2518
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2519
+
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
2520
+
dependencies = [
2521
+
"zune-core 0.5.1",
2522
+
]
+21
Cargo.toml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
[package]
2
+
name = "browser-stream"
3
+
version = "0.1.0"
4
+
edition = "2024"
5
+
6
+
[dependencies]
7
+
anyhow = "1.0"
8
+
base64 = "0.22"
9
+
clap = { version = "4.5", features = ["derive"] }
10
+
chromiumoxide = "0.8"
11
+
chromiumoxide_cdp = "0.8"
12
+
futures = "0.3"
13
+
image = { version = "0.25", default-features = true, features = ["jpeg", "png"] }
14
+
thiserror = "2.0"
15
+
tokio = { version = "1.47", features = ["full"] }
16
+
tracing = "0.1"
17
+
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
18
+
url = "2.5"
19
+
20
+
[dev-dependencies]
21
+
assert_matches = "1.5"
+113
Dockerfile
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
FROM rust:1.89-bookworm AS builder
2
+
WORKDIR /app
3
+
4
+
COPY Cargo.toml Cargo.lock ./
5
+
COPY src ./src
6
+
7
+
RUN cargo build --release
8
+
9
+
FROM debian:bookworm-slim AS sidecar-fetch
10
+
ARG TARGETARCH
11
+
12
+
RUN arch="${TARGETARCH:-amd64}" \
13
+
&& test "${arch}" = "amd64" || (echo "full image sidecars currently support linux/amd64 only" >&2 && exit 1)
14
+
15
+
RUN apt-get update \
16
+
&& apt-get install -y --no-install-recommends \
17
+
ca-certificates \
18
+
curl \
19
+
jq \
20
+
unzip \
21
+
&& rm -rf /var/lib/apt/lists/*
22
+
23
+
RUN set -euo pipefail; \
24
+
mkdir -p /out/sidecar/chromium /out/sidecar/ffmpeg; \
25
+
chromium_manifest_url="https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"; \
26
+
chromium_url="$(curl -fsSL "${chromium_manifest_url}" | jq -r '.channels.Stable.downloads["chrome-headless-shell"][] | select(.platform == "linux64") | .url')"; \
27
+
test -n "${chromium_url}" && test "${chromium_url}" != "null"; \
28
+
curl -fL "${chromium_url}" -o /tmp/chromium.zip; \
29
+
unzip -q /tmp/chromium.zip -d /tmp/chromium; \
30
+
chromium_source="$(find /tmp/chromium -type f -name chrome-headless-shell | head -n 1)"; \
31
+
test -n "${chromium_source}"; \
32
+
chromium_root="$(dirname "${chromium_source}")"; \
33
+
cp -R "${chromium_root}"/. /out/sidecar/chromium/; \
34
+
cp "${chromium_source}" /out/sidecar/chromium/headless_shell; \
35
+
chmod +x /out/sidecar/chromium/headless_shell; \
36
+
ffmpeg_url="https://ffmpeg.martin-riedl.de/redirect/latest/linux/amd64/release/ffmpeg.zip"; \
37
+
curl -fL "${ffmpeg_url}" -o /tmp/ffmpeg.zip; \
38
+
unzip -q /tmp/ffmpeg.zip -d /tmp/ffmpeg; \
39
+
ffmpeg_source="$(find /tmp/ffmpeg -type f -name ffmpeg | head -n 1)"; \
40
+
test -n "${ffmpeg_source}"; \
41
+
cp "${ffmpeg_source}" /out/sidecar/ffmpeg/ffmpeg; \
42
+
chmod +x /out/sidecar/ffmpeg/ffmpeg
43
+
44
+
FROM debian:bookworm-slim AS runtime-base
45
+
WORKDIR /app
46
+
47
+
RUN apt-get update \
48
+
&& apt-get install -y --no-install-recommends \
49
+
ca-certificates \
50
+
fonts-liberation \
51
+
libasound2 \
52
+
libatk-bridge2.0-0 \
53
+
libatk1.0-0 \
54
+
libc6 \
55
+
libcairo2 \
56
+
libcups2 \
57
+
libdbus-1-3 \
58
+
libdrm2 \
59
+
libexpat1 \
60
+
libgbm1 \
61
+
libglib2.0-0 \
62
+
libgtk-3-0 \
63
+
libnspr4 \
64
+
libnss3 \
65
+
libpango-1.0-0 \
66
+
libu2f-udev \
67
+
libx11-6 \
68
+
libx11-xcb1 \
69
+
libxcb1 \
70
+
libxcomposite1 \
71
+
libxcursor1 \
72
+
libxdamage1 \
73
+
libxext6 \
74
+
libxfixes3 \
75
+
libxi6 \
76
+
libxkbcommon0 \
77
+
libxrandr2 \
78
+
libxrender1 \
79
+
libxshmfence1 \
80
+
xdg-utils \
81
+
&& rm -rf /var/lib/apt/lists/*
82
+
83
+
COPY --from=builder /app/target/release/browser-stream /usr/local/bin/browser-stream
84
+
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
85
+
RUN chmod +x /usr/local/bin/entrypoint.sh
86
+
87
+
RUN groupadd --system browserstream \
88
+
&& useradd --system --gid browserstream --create-home --home-dir /home/browserstream browserstream
89
+
90
+
ENV RUST_LOG=info
91
+
ENV BROWSER_STREAM_NO_SANDBOX=1
92
+
93
+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
94
+
95
+
FROM runtime-base AS full
96
+
COPY --from=sidecar-fetch /out/sidecar /opt/sidecar
97
+
RUN chown -R browserstream:browserstream /opt/sidecar
98
+
99
+
ENV BROWSER_STREAM_CHROMIUM_PATH=/opt/sidecar/chromium/headless_shell
100
+
ENV BROWSER_STREAM_FFMPEG_PATH=/opt/sidecar/ffmpeg/ffmpeg
101
+
102
+
USER browserstream
103
+
104
+
FROM runtime-base AS slim
105
+
RUN apt-get update \
106
+
&& apt-get install -y --no-install-recommends \
107
+
chromium \
108
+
ffmpeg \
109
+
&& rm -rf /var/lib/apt/lists/*
110
+
111
+
USER browserstream
112
+
113
+
FROM slim AS default
+177
README.md
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
# browser-stream
2
+
3
+
Rust CLI to stream a fullscreen browser-rendered website to RTMP/RTMPS using bundled Chromium Headless Shell and FFmpeg sidecars.
4
+
5
+
## Expected sidecar layout
6
+
7
+
The binary resolves sidecars relative to itself:
8
+
9
+
- `../sidecar/chromium/headless_shell` (`headless_shell.exe` on Windows)
10
+
- `../sidecar/ffmpeg/ffmpeg` (`ffmpeg.exe` on Windows)
11
+
12
+
You can override both paths with CLI flags:
13
+
14
+
- `--chromium-path /abs/path/to/headless_shell`
15
+
- `--ffmpeg-path /abs/path/to/ffmpeg`
16
+
17
+
## Local Sidecar Setup
18
+
19
+
When running with `cargo run`, the executable lives under `target/`, so sidecars are expected under `target/sidecar`.
20
+
21
+
Fetch sidecars for your host platform:
22
+
23
+
```bash
24
+
./scripts/fetch-sidecars.sh
25
+
```
26
+
27
+
Windows (PowerShell):
28
+
29
+
```powershell
30
+
./scripts/fetch-sidecars.ps1
31
+
```
32
+
33
+
## Usage
34
+
35
+
```bash
36
+
cargo run -- \
37
+
--url https://example.com \
38
+
--width 1920 \
39
+
--height 1080 \
40
+
--fps 30 \
41
+
--bitrate-kbps 4500 \
42
+
--keyint-sec 1 \
43
+
--x264-opts bframes=0 \
44
+
--rtmp-url rtmp://live.example.com/app \
45
+
--stream-key mystream
46
+
```
47
+
48
+
Equivalent full-output form:
49
+
50
+
```bash
51
+
cargo run -- \
52
+
--url https://example.com \
53
+
--output rtmp://live.example.com/app/mystream
54
+
```
55
+
56
+
## Docker Compose
57
+
58
+
`docker-compose.yml` defines two image variants:
59
+
60
+
- `slim` (default service `browser-stream`): uses system `chromium` + `ffmpeg` packages in the container.
61
+
- `full` (service `browser-stream-full`): bundles sidecar binaries in image at `/opt/sidecar/...`.
62
+
63
+
Build and run `slim` (default):
64
+
65
+
```bash
66
+
docker compose up --build
67
+
```
68
+
69
+
Build and run `full`:
70
+
71
+
```bash
72
+
docker compose --profile full up --build browser-stream-full
73
+
```
74
+
75
+
Or build directly with Docker targets:
76
+
77
+
```bash
78
+
docker build --target slim -t browser-stream:slim .
79
+
docker build --target full -t browser-stream:full .
80
+
```
81
+
82
+
Configure with environment variables (for example in `.env`):
83
+
84
+
```bash
85
+
WEBSITE_URL=https://example.com
86
+
# Option A: full output URL
87
+
OUTPUT=rtmp://live.example.com/app/mystream
88
+
89
+
# Option B: split URL + key (used when OUTPUT is empty)
90
+
RTMP_URL=rtmp://live.example.com/app
91
+
STREAM_KEY=mystream
92
+
93
+
WIDTH=1920
94
+
HEIGHT=1080
95
+
FPS=30
96
+
BITRATE_KBPS=4500
97
+
KEYINT_SEC=1
98
+
X264_OPTS=bframes=0
99
+
RETRIES=5
100
+
RETRY_BACKOFF_MS=1000
101
+
STARTUP_DELAY_MS=2000
102
+
FRAME_TIMEOUT_MS=30000
103
+
NO_AUDIO=0
104
+
VERBOSE=0
105
+
```
106
+
107
+
Binary resolution in containers:
108
+
109
+
- `slim`: auto-detects system `chromium`/`chromium-browser` and `ffmpeg`.
110
+
- `full`: uses bundled sidecars via `BROWSER_STREAM_CHROMIUM_PATH` and `BROWSER_STREAM_FFMPEG_PATH`.
111
+
112
+
No sidecar downloads are required for either image.
113
+
Both images set `BROWSER_STREAM_NO_SANDBOX=1` for Chromium compatibility in containers.
114
+
115
+
## Defaults
116
+
117
+
- `width=1920`
118
+
- `height=1080`
119
+
- `fps=30`
120
+
- `bitrate-kbps=4500`
121
+
- `keyint-sec=1`
122
+
- `x264-opts=bframes=0`
123
+
- `retries=5`
124
+
- `retry-backoff-ms=1000`
125
+
- `startup-delay-ms=2000`
126
+
- `frame-timeout-ms=30000`
127
+
- `no-audio=false` (silent audio track enabled by default)
128
+
129
+
## Notes
130
+
131
+
- v1 supports public HTTP(S) website URLs.
132
+
- A silent audio track is included by default for RTMP compatibility.
133
+
- Use `--no-audio` to disable audio.
134
+
- RTMP output supports `rtmp://` and `rtmps://`.
135
+
- On stream failure, the app retries a fixed number of times and exits non-zero once exhausted.
136
+
- Runtime controls while streaming:
137
+
- type `r` or `refresh` then press Enter to manually reload the page.
138
+
- type `h` or `help` then press Enter to print controls.
139
+
- with compose, run foreground (`docker compose up`) to send commands directly via stdin.
140
+
141
+
## GitHub Release Bundling
142
+
143
+
Workflow: `.github/workflows/build-and-release.yml`
144
+
145
+
- Builds release binaries for:
146
+
- macOS arm64
147
+
- Linux x86_64
148
+
- Windows x86_64
149
+
- Downloads platform sidecars and packages archives in this layout:
150
+
- `bin/browser-stream[.exe]`
151
+
- `sidecar/chromium/headless_shell[.exe]`
152
+
- `sidecar/ffmpeg/ffmpeg[.exe]`
153
+
- Uploads build artifacts on PR/push.
154
+
- Publishes release assets automatically on tags like `v0.1.0`.
155
+
156
+
## GHCR Docker Publish
157
+
158
+
Workflow: `.github/workflows/docker-publish.yml`
159
+
160
+
- Builds and publishes both Docker variants:
161
+
- `slim`: `linux/amd64` and `linux/arm64`
162
+
- `full`: `linux/amd64` (sidecar availability)
163
+
- Publishes to GitHub Container Registry on push to `main` and version tags (`v*`).
164
+
- Uses image name:
165
+
- `ghcr.io/<owner>/<repo>`
166
+
- Example for this repo: `ghcr.io/mmattbtw/browser-stream`
167
+
- Tags include:
168
+
- `latest-slim` / `latest-full` (default branch)
169
+
- branch/tag refs with suffixes (`-slim`, `-full`)
170
+
- `sha-<commit>-slim` / `sha-<commit>-full`
171
+
172
+
Example:
173
+
174
+
```bash
175
+
docker pull ghcr.io/mmattbtw/browser-stream:latest-slim
176
+
docker pull ghcr.io/mmattbtw/browser-stream:latest-full
177
+
```
+42
docker-compose.yml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
x-browser-stream-env: &browser-stream-env
2
+
WEBSITE_URL: ${WEBSITE_URL:-https://example.com}
3
+
OUTPUT: ${OUTPUT:-}
4
+
RTMP_URL: ${RTMP_URL:-rtmp://live.example.com/app}
5
+
STREAM_KEY: ${STREAM_KEY:-changeme}
6
+
WIDTH: ${WIDTH:-1920}
7
+
HEIGHT: ${HEIGHT:-1080}
8
+
FPS: ${FPS:-30}
9
+
BITRATE_KBPS: ${BITRATE_KBPS:-4500}
10
+
KEYINT_SEC: ${KEYINT_SEC:-1}
11
+
X264_OPTS: ${X264_OPTS:-bframes=0}
12
+
RETRIES: ${RETRIES:-5}
13
+
RETRY_BACKOFF_MS: ${RETRY_BACKOFF_MS:-1000}
14
+
STARTUP_DELAY_MS: ${STARTUP_DELAY_MS:-2000}
15
+
FRAME_TIMEOUT_MS: ${FRAME_TIMEOUT_MS:-30000}
16
+
NO_AUDIO: ${NO_AUDIO:-0}
17
+
VERBOSE: ${VERBOSE:-0}
18
+
19
+
x-browser-stream-common: &browser-stream-common
20
+
restart: unless-stopped
21
+
environment: *browser-stream-env
22
+
stdin_open: true
23
+
tty: true
24
+
25
+
services:
26
+
browser-stream:
27
+
<<: *browser-stream-common
28
+
build:
29
+
context: .
30
+
dockerfile: Dockerfile
31
+
target: slim
32
+
image: browser-stream:slim
33
+
34
+
browser-stream-full:
35
+
<<: *browser-stream-common
36
+
build:
37
+
context: .
38
+
dockerfile: Dockerfile
39
+
target: full
40
+
image: browser-stream:full
41
+
profiles:
42
+
- full
+74
docker/entrypoint.sh
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
#!/usr/bin/env bash
2
+
set -euo pipefail
3
+
4
+
if [[ -z "${WEBSITE_URL:-}" ]]; then
5
+
echo "WEBSITE_URL is required" >&2
6
+
exit 1
7
+
fi
8
+
9
+
if [[ -z "${OUTPUT:-}" && ( -z "${RTMP_URL:-}" || -z "${STREAM_KEY:-}" ) ]]; then
10
+
echo "Provide either OUTPUT or both RTMP_URL and STREAM_KEY" >&2
11
+
exit 1
12
+
fi
13
+
14
+
if [[ -n "${BROWSER_STREAM_CHROMIUM_PATH:-}" ]]; then
15
+
CHROMIUM_BIN="${BROWSER_STREAM_CHROMIUM_PATH}"
16
+
elif command -v chromium >/dev/null 2>&1; then
17
+
CHROMIUM_BIN="$(command -v chromium)"
18
+
elif command -v chromium-browser >/dev/null 2>&1; then
19
+
CHROMIUM_BIN="$(command -v chromium-browser)"
20
+
else
21
+
echo "Could not find chromium binary in container" >&2
22
+
exit 1
23
+
fi
24
+
25
+
if [[ ! -x "${CHROMIUM_BIN}" ]]; then
26
+
echo "Configured chromium binary is not executable: ${CHROMIUM_BIN}" >&2
27
+
exit 1
28
+
fi
29
+
30
+
if [[ -n "${BROWSER_STREAM_FFMPEG_PATH:-}" ]]; then
31
+
FFMPEG_BIN="${BROWSER_STREAM_FFMPEG_PATH}"
32
+
elif command -v ffmpeg >/dev/null 2>&1; then
33
+
FFMPEG_BIN="$(command -v ffmpeg)"
34
+
else
35
+
echo "Could not find ffmpeg binary in container" >&2
36
+
exit 1
37
+
fi
38
+
39
+
if [[ ! -x "${FFMPEG_BIN}" ]]; then
40
+
echo "Configured ffmpeg binary is not executable: ${FFMPEG_BIN}" >&2
41
+
exit 1
42
+
fi
43
+
44
+
args=(
45
+
--url "${WEBSITE_URL}"
46
+
--width "${WIDTH:-1920}"
47
+
--height "${HEIGHT:-1080}"
48
+
--fps "${FPS:-30}"
49
+
--bitrate-kbps "${BITRATE_KBPS:-4500}"
50
+
--keyint-sec "${KEYINT_SEC:-1}"
51
+
--x264-opts "${X264_OPTS:-bframes=0}"
52
+
--retries "${RETRIES:-5}"
53
+
--retry-backoff-ms "${RETRY_BACKOFF_MS:-1000}"
54
+
--startup-delay-ms "${STARTUP_DELAY_MS:-2000}"
55
+
--frame-timeout-ms "${FRAME_TIMEOUT_MS:-30000}"
56
+
--chromium-path "${CHROMIUM_BIN}"
57
+
--ffmpeg-path "${FFMPEG_BIN}"
58
+
)
59
+
60
+
if [[ -n "${OUTPUT:-}" ]]; then
61
+
args+=(--output "${OUTPUT}")
62
+
else
63
+
args+=(--rtmp-url "${RTMP_URL}" --stream-key "${STREAM_KEY}")
64
+
fi
65
+
66
+
if [[ "${VERBOSE:-0}" == "1" || "${VERBOSE:-false}" == "true" ]]; then
67
+
args+=(--verbose)
68
+
fi
69
+
70
+
if [[ "${NO_AUDIO:-0}" == "1" || "${NO_AUDIO:-false}" == "true" ]]; then
71
+
args+=(--no-audio)
72
+
fi
73
+
74
+
exec /usr/local/bin/browser-stream "${args[@]}" "$@"
+60
scripts/fetch-sidecars.ps1
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
param(
2
+
[string]$Destination = "target/sidecar"
3
+
)
4
+
5
+
$ErrorActionPreference = "Stop"
6
+
7
+
Write-Host "Fetching Chromium headless shell and FFmpeg sidecars..."
8
+
9
+
New-Item -ItemType Directory -Force -Path "$Destination/chromium" | Out-Null
10
+
New-Item -ItemType Directory -Force -Path "$Destination/ffmpeg" | Out-Null
11
+
12
+
$tmpRoot = Join-Path $env:TEMP ("browser-stream-sidecars-" + [guid]::NewGuid().ToString())
13
+
New-Item -ItemType Directory -Path $tmpRoot | Out-Null
14
+
15
+
try {
16
+
$manifestUrl = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
17
+
$manifest = Invoke-RestMethod -Uri $manifestUrl
18
+
19
+
$chromiumEntry = $manifest.channels.Stable.downloads.'chrome-headless-shell' |
20
+
Where-Object { $_.platform -eq 'win64' } |
21
+
Select-Object -First 1
22
+
23
+
if (-not $chromiumEntry) {
24
+
throw "Could not resolve chrome-headless-shell download URL for win64"
25
+
}
26
+
27
+
$chromiumZip = Join-Path $tmpRoot "chromium.zip"
28
+
Invoke-WebRequest -Uri $chromiumEntry.url -OutFile $chromiumZip
29
+
Expand-Archive -Path $chromiumZip -DestinationPath (Join-Path $tmpRoot "chromium") -Force
30
+
31
+
$chromiumExe = Get-ChildItem -Path (Join-Path $tmpRoot "chromium") -Recurse -File -Filter "chrome-headless-shell.exe" | Select-Object -First 1
32
+
if (-not $chromiumExe) {
33
+
throw "Could not find chrome-headless-shell.exe in downloaded Chromium archive"
34
+
}
35
+
36
+
$chromiumRoot = $chromiumExe.Directory.FullName
37
+
Copy-Item -Path (Join-Path $chromiumRoot '*') -Destination (Join-Path $Destination "chromium") -Recurse -Force
38
+
Copy-Item -Path $chromiumExe.FullName -Destination (Join-Path $Destination "chromium/headless_shell.exe") -Force
39
+
40
+
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
41
+
$ffmpegZip = Join-Path $tmpRoot "ffmpeg.zip"
42
+
Invoke-WebRequest -Uri $ffmpegUrl -OutFile $ffmpegZip
43
+
Expand-Archive -Path $ffmpegZip -DestinationPath (Join-Path $tmpRoot "ffmpeg") -Force
44
+
45
+
$ffmpegExe = Get-ChildItem -Path (Join-Path $tmpRoot "ffmpeg") -Recurse -File -Filter "ffmpeg.exe" | Select-Object -First 1
46
+
if (-not $ffmpegExe) {
47
+
throw "Could not find ffmpeg.exe in downloaded FFmpeg archive"
48
+
}
49
+
50
+
Copy-Item -Path $ffmpegExe.FullName -Destination (Join-Path $Destination "ffmpeg/ffmpeg.exe") -Force
51
+
52
+
Write-Host "Sidecars installed:"
53
+
Write-Host " Chromium: $Destination/chromium/headless_shell.exe"
54
+
Write-Host " FFmpeg: $Destination/ffmpeg/ffmpeg.exe"
55
+
}
56
+
finally {
57
+
if (Test-Path $tmpRoot) {
58
+
Remove-Item -Path $tmpRoot -Recurse -Force
59
+
}
60
+
}
+115
scripts/fetch-sidecars.sh
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
#!/usr/bin/env bash
2
+
set -euo pipefail
3
+
4
+
DESTINATION="target/sidecar"
5
+
6
+
while [[ $# -gt 0 ]]; do
7
+
case "$1" in
8
+
--destination)
9
+
DESTINATION="$2"
10
+
shift 2
11
+
;;
12
+
-h|--help)
13
+
cat <<'USAGE'
14
+
Fetch bundled Chromium Headless Shell and FFmpeg sidecars for the current platform.
15
+
16
+
Usage:
17
+
./scripts/fetch-sidecars.sh [--destination <dir>]
18
+
19
+
Defaults:
20
+
destination: target/sidecar
21
+
USAGE
22
+
exit 0
23
+
;;
24
+
*)
25
+
echo "Unknown argument: $1" >&2
26
+
exit 1
27
+
;;
28
+
esac
29
+
done
30
+
31
+
if ! command -v curl >/dev/null 2>&1; then
32
+
echo "curl is required" >&2
33
+
exit 1
34
+
fi
35
+
36
+
if ! command -v jq >/dev/null 2>&1; then
37
+
echo "jq is required" >&2
38
+
exit 1
39
+
fi
40
+
41
+
if ! command -v unzip >/dev/null 2>&1; then
42
+
echo "unzip is required" >&2
43
+
exit 1
44
+
fi
45
+
46
+
uname_s="$(uname -s)"
47
+
uname_m="$(uname -m)"
48
+
49
+
chromium_platform=""
50
+
ffmpeg_url=""
51
+
chromium_exec_name="chrome-headless-shell"
52
+
ffmpeg_exec_name="ffmpeg"
53
+
54
+
case "${uname_s}:${uname_m}" in
55
+
Darwin:arm64)
56
+
chromium_platform="mac-arm64"
57
+
ffmpeg_url="https://ffmpeg.martin-riedl.de/redirect/latest/macos/arm64/release/ffmpeg.zip"
58
+
;;
59
+
Linux:x86_64)
60
+
chromium_platform="linux64"
61
+
ffmpeg_url="https://ffmpeg.martin-riedl.de/redirect/latest/linux/amd64/release/ffmpeg.zip"
62
+
;;
63
+
*)
64
+
echo "Unsupported platform ${uname_s}/${uname_m}. This script supports macOS arm64 and Linux x86_64." >&2
65
+
exit 1
66
+
;;
67
+
esac
68
+
69
+
chrome_manifest_url="https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
70
+
chromium_url="$(curl -fsSL "$chrome_manifest_url" | jq -r --arg platform "$chromium_platform" '.channels.Stable.downloads["chrome-headless-shell"][] | select(.platform == $platform) | .url')"
71
+
72
+
if [[ -z "$chromium_url" || "$chromium_url" == "null" ]]; then
73
+
echo "Failed to resolve Chromium headless shell URL for platform ${chromium_platform}" >&2
74
+
exit 1
75
+
fi
76
+
77
+
tmp_dir="$(mktemp -d)"
78
+
trap 'rm -rf "$tmp_dir"' EXIT
79
+
80
+
mkdir -p "$DESTINATION/chromium" "$DESTINATION/ffmpeg"
81
+
82
+
chromium_zip="$tmp_dir/chromium.zip"
83
+
ffmpeg_zip="$tmp_dir/ffmpeg.zip"
84
+
85
+
printf 'Downloading Chromium headless shell (%s)\n' "$chromium_platform"
86
+
curl -fL "$chromium_url" -o "$chromium_zip"
87
+
unzip -q "$chromium_zip" -d "$tmp_dir/chromium"
88
+
89
+
chromium_source="$(find "$tmp_dir/chromium" -type f -name "$chromium_exec_name" | head -n 1)"
90
+
if [[ -z "$chromium_source" ]]; then
91
+
echo "Could not find ${chromium_exec_name} inside Chromium archive" >&2
92
+
exit 1
93
+
fi
94
+
95
+
chromium_root="$(dirname "$chromium_source")"
96
+
cp -R "$chromium_root"/. "$DESTINATION/chromium/"
97
+
cp "$chromium_source" "$DESTINATION/chromium/headless_shell"
98
+
chmod +x "$DESTINATION/chromium/headless_shell"
99
+
100
+
printf 'Downloading FFmpeg\n'
101
+
curl -fL "$ffmpeg_url" -o "$ffmpeg_zip"
102
+
unzip -q "$ffmpeg_zip" -d "$tmp_dir/ffmpeg"
103
+
104
+
ffmpeg_source="$(find "$tmp_dir/ffmpeg" -type f -name "$ffmpeg_exec_name" | head -n 1)"
105
+
if [[ -z "$ffmpeg_source" ]]; then
106
+
echo "Could not find ${ffmpeg_exec_name} inside FFmpeg archive" >&2
107
+
exit 1
108
+
fi
109
+
110
+
cp "$ffmpeg_source" "$DESTINATION/ffmpeg/ffmpeg"
111
+
chmod +x "$DESTINATION/ffmpeg/ffmpeg"
112
+
113
+
printf 'Sidecars installed:\n'
114
+
printf ' Chromium: %s\n' "$DESTINATION/chromium/headless_shell"
115
+
printf ' FFmpeg: %s\n' "$DESTINATION/ffmpeg/ffmpeg"
+289
src/chromium.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::path::{Path, PathBuf};
2
+
use std::time::Duration;
3
+
4
+
use anyhow::{Context, Result, anyhow};
5
+
use chromiumoxide::browser::{Browser, BrowserConfig};
6
+
use chromiumoxide::cdp::browser_protocol::page::{
7
+
EventScreencastFrame, ScreencastFrameAckParams, StartScreencastFormat, StartScreencastParams,
8
+
StopScreencastParams,
9
+
};
10
+
use chromiumoxide::handler::viewport::Viewport;
11
+
use futures::StreamExt;
12
+
use tokio::io::{AsyncBufReadExt, BufReader};
13
+
use tokio::sync::mpsc;
14
+
use tokio::time::MissedTickBehavior;
15
+
use tracing::{debug, error, info, warn};
16
+
17
+
use crate::cli::AppConfig;
18
+
use crate::encoder::FfmpegEncoder;
19
+
use crate::error::RuntimeError;
20
+
use crate::frame::{RgbFrame, decode_screencast_frame};
21
+
22
+
pub async fn stream_browser_to_encoder(
23
+
config: &AppConfig,
24
+
chromium_path: &Path,
25
+
encoder: &mut FfmpegEncoder,
26
+
) -> Result<()> {
27
+
let viewport = Viewport {
28
+
width: config.width,
29
+
height: config.height,
30
+
device_scale_factor: Some(1.0),
31
+
emulating_mobile: false,
32
+
is_landscape: config.width >= config.height,
33
+
has_touch: false,
34
+
};
35
+
36
+
let mut browser_builder = BrowserConfig::builder()
37
+
.chrome_executable(chromium_path)
38
+
.window_size(config.width, config.height)
39
+
.new_headless_mode()
40
+
.viewport(viewport)
41
+
.arg("--autoplay-policy=no-user-gesture-required")
42
+
.arg("--disable-background-timer-throttling")
43
+
.arg("--disable-backgrounding-occluded-windows")
44
+
.arg("--disable-renderer-backgrounding");
45
+
46
+
if no_sandbox_from_env() {
47
+
browser_builder = browser_builder.no_sandbox();
48
+
}
49
+
50
+
let browser_config = browser_builder
51
+
.build()
52
+
.map_err(|err| anyhow!("failed to build browser config: {err}"))?;
53
+
54
+
info!(chromium = %chromium_path.display(), "starting chromium");
55
+
56
+
let (mut browser, mut handler) = Browser::launch(browser_config)
57
+
.await
58
+
.context("failed to launch chromium")?;
59
+
60
+
let handler_task = tokio::spawn(async move {
61
+
while let Some(item) = handler.next().await {
62
+
if let Err(err) = item {
63
+
error!("chromium handler error: {err}");
64
+
break;
65
+
}
66
+
}
67
+
});
68
+
69
+
let page = browser
70
+
.new_page("about:blank")
71
+
.await
72
+
.context("failed to create page")?;
73
+
74
+
page.goto(config.website_url.as_str())
75
+
.await
76
+
.with_context(|| format!("failed loading {}", config.website_url))?;
77
+
78
+
// `goto` waits for page load completion. Delay further for dynamic JS/CSS settling.
79
+
tokio::time::sleep(Duration::from_millis(config.startup_delay_ms)).await;
80
+
81
+
let mut frame_events = page
82
+
.event_listener::<EventScreencastFrame>()
83
+
.await
84
+
.context("failed to register screencast event listener")?;
85
+
86
+
let start_params = StartScreencastParams::builder()
87
+
.format(StartScreencastFormat::Jpeg)
88
+
.quality(80_i64)
89
+
.max_width(i64::from(config.width))
90
+
.max_height(i64::from(config.height))
91
+
.every_nth_frame(1_i64)
92
+
.build();
93
+
94
+
page.execute(start_params)
95
+
.await
96
+
.context("failed to start screencast")?;
97
+
98
+
info!("runtime controls: type `r` then Enter to refresh the page");
99
+
100
+
let mut control_rx = spawn_control_listener();
101
+
let frame_interval = Duration::from_secs_f64(1.0_f64 / f64::from(config.fps));
102
+
let mut frame_tick = tokio::time::interval(frame_interval);
103
+
frame_tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
104
+
frame_tick.tick().await;
105
+
let mut stats_tick = tokio::time::interval(Duration::from_secs(5));
106
+
stats_tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
107
+
stats_tick.tick().await;
108
+
109
+
let first_frame_timeout = tokio::time::sleep(Duration::from_millis(config.frame_timeout_ms));
110
+
tokio::pin!(first_frame_timeout);
111
+
let mut latest_frame: Option<RgbFrame> = None;
112
+
let mut decoded_frames: u64 = 0;
113
+
let mut encoded_frames: u64 = 0;
114
+
115
+
let stream_result: Result<()> = async {
116
+
loop {
117
+
tokio::select! {
118
+
biased;
119
+
_ = frame_tick.tick() => {
120
+
if let Some(frame) = latest_frame.as_ref() {
121
+
encoder.write_frame(frame).await?;
122
+
encoded_frames = encoded_frames.saturating_add(1);
123
+
}
124
+
}
125
+
maybe_event = frame_events.next() => {
126
+
let event = maybe_event.context("screencast event stream ended unexpectedly")?;
127
+
128
+
page.execute(ScreencastFrameAckParams::new(event.session_id))
129
+
.await
130
+
.context("failed to ack screencast frame")?;
131
+
132
+
let frame = decode_screencast_frame(event.data.as_ref(), config.width, config.height)
133
+
.context("failed to decode screencast frame")?;
134
+
135
+
if latest_frame.is_none() {
136
+
info!("received first screencast frame");
137
+
// Prime ffmpeg immediately so it can initialize output without waiting for the first tick.
138
+
encoder.write_frame(&frame).await?;
139
+
encoded_frames = encoded_frames.saturating_add(1);
140
+
}
141
+
decoded_frames = decoded_frames.saturating_add(1);
142
+
latest_frame = Some(frame);
143
+
}
144
+
_ = stats_tick.tick() => {
145
+
debug!(
146
+
decoded_frames,
147
+
encoded_frames,
148
+
has_frame = latest_frame.is_some(),
149
+
"streaming stats"
150
+
);
151
+
}
152
+
command = control_rx.recv() => {
153
+
match command {
154
+
Some(ControlCommand::Refresh) => {
155
+
page.reload()
156
+
.await
157
+
.context("manual refresh failed")?;
158
+
info!("manual refresh applied");
159
+
}
160
+
Some(ControlCommand::Help) => {
161
+
info!("runtime controls: `r` or `refresh` reloads the page");
162
+
}
163
+
None => {
164
+
// stdin closed; continue streaming without runtime controls.
165
+
}
166
+
}
167
+
}
168
+
_ = tokio::signal::ctrl_c() => {
169
+
return Err(RuntimeError::ShutdownRequested.into());
170
+
}
171
+
_ = &mut first_frame_timeout, if latest_frame.is_none() => {
172
+
return Err(RuntimeError::ScreencastTimeout.into());
173
+
}
174
+
}
175
+
}
176
+
}
177
+
.await;
178
+
179
+
if let Err(err) = page.execute(StopScreencastParams::default()).await {
180
+
warn!("failed to stop screencast cleanly: {err}");
181
+
}
182
+
183
+
if let Err(err) = browser.close().await {
184
+
warn!("failed to close browser cleanly: {err}");
185
+
}
186
+
if let Err(err) = browser.wait().await {
187
+
warn!("failed to wait for browser process: {err}");
188
+
}
189
+
190
+
handler_task.abort();
191
+
192
+
stream_result
193
+
}
194
+
195
+
fn no_sandbox_from_env() -> bool {
196
+
match std::env::var("BROWSER_STREAM_NO_SANDBOX") {
197
+
Ok(value) => parse_truthy(&value),
198
+
Err(_) => false,
199
+
}
200
+
}
201
+
202
+
fn parse_truthy(value: &str) -> bool {
203
+
matches!(
204
+
value.trim().to_ascii_lowercase().as_str(),
205
+
"1" | "true" | "yes"
206
+
)
207
+
}
208
+
209
+
pub fn chromium_executable_name() -> &'static str {
210
+
if cfg!(target_os = "windows") {
211
+
"headless_shell.exe"
212
+
} else {
213
+
"headless_shell"
214
+
}
215
+
}
216
+
217
+
pub fn default_chromium_sidecar_path(exe_dir: &Path) -> PathBuf {
218
+
exe_dir
219
+
.join("..")
220
+
.join("sidecar")
221
+
.join("chromium")
222
+
.join(chromium_executable_name())
223
+
}
224
+
225
+
#[derive(Debug, Copy, Clone)]
226
+
enum ControlCommand {
227
+
Refresh,
228
+
Help,
229
+
}
230
+
231
+
fn parse_control_command(input: &str) -> Option<ControlCommand> {
232
+
match input.trim().to_ascii_lowercase().as_str() {
233
+
"r" | "refresh" => Some(ControlCommand::Refresh),
234
+
"h" | "help" => Some(ControlCommand::Help),
235
+
_ => None,
236
+
}
237
+
}
238
+
239
+
fn spawn_control_listener() -> mpsc::UnboundedReceiver<ControlCommand> {
240
+
let (tx, rx) = mpsc::unbounded_channel();
241
+
242
+
tokio::spawn(async move {
243
+
let mut lines = BufReader::new(tokio::io::stdin()).lines();
244
+
while let Ok(Some(line)) = lines.next_line().await {
245
+
if let Some(command) = parse_control_command(&line) {
246
+
if tx.send(command).is_err() {
247
+
break;
248
+
}
249
+
}
250
+
}
251
+
});
252
+
253
+
rx
254
+
}
255
+
256
+
#[cfg(test)]
257
+
mod tests {
258
+
use super::{ControlCommand, parse_control_command, parse_truthy};
259
+
260
+
#[test]
261
+
fn parses_refresh_shortcut() {
262
+
assert!(matches!(
263
+
parse_control_command("r"),
264
+
Some(ControlCommand::Refresh)
265
+
));
266
+
}
267
+
268
+
#[test]
269
+
fn parses_refresh_word() {
270
+
assert!(matches!(
271
+
parse_control_command(" refresh "),
272
+
Some(ControlCommand::Refresh)
273
+
));
274
+
}
275
+
276
+
#[test]
277
+
fn ignores_unknown_commands() {
278
+
assert!(parse_control_command("noop").is_none());
279
+
}
280
+
281
+
#[test]
282
+
fn truthy_parser() {
283
+
assert!(parse_truthy("true"));
284
+
assert!(parse_truthy("1"));
285
+
assert!(parse_truthy("YES"));
286
+
assert!(!parse_truthy("0"));
287
+
assert!(!parse_truthy("false"));
288
+
}
289
+
}
+144
src/cli.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::path::PathBuf;
2
+
3
+
use clap::Parser;
4
+
use url::Url;
5
+
6
+
use crate::error::ConfigError;
7
+
8
+
#[derive(Debug, Parser, Clone)]
9
+
#[command(
10
+
name = "browser-stream",
11
+
version,
12
+
about = "Stream a browser page to RTMP"
13
+
)]
14
+
pub struct CliArgs {
15
+
#[arg(long)]
16
+
pub url: String,
17
+
18
+
#[arg(long, default_value_t = 1920)]
19
+
pub width: u32,
20
+
21
+
#[arg(long, default_value_t = 1080)]
22
+
pub height: u32,
23
+
24
+
#[arg(long, default_value_t = 30)]
25
+
pub fps: u32,
26
+
27
+
#[arg(long, default_value_t = 4500)]
28
+
pub bitrate_kbps: u32,
29
+
30
+
#[arg(long, default_value_t = 1)]
31
+
pub keyint_sec: u32,
32
+
33
+
#[arg(long, default_value = "bframes=0")]
34
+
pub x264_opts: String,
35
+
36
+
#[arg(long)]
37
+
pub rtmp_url: Option<String>,
38
+
39
+
#[arg(long)]
40
+
pub stream_key: Option<String>,
41
+
42
+
#[arg(long)]
43
+
pub output: Option<String>,
44
+
45
+
#[arg(long, default_value_t = 5)]
46
+
pub retries: u32,
47
+
48
+
#[arg(long, default_value_t = 1000)]
49
+
pub retry_backoff_ms: u64,
50
+
51
+
#[arg(long, default_value_t = 2000)]
52
+
pub startup_delay_ms: u64,
53
+
54
+
#[arg(long, default_value_t = 30000)]
55
+
pub frame_timeout_ms: u64,
56
+
57
+
#[arg(long, default_value_t = false)]
58
+
pub no_audio: bool,
59
+
60
+
#[arg(long)]
61
+
pub ffmpeg_path: Option<PathBuf>,
62
+
63
+
#[arg(long)]
64
+
pub chromium_path: Option<PathBuf>,
65
+
66
+
#[arg(long, default_value_t = false)]
67
+
pub verbose: bool,
68
+
}
69
+
70
+
#[derive(Debug, Clone)]
71
+
pub struct AppConfig {
72
+
pub website_url: Url,
73
+
pub width: u32,
74
+
pub height: u32,
75
+
pub fps: u32,
76
+
pub bitrate_kbps: u32,
77
+
pub keyint_sec: u32,
78
+
pub x264_opts: String,
79
+
pub output: String,
80
+
pub retries: u32,
81
+
pub retry_backoff_ms: u64,
82
+
pub startup_delay_ms: u64,
83
+
pub frame_timeout_ms: u64,
84
+
pub no_audio: bool,
85
+
pub ffmpeg_path: Option<PathBuf>,
86
+
pub chromium_path: Option<PathBuf>,
87
+
pub verbose: bool,
88
+
}
89
+
90
+
impl CliArgs {
91
+
pub fn into_config(self) -> Result<AppConfig, ConfigError> {
92
+
validate_range("width", self.width as u64, 16, u32::MAX as u64)?;
93
+
validate_range("height", self.height as u64, 16, u32::MAX as u64)?;
94
+
validate_range("fps", self.fps as u64, 1, 120)?;
95
+
validate_range(
96
+
"bitrate-kbps",
97
+
self.bitrate_kbps as u64,
98
+
100,
99
+
u32::MAX as u64,
100
+
)?;
101
+
validate_range("keyint-sec", self.keyint_sec as u64, 1, 60)?;
102
+
validate_range("frame-timeout-ms", self.frame_timeout_ms, 1000, u64::MAX)?;
103
+
104
+
let website_url =
105
+
Url::parse(&self.url).map_err(|_| ConfigError::InvalidWebsiteUrl(self.url.clone()))?;
106
+
match website_url.scheme() {
107
+
"http" | "https" => {}
108
+
other => return Err(ConfigError::UnsupportedWebsiteScheme(other.to_string())),
109
+
}
110
+
111
+
let output = crate::rtmp::build_output(self.output, self.rtmp_url, self.stream_key)?;
112
+
113
+
Ok(AppConfig {
114
+
website_url,
115
+
width: self.width,
116
+
height: self.height,
117
+
fps: self.fps,
118
+
bitrate_kbps: self.bitrate_kbps,
119
+
keyint_sec: self.keyint_sec,
120
+
x264_opts: self.x264_opts,
121
+
output,
122
+
retries: self.retries,
123
+
retry_backoff_ms: self.retry_backoff_ms,
124
+
startup_delay_ms: self.startup_delay_ms,
125
+
frame_timeout_ms: self.frame_timeout_ms,
126
+
no_audio: self.no_audio,
127
+
ffmpeg_path: self.ffmpeg_path,
128
+
chromium_path: self.chromium_path,
129
+
verbose: self.verbose,
130
+
})
131
+
}
132
+
}
133
+
134
+
fn validate_range(field: &'static str, actual: u64, min: u64, max: u64) -> Result<(), ConfigError> {
135
+
if actual < min || actual > max {
136
+
return Err(ConfigError::OutOfRange {
137
+
field,
138
+
min,
139
+
max,
140
+
actual,
141
+
});
142
+
}
143
+
Ok(())
144
+
}
+217
src/encoder.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::path::{Path, PathBuf};
2
+
use std::process::ExitStatus;
3
+
4
+
use anyhow::{Context, Result, bail};
5
+
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
6
+
use tokio::process::{Child, ChildStdin, Command};
7
+
use tokio::task::JoinHandle;
8
+
use tracing::{debug, info, warn};
9
+
10
+
use crate::frame::RgbFrame;
11
+
12
+
#[derive(Debug, Clone)]
13
+
pub struct EncoderSettings {
14
+
pub width: u32,
15
+
pub height: u32,
16
+
pub fps: u32,
17
+
pub bitrate_kbps: u32,
18
+
pub keyint_sec: u32,
19
+
pub x264_opts: String,
20
+
pub output: String,
21
+
pub include_silent_audio: bool,
22
+
pub ffmpeg_path: PathBuf,
23
+
}
24
+
25
+
pub fn build_ffmpeg_args(settings: &EncoderSettings) -> Vec<String> {
26
+
build_ffmpeg_args_with_loglevel(settings, "warning")
27
+
}
28
+
29
+
pub fn build_ffmpeg_args_with_loglevel(settings: &EncoderSettings, loglevel: &str) -> Vec<String> {
30
+
let keyint = settings.fps.saturating_mul(settings.keyint_sec).max(1);
31
+
let bufsize = settings.bitrate_kbps.saturating_mul(2);
32
+
33
+
let mut args = vec![
34
+
"-hide_banner".to_string(),
35
+
"-loglevel".to_string(),
36
+
loglevel.to_string(),
37
+
"-stats_period".to_string(),
38
+
"5".to_string(),
39
+
"-stats".to_string(),
40
+
"-f".to_string(),
41
+
"rawvideo".to_string(),
42
+
"-pix_fmt".to_string(),
43
+
"rgb24".to_string(),
44
+
"-s".to_string(),
45
+
format!("{}x{}", settings.width, settings.height),
46
+
"-r".to_string(),
47
+
settings.fps.to_string(),
48
+
"-i".to_string(),
49
+
"-".to_string(),
50
+
];
51
+
52
+
if settings.include_silent_audio {
53
+
args.extend([
54
+
"-f".to_string(),
55
+
"lavfi".to_string(),
56
+
"-i".to_string(),
57
+
"anullsrc=r=48000:cl=stereo".to_string(),
58
+
]);
59
+
}
60
+
61
+
args.extend([
62
+
"-c:v".to_string(),
63
+
"libx264".to_string(),
64
+
"-preset".to_string(),
65
+
"veryfast".to_string(),
66
+
"-pix_fmt".to_string(),
67
+
"yuv420p".to_string(),
68
+
"-b:v".to_string(),
69
+
format!("{}k", settings.bitrate_kbps),
70
+
"-maxrate".to_string(),
71
+
format!("{}k", settings.bitrate_kbps),
72
+
"-bufsize".to_string(),
73
+
format!("{}k", bufsize),
74
+
"-g".to_string(),
75
+
keyint.to_string(),
76
+
"-keyint_min".to_string(),
77
+
keyint.to_string(),
78
+
"-x264-params".to_string(),
79
+
settings.x264_opts.clone(),
80
+
]);
81
+
82
+
if settings.include_silent_audio {
83
+
args.extend([
84
+
"-c:a".to_string(),
85
+
"aac".to_string(),
86
+
"-b:a".to_string(),
87
+
"128k".to_string(),
88
+
"-ar".to_string(),
89
+
"48000".to_string(),
90
+
"-ac".to_string(),
91
+
"2".to_string(),
92
+
]);
93
+
} else {
94
+
args.push("-an".to_string());
95
+
}
96
+
97
+
args.extend(["-f".to_string(), "flv".to_string(), settings.output.clone()]);
98
+
99
+
args
100
+
}
101
+
102
+
#[derive(Debug)]
103
+
pub struct FfmpegEncoder {
104
+
child: Child,
105
+
stdin: ChildStdin,
106
+
stderr_task: JoinHandle<()>,
107
+
}
108
+
109
+
impl FfmpegEncoder {
110
+
pub async fn spawn(settings: &EncoderSettings, verbose: bool) -> Result<Self> {
111
+
let args =
112
+
build_ffmpeg_args_with_loglevel(settings, if verbose { "info" } else { "warning" });
113
+
114
+
info!(
115
+
ffmpeg = %settings.ffmpeg_path.display(),
116
+
output = %settings.output,
117
+
"starting ffmpeg"
118
+
);
119
+
120
+
let mut cmd = Command::new(&settings.ffmpeg_path);
121
+
cmd.args(&args)
122
+
.stdin(std::process::Stdio::piped())
123
+
.stderr(std::process::Stdio::piped())
124
+
.stdout(std::process::Stdio::null());
125
+
126
+
let mut child = cmd.spawn().with_context(|| {
127
+
format!(
128
+
"failed to spawn ffmpeg from {}",
129
+
settings.ffmpeg_path.display()
130
+
)
131
+
})?;
132
+
133
+
let stdin = child.stdin.take().context("ffmpeg stdin unavailable")?;
134
+
135
+
let stderr = child.stderr.take().context("ffmpeg stderr unavailable")?;
136
+
137
+
let stderr_task = tokio::spawn(async move {
138
+
let mut lines = BufReader::new(stderr).lines();
139
+
while let Ok(Some(line)) = lines.next_line().await {
140
+
if verbose {
141
+
info!(target: "ffmpeg", "{line}");
142
+
} else {
143
+
debug!(target: "ffmpeg", "{line}");
144
+
}
145
+
}
146
+
});
147
+
148
+
Ok(Self {
149
+
child,
150
+
stdin,
151
+
stderr_task,
152
+
})
153
+
}
154
+
155
+
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
156
+
self.child
157
+
.try_wait()
158
+
.context("failed to poll ffmpeg process")
159
+
}
160
+
161
+
pub async fn write_frame(&mut self, frame: &RgbFrame) -> Result<()> {
162
+
if frame.width == 0 || frame.height == 0 {
163
+
bail!("invalid frame dimensions {}x{}", frame.width, frame.height);
164
+
}
165
+
166
+
if let Some(status) = self.try_wait()? {
167
+
bail!("ffmpeg exited early with status {status}");
168
+
}
169
+
170
+
self.stdin
171
+
.write_all(&frame.data)
172
+
.await
173
+
.context("failed writing frame to ffmpeg stdin")?;
174
+
175
+
Ok(())
176
+
}
177
+
178
+
pub async fn kill_and_wait(&mut self) {
179
+
match self.child.kill().await {
180
+
Ok(()) => {}
181
+
Err(err) => warn!("failed to kill ffmpeg: {err}"),
182
+
}
183
+
184
+
match self.child.wait().await {
185
+
Ok(status) => debug!("ffmpeg exited after kill: {status}"),
186
+
Err(err) => warn!("failed waiting on ffmpeg: {err}"),
187
+
}
188
+
}
189
+
190
+
pub async fn wait_for_exit(mut self) -> Result<ExitStatus> {
191
+
drop(self.stdin);
192
+
let status = self
193
+
.child
194
+
.wait()
195
+
.await
196
+
.context("failed waiting for ffmpeg exit")?;
197
+
198
+
self.stderr_task.abort();
199
+
Ok(status)
200
+
}
201
+
}
202
+
203
+
pub fn ffmpeg_executable_name() -> &'static str {
204
+
if cfg!(target_os = "windows") {
205
+
"ffmpeg.exe"
206
+
} else {
207
+
"ffmpeg"
208
+
}
209
+
}
210
+
211
+
pub fn default_ffmpeg_sidecar_path(exe_dir: &Path) -> PathBuf {
212
+
exe_dir
213
+
.join("..")
214
+
.join("sidecar")
215
+
.join("ffmpeg")
216
+
.join(ffmpeg_executable_name())
217
+
}
+32
src/error.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::path::PathBuf;
2
+
3
+
use thiserror::Error;
4
+
5
+
#[derive(Debug, Error)]
6
+
pub enum ConfigError {
7
+
#[error("website URL must use http or https, got `{0}`")]
8
+
UnsupportedWebsiteScheme(String),
9
+
#[error("invalid website URL `{0}`")]
10
+
InvalidWebsiteUrl(String),
11
+
#[error("`{field}` out of range: got {actual}, expected {min}..={max}")]
12
+
OutOfRange {
13
+
field: &'static str,
14
+
min: u64,
15
+
max: u64,
16
+
actual: u64,
17
+
},
18
+
#[error(transparent)]
19
+
Rtmp(#[from] crate::rtmp::RtmpError),
20
+
}
21
+
22
+
#[derive(Debug, Error)]
23
+
pub enum RuntimeError {
24
+
#[error("shutdown requested")]
25
+
ShutdownRequested,
26
+
#[error("timed out waiting for screencast frames")]
27
+
ScreencastTimeout,
28
+
#[error(
29
+
"missing sidecar binary `{name}` at `{path}`. Provide an explicit override path or place sidecars at this location. For local development, fetch sidecars with `./scripts/fetch-sidecars.sh` (macOS/Linux) or `./scripts/fetch-sidecars.ps1` (Windows). Supported packaged targets: macOS arm64, Linux x86_64, Windows x86_64"
30
+
)]
31
+
MissingSidecar { name: &'static str, path: PathBuf },
32
+
}
+37
src/frame.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use anyhow::{Context, Result};
2
+
use base64::Engine;
3
+
4
+
#[derive(Debug, Clone)]
5
+
pub struct RgbFrame {
6
+
pub width: u32,
7
+
pub height: u32,
8
+
pub data: Vec<u8>,
9
+
}
10
+
11
+
pub fn decode_screencast_frame(
12
+
encoded_data: &str,
13
+
target_width: u32,
14
+
target_height: u32,
15
+
) -> Result<RgbFrame> {
16
+
let bytes = base64::engine::general_purpose::STANDARD
17
+
.decode(encoded_data)
18
+
.context("failed to decode CDP frame payload")?;
19
+
20
+
let img = image::load_from_memory(&bytes).context("failed to decode image bytes")?;
21
+
let normalized = if img.width() == target_width && img.height() == target_height {
22
+
img
23
+
} else {
24
+
img.resize_exact(
25
+
target_width,
26
+
target_height,
27
+
image::imageops::FilterType::Triangle,
28
+
)
29
+
};
30
+
31
+
let rgb = normalized.to_rgb8();
32
+
Ok(RgbFrame {
33
+
width: target_width,
34
+
height: target_height,
35
+
data: rgb.into_raw(),
36
+
})
37
+
}
+7
src/lib.rs
···
0
0
0
0
0
0
0
···
1
+
pub mod chromium;
2
+
pub mod cli;
3
+
pub mod encoder;
4
+
pub mod error;
5
+
pub mod frame;
6
+
pub mod retry;
7
+
pub mod rtmp;
+235
src/main.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::path::PathBuf;
2
+
use std::time::Duration;
3
+
4
+
use anyhow::{Context, Result, bail};
5
+
use clap::Parser;
6
+
use tracing::{info, warn};
7
+
8
+
use browser_stream::chromium;
9
+
use browser_stream::cli::{AppConfig, CliArgs};
10
+
use browser_stream::encoder::{self, EncoderSettings, FfmpegEncoder};
11
+
use browser_stream::error::RuntimeError;
12
+
use browser_stream::retry::RetryPolicy;
13
+
14
+
#[derive(Debug, Clone)]
15
+
struct RuntimePaths {
16
+
ffmpeg: PathBuf,
17
+
chromium: PathBuf,
18
+
}
19
+
20
+
#[tokio::main]
21
+
async fn main() -> Result<()> {
22
+
let args = CliArgs::parse();
23
+
init_tracing(args.verbose);
24
+
25
+
let config = args.into_config()?;
26
+
let runtime_paths = resolve_runtime_paths(&config)?;
27
+
let retry_policy = RetryPolicy::new(
28
+
config.retries,
29
+
Duration::from_millis(config.retry_backoff_ms),
30
+
);
31
+
32
+
run_with_retry(&config, &runtime_paths, &retry_policy).await
33
+
}
34
+
35
+
async fn run_with_retry(
36
+
config: &AppConfig,
37
+
runtime_paths: &RuntimePaths,
38
+
retry_policy: &RetryPolicy,
39
+
) -> Result<()> {
40
+
let mut failures = 0_u32;
41
+
42
+
loop {
43
+
let attempt = failures + 1;
44
+
info!(attempt, "starting stream attempt");
45
+
46
+
let result = run_once(config, runtime_paths).await;
47
+
48
+
match result {
49
+
Ok(()) => return Ok(()),
50
+
Err(err) => {
51
+
if is_shutdown_error(&err) {
52
+
info!("shutdown requested, exiting");
53
+
// Force process termination in case any background runtime task/thread
54
+
// holds the process open after graceful shutdown.
55
+
std::process::exit(0);
56
+
}
57
+
58
+
failures = failures.saturating_add(1);
59
+
if !retry_policy.should_retry(failures) {
60
+
return Err(err.context(format!(
61
+
"stream failed after {attempt} attempt(s) with {failures} failure(s)"
62
+
)));
63
+
}
64
+
65
+
warn!(
66
+
attempt,
67
+
failures,
68
+
backoff_ms = retry_policy.backoff.as_millis(),
69
+
error = %err,
70
+
"stream attempt failed; retrying"
71
+
);
72
+
73
+
tokio::select! {
74
+
_ = tokio::time::sleep(retry_policy.backoff) => {}
75
+
_ = tokio::signal::ctrl_c() => {
76
+
info!("shutdown requested during retry backoff, exiting");
77
+
return Ok(());
78
+
}
79
+
}
80
+
}
81
+
}
82
+
}
83
+
}
84
+
85
+
async fn run_once(config: &AppConfig, runtime_paths: &RuntimePaths) -> Result<()> {
86
+
let settings = EncoderSettings {
87
+
width: config.width,
88
+
height: config.height,
89
+
fps: config.fps,
90
+
bitrate_kbps: config.bitrate_kbps,
91
+
keyint_sec: config.keyint_sec,
92
+
x264_opts: config.x264_opts.clone(),
93
+
output: config.output.clone(),
94
+
include_silent_audio: !config.no_audio,
95
+
ffmpeg_path: runtime_paths.ffmpeg.clone(),
96
+
};
97
+
98
+
let mut encoder = FfmpegEncoder::spawn(&settings, config.verbose).await?;
99
+
100
+
let stream_result =
101
+
chromium::stream_browser_to_encoder(config, &runtime_paths.chromium, &mut encoder).await;
102
+
103
+
match stream_result {
104
+
Ok(()) => {
105
+
let status = encoder.wait_for_exit().await?;
106
+
if !status.success() {
107
+
bail!("ffmpeg exited with status {status}");
108
+
}
109
+
Ok(())
110
+
}
111
+
Err(err) => {
112
+
encoder.kill_and_wait().await;
113
+
Err(err)
114
+
}
115
+
}
116
+
}
117
+
118
+
fn resolve_runtime_paths(config: &AppConfig) -> Result<RuntimePaths> {
119
+
let current_exe =
120
+
std::env::current_exe().context("failed to determine current executable path")?;
121
+
let exe_dir = current_exe
122
+
.parent()
123
+
.context("failed to determine current executable directory")?;
124
+
125
+
let ffmpeg_path = resolve_ffmpeg_path(config.ffmpeg_path.clone(), exe_dir)?;
126
+
127
+
let chromium_path = resolve_binary_path(
128
+
config.chromium_path.clone(),
129
+
chromium::default_chromium_sidecar_path(exe_dir),
130
+
"headless_shell",
131
+
)?;
132
+
133
+
Ok(RuntimePaths {
134
+
ffmpeg: ffmpeg_path,
135
+
chromium: chromium_path,
136
+
})
137
+
}
138
+
139
+
fn resolve_binary_path(
140
+
override_path: Option<PathBuf>,
141
+
default_path: PathBuf,
142
+
name: &'static str,
143
+
) -> Result<PathBuf> {
144
+
let candidate = override_path.unwrap_or(default_path);
145
+
146
+
if candidate.is_file() {
147
+
return Ok(candidate);
148
+
}
149
+
150
+
Err(RuntimeError::MissingSidecar {
151
+
name,
152
+
path: candidate,
153
+
}
154
+
.into())
155
+
}
156
+
157
+
fn resolve_ffmpeg_path(
158
+
override_path: Option<PathBuf>,
159
+
exe_dir: &std::path::Path,
160
+
) -> Result<PathBuf> {
161
+
if let Some(path) = override_path {
162
+
return resolve_binary_path(Some(path), PathBuf::new(), "ffmpeg");
163
+
}
164
+
165
+
let sidecar = encoder::default_ffmpeg_sidecar_path(exe_dir);
166
+
let system = find_in_path(encoder::ffmpeg_executable_name());
167
+
168
+
if cfg!(target_os = "macos") {
169
+
if let Some(system_path) = system {
170
+
info!(
171
+
ffmpeg = %system_path.display(),
172
+
"using system ffmpeg on macOS (preferred over sidecar)"
173
+
);
174
+
return Ok(system_path);
175
+
}
176
+
}
177
+
178
+
if sidecar.is_file() {
179
+
return Ok(sidecar);
180
+
}
181
+
182
+
if let Some(system_path) = find_in_path(encoder::ffmpeg_executable_name()) {
183
+
info!(
184
+
ffmpeg = %system_path.display(),
185
+
"using system ffmpeg from PATH"
186
+
);
187
+
return Ok(system_path);
188
+
}
189
+
190
+
Err(RuntimeError::MissingSidecar {
191
+
name: "ffmpeg",
192
+
path: sidecar,
193
+
}
194
+
.into())
195
+
}
196
+
197
+
fn find_in_path(executable_name: &str) -> Option<PathBuf> {
198
+
let path_var = std::env::var_os("PATH")?;
199
+
let candidates = std::env::split_paths(&path_var);
200
+
201
+
for dir in candidates {
202
+
let direct = dir.join(executable_name);
203
+
if direct.is_file() {
204
+
return Some(direct);
205
+
}
206
+
207
+
if cfg!(target_os = "windows") {
208
+
let exe = dir.join(format!("{executable_name}.exe"));
209
+
if exe.is_file() {
210
+
return Some(exe);
211
+
}
212
+
}
213
+
}
214
+
215
+
None
216
+
}
217
+
218
+
fn is_shutdown_error(err: &anyhow::Error) -> bool {
219
+
err.downcast_ref::<RuntimeError>()
220
+
.is_some_and(|runtime| matches!(runtime, RuntimeError::ShutdownRequested))
221
+
}
222
+
223
+
fn init_tracing(verbose: bool) {
224
+
let filter = if verbose {
225
+
tracing_subscriber::EnvFilter::new("info,browser_stream=debug,ffmpeg=info")
226
+
} else {
227
+
tracing_subscriber::EnvFilter::try_from_default_env()
228
+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
229
+
};
230
+
231
+
let _ = tracing_subscriber::fmt()
232
+
.with_env_filter(filter)
233
+
.with_target(true)
234
+
.try_init();
235
+
}
+20
src/retry.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::time::Duration;
2
+
3
+
#[derive(Debug, Clone)]
4
+
pub struct RetryPolicy {
5
+
pub max_retries: u32,
6
+
pub backoff: Duration,
7
+
}
8
+
9
+
impl RetryPolicy {
10
+
pub fn new(max_retries: u32, backoff: Duration) -> Self {
11
+
Self {
12
+
max_retries,
13
+
backoff,
14
+
}
15
+
}
16
+
17
+
pub fn should_retry(&self, failures_so_far: u32) -> bool {
18
+
failures_so_far <= self.max_retries
19
+
}
20
+
}
+54
src/rtmp.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use thiserror::Error;
2
+
use url::Url;
3
+
4
+
#[derive(Debug, Error, PartialEq)]
5
+
pub enum RtmpError {
6
+
#[error("provide either `--output` or both `--rtmp-url` and `--stream-key`")]
7
+
MissingDestination,
8
+
#[error("stream key cannot be empty")]
9
+
EmptyStreamKey,
10
+
#[error("invalid RTMP output URL `{0}`")]
11
+
InvalidOutputUrl(String),
12
+
#[error("RTMP URL scheme must be `rtmp` or `rtmps`, got `{0}`")]
13
+
InvalidScheme(String),
14
+
}
15
+
16
+
pub fn build_output(
17
+
output: Option<String>,
18
+
rtmp_url: Option<String>,
19
+
stream_key: Option<String>,
20
+
) -> Result<String, RtmpError> {
21
+
if let Some(full_output) = output {
22
+
let trimmed = full_output.trim();
23
+
validate_output_url(trimmed)?;
24
+
return Ok(trimmed.to_string());
25
+
}
26
+
27
+
match (rtmp_url, stream_key) {
28
+
(Some(base), Some(key)) => {
29
+
let normalized_key = normalize_stream_key(&key)?;
30
+
let merged = format!("{}/{}", base.trim_end_matches('/'), normalized_key);
31
+
validate_output_url(&merged)?;
32
+
Ok(merged)
33
+
}
34
+
_ => Err(RtmpError::MissingDestination),
35
+
}
36
+
}
37
+
38
+
fn normalize_stream_key(raw: &str) -> Result<String, RtmpError> {
39
+
let key = raw.trim().trim_start_matches('/').trim();
40
+
if key.is_empty() {
41
+
return Err(RtmpError::EmptyStreamKey);
42
+
}
43
+
44
+
Ok(key.to_string())
45
+
}
46
+
47
+
fn validate_output_url(candidate: &str) -> Result<(), RtmpError> {
48
+
let parsed =
49
+
Url::parse(candidate).map_err(|_| RtmpError::InvalidOutputUrl(candidate.to_string()))?;
50
+
match parsed.scheme() {
51
+
"rtmp" | "rtmps" => Ok(()),
52
+
other => Err(RtmpError::InvalidScheme(other.to_string())),
53
+
}
54
+
}
+120
tests/cli.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use assert_matches::assert_matches;
2
+
use clap::Parser;
3
+
4
+
use browser_stream::cli::CliArgs;
5
+
use browser_stream::error::ConfigError;
6
+
use browser_stream::rtmp::RtmpError;
7
+
8
+
#[test]
9
+
fn parses_defaults_and_required_fields() {
10
+
let args = CliArgs::try_parse_from([
11
+
"browser-stream",
12
+
"--url",
13
+
"https://example.com",
14
+
"--output",
15
+
"rtmp://live.example.com/app/stream",
16
+
])
17
+
.expect("cli parse should succeed");
18
+
19
+
let config = args.into_config().expect("config should validate");
20
+
21
+
assert_eq!(config.width, 1920);
22
+
assert_eq!(config.height, 1080);
23
+
assert_eq!(config.fps, 30);
24
+
assert_eq!(config.bitrate_kbps, 4500);
25
+
assert_eq!(config.keyint_sec, 1);
26
+
assert_eq!(config.x264_opts, "bframes=0");
27
+
assert_eq!(config.retries, 5);
28
+
assert_eq!(config.retry_backoff_ms, 1000);
29
+
assert_eq!(config.startup_delay_ms, 2000);
30
+
assert_eq!(config.frame_timeout_ms, 30000);
31
+
assert!(!config.no_audio);
32
+
}
33
+
34
+
#[test]
35
+
fn requires_destination_settings() {
36
+
let args = CliArgs::try_parse_from(["browser-stream", "--url", "https://example.com"])
37
+
.expect("cli parse should succeed");
38
+
39
+
let err = args.into_config().expect_err("validation should fail");
40
+
assert_matches!(err, ConfigError::Rtmp(RtmpError::MissingDestination));
41
+
}
42
+
43
+
#[test]
44
+
fn output_precedence_wins_over_split_fields() {
45
+
let args = CliArgs::try_parse_from([
46
+
"browser-stream",
47
+
"--url",
48
+
"https://example.com",
49
+
"--output",
50
+
"rtmps://primary.example.com/app/final",
51
+
"--rtmp-url",
52
+
"rtmp://secondary.example.com/app",
53
+
"--stream-key",
54
+
"secondary",
55
+
])
56
+
.expect("cli parse should succeed");
57
+
58
+
let config = args.into_config().expect("config should validate");
59
+
assert_eq!(config.output, "rtmps://primary.example.com/app/final");
60
+
}
61
+
62
+
#[test]
63
+
fn rejects_non_http_website_scheme() {
64
+
let args = CliArgs::try_parse_from([
65
+
"browser-stream",
66
+
"--url",
67
+
"file:///tmp/index.html",
68
+
"--output",
69
+
"rtmp://live.example.com/app/key",
70
+
])
71
+
.expect("cli parse should succeed");
72
+
73
+
let err = args.into_config().expect_err("validation should fail");
74
+
assert_matches!(err, ConfigError::UnsupportedWebsiteScheme(s) if s == "file");
75
+
}
76
+
77
+
#[test]
78
+
fn rejects_out_of_range_fps() {
79
+
let args = CliArgs::try_parse_from([
80
+
"browser-stream",
81
+
"--url",
82
+
"https://example.com",
83
+
"--output",
84
+
"rtmp://live.example.com/app/key",
85
+
"--fps",
86
+
"121",
87
+
])
88
+
.expect("cli parse should succeed");
89
+
90
+
let err = args.into_config().expect_err("validation should fail");
91
+
assert_matches!(
92
+
err,
93
+
ConfigError::OutOfRange { field, min: 1, max: 120, actual: 121 } if field == "fps"
94
+
);
95
+
}
96
+
97
+
#[test]
98
+
fn rejects_out_of_range_frame_timeout() {
99
+
let args = CliArgs::try_parse_from([
100
+
"browser-stream",
101
+
"--url",
102
+
"https://example.com",
103
+
"--output",
104
+
"rtmp://live.example.com/app/key",
105
+
"--frame-timeout-ms",
106
+
"500",
107
+
])
108
+
.expect("cli parse should succeed");
109
+
110
+
let err = args.into_config().expect_err("validation should fail");
111
+
assert_matches!(
112
+
err,
113
+
ConfigError::OutOfRange {
114
+
field,
115
+
min: 1000,
116
+
max,
117
+
actual: 500
118
+
} if field == "frame-timeout-ms" && max == u64::MAX
119
+
);
120
+
}
+80
tests/ffmpeg_cmd.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::path::PathBuf;
2
+
3
+
use browser_stream::encoder::{EncoderSettings, build_ffmpeg_args};
4
+
5
+
#[test]
6
+
fn derives_keyint_from_fps_and_seconds() {
7
+
let settings = EncoderSettings {
8
+
width: 1280,
9
+
height: 720,
10
+
fps: 30,
11
+
bitrate_kbps: 2500,
12
+
keyint_sec: 2,
13
+
x264_opts: "bframes=0".to_string(),
14
+
output: "rtmp://live.example.com/app/key".to_string(),
15
+
include_silent_audio: true,
16
+
ffmpeg_path: PathBuf::from("/tmp/ffmpeg"),
17
+
};
18
+
19
+
let args = build_ffmpeg_args(&settings);
20
+
21
+
assert_pair(&args, "-g", "60");
22
+
assert_pair(&args, "-keyint_min", "60");
23
+
}
24
+
25
+
#[test]
26
+
fn includes_cbr_like_flags() {
27
+
let settings = EncoderSettings {
28
+
width: 1920,
29
+
height: 1080,
30
+
fps: 60,
31
+
bitrate_kbps: 4500,
32
+
keyint_sec: 1,
33
+
x264_opts: "bframes=0".to_string(),
34
+
output: "rtmps://live.example.com/app/key".to_string(),
35
+
include_silent_audio: true,
36
+
ffmpeg_path: PathBuf::from("/tmp/ffmpeg"),
37
+
};
38
+
39
+
let args = build_ffmpeg_args(&settings);
40
+
41
+
assert_pair(&args, "-b:v", "4500k");
42
+
assert_pair(&args, "-maxrate", "4500k");
43
+
assert_pair(&args, "-bufsize", "9000k");
44
+
}
45
+
46
+
#[test]
47
+
fn passes_x264_opts_and_output() {
48
+
let settings = EncoderSettings {
49
+
width: 1920,
50
+
height: 1080,
51
+
fps: 30,
52
+
bitrate_kbps: 3000,
53
+
keyint_sec: 1,
54
+
x264_opts: "bframes=0:scenecut=0".to_string(),
55
+
output: "rtmp://live.example.com/app/key".to_string(),
56
+
include_silent_audio: true,
57
+
ffmpeg_path: PathBuf::from("/tmp/ffmpeg"),
58
+
};
59
+
60
+
let args = build_ffmpeg_args(&settings);
61
+
62
+
assert_pair(&args, "-x264-params", "bframes=0:scenecut=0");
63
+
assert_eq!(
64
+
args.last().expect("args should not be empty"),
65
+
"rtmp://live.example.com/app/key"
66
+
);
67
+
}
68
+
69
+
fn assert_pair(args: &[String], flag: &str, value: &str) {
70
+
let index = args
71
+
.iter()
72
+
.position(|item| item == flag)
73
+
.expect("flag should exist in arg list");
74
+
75
+
let next = args
76
+
.get(index + 1)
77
+
.expect("flag should have a following value");
78
+
79
+
assert_eq!(next, value);
80
+
}
+13
tests/frame.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use browser_stream::frame::decode_screencast_frame;
2
+
3
+
#[test]
4
+
fn decodes_and_resizes_frame() {
5
+
// 1x1 red PNG
6
+
let png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAABJRU5ErkJggg==";
7
+
8
+
let frame = decode_screencast_frame(png_base64, 2, 2).expect("decode should work");
9
+
10
+
assert_eq!(frame.width, 2);
11
+
assert_eq!(frame.height, 2);
12
+
assert_eq!(frame.data.len(), 2 * 2 * 3);
13
+
}
+12
tests/retry.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::time::Duration;
2
+
3
+
use browser_stream::retry::RetryPolicy;
4
+
5
+
#[test]
6
+
fn retries_up_to_configured_limit() {
7
+
let policy = RetryPolicy::new(5, Duration::from_millis(100));
8
+
9
+
assert!(policy.should_retry(1));
10
+
assert!(policy.should_retry(5));
11
+
assert!(!policy.should_retry(6));
12
+
}
+59
tests/rtmp.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use assert_matches::assert_matches;
2
+
3
+
use browser_stream::rtmp::{RtmpError, build_output};
4
+
5
+
#[test]
6
+
fn builds_output_from_split_fields() {
7
+
let output = build_output(
8
+
None,
9
+
Some("rtmp://live.example.com/app".to_string()),
10
+
Some("streamkey123".to_string()),
11
+
)
12
+
.expect("build should succeed");
13
+
14
+
assert_eq!(output, "rtmp://live.example.com/app/streamkey123");
15
+
}
16
+
17
+
#[test]
18
+
fn trims_slashes_and_spaces_in_stream_key() {
19
+
let output = build_output(
20
+
None,
21
+
Some("rtmp://live.example.com/app/".to_string()),
22
+
Some(" /abc123 ".to_string()),
23
+
)
24
+
.expect("build should succeed");
25
+
26
+
assert_eq!(output, "rtmp://live.example.com/app/abc123");
27
+
}
28
+
29
+
#[test]
30
+
fn output_flag_takes_precedence() {
31
+
let output = build_output(
32
+
Some("rtmps://primary.example.com/live/final".to_string()),
33
+
Some("rtmp://secondary.example.com/app".to_string()),
34
+
Some("secondary".to_string()),
35
+
)
36
+
.expect("build should succeed");
37
+
38
+
assert_eq!(output, "rtmps://primary.example.com/live/final");
39
+
}
40
+
41
+
#[test]
42
+
fn rejects_invalid_scheme() {
43
+
let err = build_output(Some("https://example.com/not-rtmp".to_string()), None, None)
44
+
.expect_err("should fail");
45
+
46
+
assert_matches!(err, RtmpError::InvalidScheme(s) if s == "https");
47
+
}
48
+
49
+
#[test]
50
+
fn rejects_empty_key() {
51
+
let err = build_output(
52
+
None,
53
+
Some("rtmp://live.example.com/app".to_string()),
54
+
Some(" / ".to_string()),
55
+
)
56
+
.expect_err("should fail");
57
+
58
+
assert_matches!(err, RtmpError::EmptyStreamKey);
59
+
}