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