* feat: add in-process GStreamer encoder control * test: gate in-process GStreamer control * docs: describe opt-in encoder control
- commit
- d0bb8e06159770016a49145cbb953a9a044cd94c
- parent
- e30e0693e1b036126c4d1c87a0e45c567c2534f9
- author
- FelineStateMachine <dami.etoile@gmail.com>
| M | .github/ | 9 | + |
| M | AGENTS.md | 18 | +- |
| M | Cargo.lock | 451 | ++++++++++++++--- |
| M | README.md | 18 | +- |
| M | crates/ | 7 | + |
| M | crates/ | 4 | +- |
| M | crates/ | 143 | +++++ |
| M | crates/ | 7 | + |
| M | crates/ | 796 | +++++++++++++++++++++++++++++- |
| M | docs/ | 46 | ++ |
| M | scripts/ | 71 | ++- |
| M | scripts/ | 3 | + |
| A | scripts/ | 86 | +++ |
| M | scripts/ | 45 | ++ |
@@ -31,7 +31,14 @@ jobs:
3131 build-essential \3232 file \3333 ffmpeg \34+ gstreamer1.0-plugins-base \35+ gstreamer1.0-plugins-bad \36+ gstreamer1.0-plugins-good \37+ gstreamer1.0-plugins-ugly \38+ gstreamer1.0-tools \3439 libayatana-appindicator3-dev \40+ libgstreamer-plugins-base1.0-dev \41+ libgstreamer1.0-dev \3542 librsvg2-dev \3643 libssl-dev \3744 libudev-dev \@@ -41,4 +48,6 @@ jobs:
4148 shellcheck4249 4350 - name: Run complete demo gate51+ env:52+ GOQ_VERIFY_IN_PROCESS_GSTREAMER: "1"4453 run: ./scripts/verify-demo-build.sh@@ -29,7 +29,8 @@ Sigil host.
2929 - `server.rs` owns session admission and separate media, input, and audio3030 protocols.3131 - `source.rs` owns test-pattern and Gamescope PipeWire capture plus AMD GstVA32- H.264 encoding.32+ H.264 encoding. External `gst-launch` remains the default; an explicitly33+ feature-gated in-process backend owns bounded encoder control.3334 - `input.rs` owns strict Linux `uinput` pointer, keyboard, and Xbox-style3435 virtual gamepad devices.3536 - `audio.rs` owns bounded PipeWire/Opus capture.@@ -52,6 +53,11 @@ Sigil host.
5253 - Gamescope's exact PipeWire node is resolved from strict configured5354 properties. AMD GstVA H.264 sustains the fixed 1280x800/60 first target on5455 the GPD Pocket 4 test host with bounded post-encode delivery.56+- Existing host configurations default to the proven external `gst-launch`57+ backend. `encoder_backend = "in-process-gstreamer"` fails closed unless the58+ Linux binary was explicitly built with that feature and its runtime59+ GStreamer elements are available. The opt-in backend currently requires CBR;60+ CQP remains on the external compatibility backend.5561 - The preferred media path authenticates and claims the single client over5662 `sigil/control/1`, then admits that exact peer to a session-scoped upstream5763 `iroh-moq` broadcast and static H.264 track. One configured GOP maps to one@@ -141,14 +147,20 @@ Sigil host.
141147 bitrate, motion-sensitive resolution, and automatic low-latency codec142148 selection. The preferred path now uses pinned upstream `iroh-moq`; the143149 external `gst-launch` encoder can still honor a keyframe request only at its144- next natural configured IDR. An in-process GStreamer control path plus145- hardware loss/relay proof remain before claiming issue #7 complete.150+ next natural configured IDR. An opt-in in-process GStreamer control path now151+ exists; hardware loss/relay proof remains before claiming issue #7 complete.152+ The in-process encoder backend is not part of the product release feature set153+ until its native Bazzite and cross-build/package dependency gates pass.146154 147155 ## Working rules148156 149157 - Use Rust edition 2024. Sigil and Portal require Rust 1.91 or newer and the150158 repository pins Rust 1.95 in `rust-toolchain.toml`; `sigil-protocol` retains a151159 1.85 minimum. Source `~/.cargo/env` before invoking Cargo directly.160+- Building Sigil with `in-process-gstreamer` requires the GStreamer core, app,161+ and video development packages. Deployment requires their matching runtime162+ libraries plus `appsink`, `pipewiresrc`, parser/conversion elements, and the163+ selected GstVA encoder plugin; never treat build headers as runtime payload.152164 - Run `./scripts/verify-demo-build.sh` for the complete repository gate. It153165 covers Rust format/tests/clippy, the Linux cross-build when available,154166 frontend syntax/tests, ShellCheck, package tests, loopback transport, and@@ -146,7 +146,7 @@ version = "1.1.5"
146146 source = "registry+https://github.com/rust-lang/crates.io-index"147147 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"148148 dependencies = [149- "windows-sys 0.60.2",149+ "windows-sys 0.61.2",150150 ]151151 152152 [[package]]@@ -157,7 +157,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
157157 dependencies = [158158 "anstyle",159159 "once_cell_polyfill",160- "windows-sys 0.60.2",160+ "windows-sys 0.61.2",161161 ]162162 163163 [[package]]@@ -375,7 +375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
375375 checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"376376 dependencies = [377377 "atk-sys",378- "glib",378+ "glib 0.18.5",379379 "libc",380380 ]381381 @@ -385,10 +385,10 @@ version = "0.18.2"
385385 source = "registry+https://github.com/rust-lang/crates.io-index"386386 checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"387387 dependencies = [388- "glib-sys",389- "gobject-sys",388+ "glib-sys 0.18.1",389+ "gobject-sys 0.18.0",390390 "libc",391- "system-deps",391+ "system-deps 6.2.2",392392 ]393393 394394 [[package]]@@ -397,6 +397,12 @@ version = "1.1.2"
397397 source = "registry+https://github.com/rust-lang/crates.io-index"398398 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"399399 400+[[package]]401+name = "atomic_refcell"402+version = "0.1.14"403+source = "registry+https://github.com/rust-lang/crates.io-index"404+checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4"405+400406 [[package]]401407 name = "attohttpc"402408 version = "0.30.1"@@ -637,7 +643,7 @@ checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
637643 dependencies = [638644 "bitflags 2.13.1",639645 "cairo-sys-rs",640- "glib",646+ "glib 0.18.5",641647 "libc",642648 "once_cell",643649 "thiserror 1.0.69",@@ -649,9 +655,9 @@ version = "0.18.2"
649655 source = "registry+https://github.com/rust-lang/crates.io-index"650656 checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"651657 dependencies = [652- "glib-sys",658+ "glib-sys 0.18.1",653659 "libc",654- "system-deps",660+ "system-deps 6.2.2",655661 ]656662 657663 [[package]]@@ -741,7 +747,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
741747 checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"742748 dependencies = [743749 "smallvec",744- "target-lexicon",750+ "target-lexicon 0.12.16",751+]752+753+[[package]]754+name = "cfg-expr"755+version = "0.20.8"756+source = "registry+https://github.com/rust-lang/crates.io-index"757+checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"758+dependencies = [759+ "smallvec",760+ "target-lexicon 0.13.5",745761 ]746762 747763 [[package]]@@ -1280,7 +1296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
12801296 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"12811297 dependencies = [12821298 "data-encoding",1283- "syn 1.0.109",1299+ "syn 2.0.119",12841300 ]12851301 12861302 [[package]]@@ -1395,7 +1411,7 @@ dependencies = [
13951411 "libc",13961412 "option-ext",13971413 "redox_users",1398- "windows-sys 0.59.0",1414+ "windows-sys 0.61.2",13991415 ]14001416 14011417 [[package]]@@ -1656,7 +1672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
16561672 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"16571673 dependencies = [16581674 "libc",1659- "windows-sys 0.59.0",1675+ "windows-sys 0.61.2",16601676 ]16611677 16621678 [[package]]@@ -1914,7 +1930,7 @@ dependencies = [
19141930 "gdk-pixbuf",19151931 "gdk-sys",19161932 "gio",1917- "glib",1933+ "glib 0.18.5",19181934 "libc",19191935 "pango",19201936 ]@@ -1927,7 +1943,7 @@ checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
19271943 dependencies = [19281944 "gdk-pixbuf-sys",19291945 "gio",1930- "glib",1946+ "glib 0.18.5",19311947 "libc",19321948 "once_cell",19331949 ]@@ -1938,11 +1954,11 @@ version = "0.18.0"
19381954 source = "registry+https://github.com/rust-lang/crates.io-index"19391955 checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"19401956 dependencies = [1941- "gio-sys",1942- "glib-sys",1943- "gobject-sys",1957+ "gio-sys 0.18.1",1958+ "glib-sys 0.18.1",1959+ "gobject-sys 0.18.0",19441960 "libc",1945- "system-deps",1961+ "system-deps 6.2.2",19461962 ]19471963 19481964 [[package]]@@ -1953,13 +1969,13 @@ checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
19531969 dependencies = [19541970 "cairo-sys-rs",19551971 "gdk-pixbuf-sys",1956- "gio-sys",1957- "glib-sys",1958- "gobject-sys",1972+ "gio-sys 0.18.1",1973+ "glib-sys 0.18.1",1974+ "gobject-sys 0.18.0",19591975 "libc",19601976 "pango-sys",19611977 "pkg-config",1962- "system-deps",1978+ "system-deps 6.2.2",19631979 ]19641980 19651981 [[package]]@@ -1969,11 +1985,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19691985 checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"19701986 dependencies = [19711987 "gdk-sys",1972- "glib-sys",1973- "gobject-sys",1988+ "glib-sys 0.18.1",1989+ "gobject-sys 0.18.0",19741990 "libc",19751991 "pkg-config",1976- "system-deps",1992+ "system-deps 6.2.2",19771993 ]19781994 19791995 [[package]]@@ -1985,7 +2001,7 @@ dependencies = [
19852001 "gdk",19862002 "gdkx11-sys",19872003 "gio",1988- "glib",2004+ "glib 0.18.5",19892005 "libc",19902006 "x11",19912007 ]@@ -1997,9 +2013,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19972013 checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"19982014 dependencies = [19992015 "gdk-sys",2000- "glib-sys",2016+ "glib-sys 0.18.1",20012017 "libc",2002- "system-deps",2018+ "system-deps 6.2.2",20032019 "x11",20042020 ]20052021 @@ -2014,8 +2030,8 @@ dependencies = [
20142030 "libc",20152031 "log",20162032 "rustversion",2017- "windows-link 0.1.3",2018- "windows-result 0.3.4",2033+ "windows-link 0.2.1",2034+ "windows-result 0.4.1",20192035 ]20202036 20212037 [[package]]@@ -2095,8 +2111,8 @@ dependencies = [
20952111 "futures-core",20962112 "futures-io",20972113 "futures-util",2098- "gio-sys",2099- "glib",2114+ "gio-sys 0.18.1",2115+ "glib 0.18.5",21002116 "libc",21012117 "once_cell",21022118 "pin-project-lite",@@ -2110,13 +2126,26 @@ version = "0.18.1"
21102126 source = "registry+https://github.com/rust-lang/crates.io-index"21112127 checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"21122128 dependencies = [2113- "glib-sys",2114- "gobject-sys",2129+ "glib-sys 0.18.1",2130+ "gobject-sys 0.18.0",21152131 "libc",2116- "system-deps",2132+ "system-deps 6.2.2",21172133 "winapi",21182134 ]21192135 2136+[[package]]2137+name = "gio-sys"2138+version = "0.21.5"2139+source = "registry+https://github.com/rust-lang/crates.io-index"2140+checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"2141+dependencies = [2142+ "glib-sys 0.21.5",2143+ "gobject-sys 0.21.5",2144+ "libc",2145+ "system-deps 7.0.8",2146+ "windows-sys 0.61.2",2147+]2148+21202149 [[package]]21212150 name = "glib"21222151 version = "0.18.5"@@ -2129,10 +2158,10 @@ dependencies = [
21292158 "futures-executor",21302159 "futures-task",21312160 "futures-util",2132- "gio-sys",2133- "glib-macros",2134- "glib-sys",2135- "gobject-sys",2161+ "gio-sys 0.18.1",2162+ "glib-macros 0.18.5",2163+ "glib-sys 0.18.1",2164+ "gobject-sys 0.18.0",21362165 "libc",21372166 "memchr",21382167 "once_cell",@@ -2140,6 +2169,27 @@ dependencies = [
21402169 "thiserror 1.0.69",21412170 ]21422171 2172+[[package]]2173+name = "glib"2174+version = "0.21.5"2175+source = "registry+https://github.com/rust-lang/crates.io-index"2176+checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"2177+dependencies = [2178+ "bitflags 2.13.1",2179+ "futures-channel",2180+ "futures-core",2181+ "futures-executor",2182+ "futures-task",2183+ "futures-util",2184+ "gio-sys 0.21.5",2185+ "glib-macros 0.21.5",2186+ "glib-sys 0.21.5",2187+ "gobject-sys 0.21.5",2188+ "libc",2189+ "memchr",2190+ "smallvec",2191+]2192+21432193 [[package]]21442194 name = "glib-macros"21452195 version = "0.18.5"@@ -2154,6 +2204,19 @@ dependencies = [
21542204 "syn 2.0.119",21552205 ]21562206 2207+[[package]]2208+name = "glib-macros"2209+version = "0.21.5"2210+source = "registry+https://github.com/rust-lang/crates.io-index"2211+checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"2212+dependencies = [2213+ "heck 0.5.0",2214+ "proc-macro-crate 3.5.0",2215+ "proc-macro2",2216+ "quote",2217+ "syn 2.0.119",2218+]2219+21572220 [[package]]21582221 name = "glib-sys"21592222 version = "0.18.1"@@ -2161,7 +2224,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
21612224 checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"21622225 dependencies = [21632226 "libc",2164- "system-deps",2227+ "system-deps 6.2.2",2228+]2229+2230+[[package]]2231+name = "glib-sys"2232+version = "0.21.5"2233+source = "registry+https://github.com/rust-lang/crates.io-index"2234+checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"2235+dependencies = [2236+ "libc",2237+ "system-deps 7.0.8",21652238 ]21662239 21672240 [[package]]@@ -2188,9 +2261,143 @@ version = "0.18.0"
21882261 source = "registry+https://github.com/rust-lang/crates.io-index"21892262 checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"21902263 dependencies = [2191- "glib-sys",2264+ "glib-sys 0.18.1",21922265 "libc",2193- "system-deps",2266+ "system-deps 6.2.2",2267+]2268+2269+[[package]]2270+name = "gobject-sys"2271+version = "0.21.5"2272+source = "registry+https://github.com/rust-lang/crates.io-index"2273+checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"2274+dependencies = [2275+ "glib-sys 0.21.5",2276+ "libc",2277+ "system-deps 7.0.8",2278+]2279+2280+[[package]]2281+name = "gstreamer"2282+version = "0.24.5"2283+source = "registry+https://github.com/rust-lang/crates.io-index"2284+checksum = "1e8251db223ca38d9aefaf3d19f6f11581a9123cd12dacebd8b9e182da965023"2285+dependencies = [2286+ "cfg-if",2287+ "futures-channel",2288+ "futures-core",2289+ "futures-util",2290+ "glib 0.21.5",2291+ "gstreamer-sys",2292+ "itertools",2293+ "kstring",2294+ "libc",2295+ "muldiv",2296+ "num-integer",2297+ "num-rational",2298+ "option-operations",2299+ "pastey",2300+ "pin-project-lite",2301+ "smallvec",2302+ "thiserror 2.0.19",2303+]2304+2305+[[package]]2306+name = "gstreamer-app"2307+version = "0.24.5"2308+source = "registry+https://github.com/rust-lang/crates.io-index"2309+checksum = "3da7017b2a2fa5cdf9123b1603947ea24174f6d8cea0ea673411df824c811921"2310+dependencies = [2311+ "futures-core",2312+ "futures-sink",2313+ "glib 0.21.5",2314+ "gstreamer",2315+ "gstreamer-app-sys",2316+ "gstreamer-base",2317+ "libc",2318+]2319+2320+[[package]]2321+name = "gstreamer-app-sys"2322+version = "0.24.5"2323+source = "registry+https://github.com/rust-lang/crates.io-index"2324+checksum = "4fa9f1b12b546aea543c15a0fdbc5a53617902a74f6d357a32b6a9fb4bc4725c"2325+dependencies = [2326+ "glib-sys 0.21.5",2327+ "gstreamer-base-sys",2328+ "gstreamer-sys",2329+ "libc",2330+ "system-deps 7.0.8",2331+]2332+2333+[[package]]2334+name = "gstreamer-base"2335+version = "0.24.5"2336+source = "registry+https://github.com/rust-lang/crates.io-index"2337+checksum = "9375f9a12120a8ee17b765c816c9b23861ce258def77b0ee40a05acb00c74972"2338+dependencies = [2339+ "atomic_refcell",2340+ "cfg-if",2341+ "glib 0.21.5",2342+ "gstreamer",2343+ "gstreamer-base-sys",2344+ "libc",2345+]2346+2347+[[package]]2348+name = "gstreamer-base-sys"2349+version = "0.24.5"2350+source = "registry+https://github.com/rust-lang/crates.io-index"2351+checksum = "1b844f3559b6ab0379b4b771261643783ae4e0ffa71d5f5f46e33b7acf66b752"2352+dependencies = [2353+ "glib-sys 0.21.5",2354+ "gobject-sys 0.21.5",2355+ "gstreamer-sys",2356+ "libc",2357+ "system-deps 7.0.8",2358+]2359+2360+[[package]]2361+name = "gstreamer-sys"2362+version = "0.24.5"2363+source = "registry+https://github.com/rust-lang/crates.io-index"2364+checksum = "b5d37c1a599ae57b8186948bd5699f2dbfc044baea9d400228b489a85bcf2759"2365+dependencies = [2366+ "cfg-if",2367+ "glib-sys 0.21.5",2368+ "gobject-sys 0.21.5",2369+ "libc",2370+ "system-deps 7.0.8",2371+]2372+2373+[[package]]2374+name = "gstreamer-video"2375+version = "0.24.5"2376+source = "registry+https://github.com/rust-lang/crates.io-index"2377+checksum = "72d9ba5295b206563a990a087c6541d57f650dcd4be4ca8a5a149a758e8df8a0"2378+dependencies = [2379+ "cfg-if",2380+ "futures-channel",2381+ "glib 0.21.5",2382+ "gstreamer",2383+ "gstreamer-base",2384+ "gstreamer-video-sys",2385+ "libc",2386+ "thiserror 2.0.19",2387+]2388+2389+[[package]]2390+name = "gstreamer-video-sys"2391+version = "0.24.5"2392+source = "registry+https://github.com/rust-lang/crates.io-index"2393+checksum = "20236fa412d7a50e59aa234b46828a99e3e5f06c4f80d271a49ecd2a1d3bfcbe"2394+dependencies = [2395+ "glib-sys 0.21.5",2396+ "gobject-sys 0.21.5",2397+ "gstreamer-base-sys",2398+ "gstreamer-sys",2399+ "libc",2400+ "system-deps 7.0.8",21942401 ]21952402 21962403 [[package]]@@ -2206,7 +2413,7 @@ dependencies = [
22062413 "gdk",22072414 "gdk-pixbuf",22082415 "gio",2209- "glib",2416+ "glib 0.18.5",22102417 "gtk-sys",22112418 "gtk3-macros",22122419 "libc",@@ -2224,12 +2431,12 @@ dependencies = [
22242431 "cairo-sys-rs",22252432 "gdk-pixbuf-sys",22262433 "gdk-sys",2227- "gio-sys",2228- "glib-sys",2229- "gobject-sys",2434+ "gio-sys 0.18.1",2435+ "glib-sys 0.18.1",2436+ "gobject-sys 0.18.0",22302437 "libc",22312438 "pango-sys",2232- "system-deps",2439+ "system-deps 6.2.2",22332440 ]22342441 22352442 [[package]]@@ -2548,7 +2755,7 @@ dependencies = [
25482755 "js-sys",25492756 "log",25502757 "wasm-bindgen",2551- "windows-core 0.61.2",2758+ "windows-core 0.62.2",25522759 ]25532760 25542761 [[package]]@@ -3000,6 +3207,15 @@ version = "1.70.2"
30003207 source = "registry+https://github.com/rust-lang/crates.io-index"30013208 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"30023209 3210+[[package]]3211+name = "itertools"3212+version = "0.14.0"3213+source = "registry+https://github.com/rust-lang/crates.io-index"3214+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"3215+dependencies = [3216+ "either",3217+]3218+30033219 [[package]]30043220 name = "itoa"30053221 version = "1.0.18"@@ -3013,7 +3229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
30133229 checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"30143230 dependencies = [30153231 "bitflags 1.3.2",3016- "glib",3232+ "glib 0.18.5",30173233 "javascriptcore-rs-sys",30183234 ]30193235 @@ -3023,10 +3239,10 @@ version = "1.1.1"
30233239 source = "registry+https://github.com/rust-lang/crates.io-index"30243240 checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"30253241 dependencies = [3026- "glib-sys",3027- "gobject-sys",3242+ "glib-sys 0.18.1",3243+ "gobject-sys 0.18.0",30283244 "libc",3029- "system-deps",3245+ "system-deps 6.2.2",30303246 ]30313247 30323248 [[package]]@@ -3166,6 +3382,15 @@ dependencies = [
31663382 "smallvec",31673383 ]31683384 3385+[[package]]3386+name = "kstring"3387+version = "2.0.2"3388+source = "registry+https://github.com/rust-lang/crates.io-index"3389+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"3390+dependencies = [3391+ "static_assertions",3392+]3393+31693394 [[package]]31703395 name = "lazy_static"31713396 version = "1.5.0"@@ -3178,7 +3403,7 @@ version = "0.9.0"
31783403 source = "registry+https://github.com/rust-lang/crates.io-index"31793404 checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"31803405 dependencies = [3181- "glib",3406+ "glib 0.18.5",31823407 "gtk",31833408 "gtk-sys",31843409 "libappindicator-sys",@@ -3424,9 +3649,15 @@ dependencies = [
34243649 "png 0.18.1",34253650 "serde",34263651 "thiserror 2.0.19",3427- "windows-sys 0.60.2",3652+ "windows-sys 0.61.2",34283653 ]34293654 3655+[[package]]3656+name = "muldiv"3657+version = "1.0.1"3658+source = "registry+https://github.com/rust-lang/crates.io-index"3659+checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"3660+34303661 [[package]]34313662 name = "n0-error"34323663 version = "1.0.0"@@ -3728,7 +3959,7 @@ version = "0.50.3"
37283959 source = "registry+https://github.com/rust-lang/crates.io-index"37293960 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"37303961 dependencies = [3731- "windows-sys 0.59.0",3962+ "windows-sys 0.61.2",37323963 ]37333964 37343965 [[package]]@@ -3825,7 +4056,7 @@ version = "0.7.6"
38254056 source = "registry+https://github.com/rust-lang/crates.io-index"38264057 checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"38274058 dependencies = [3828- "proc-macro-crate 1.3.1",4059+ "proc-macro-crate 3.5.0",38294060 "proc-macro2",38304061 "quote",38314062 "syn 2.0.119",@@ -4151,6 +4382,15 @@ version = "0.2.0"
41514382 source = "registry+https://github.com/rust-lang/crates.io-index"41524383 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"41534384 4385+[[package]]4386+name = "option-operations"4387+version = "0.6.1"4388+source = "registry+https://github.com/rust-lang/crates.io-index"4389+checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754"4390+dependencies = [4391+ "pastey",4392+]4393+41544394 [[package]]41554395 name = "ordered-multimap"41564396 version = "0.7.3"@@ -4187,7 +4427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41874427 checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"41884428 dependencies = [41894429 "gio",4190- "glib",4430+ "glib 0.18.5",41914431 "libc",41924432 "once_cell",41934433 "pango-sys",@@ -4199,10 +4439,10 @@ version = "0.18.0"
41994439 source = "registry+https://github.com/rust-lang/crates.io-index"42004440 checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"42014441 dependencies = [4202- "glib-sys",4203- "gobject-sys",4442+ "glib-sys 0.18.1",4443+ "gobject-sys 0.18.0",42044444 "libc",4205- "system-deps",4445+ "system-deps 6.2.2",42064446 ]42074447 42084448 [[package]]@@ -4250,6 +4490,12 @@ version = "1.0.15"
42504490 source = "registry+https://github.com/rust-lang/crates.io-index"42514491 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"42524492 4493+[[package]]4494+name = "pastey"4495+version = "0.2.3"4496+source = "registry+https://github.com/rust-lang/crates.io-index"4497+checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"4498+42534499 [[package]]42544500 name = "pem-rfc7468"42554501 version = "1.0.0"@@ -4830,8 +5076,8 @@ checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
48305076 dependencies = [48315077 "block2",48325078 "dispatch2",4833- "glib-sys",4834- "gobject-sys",5079+ "glib-sys 0.18.1",5080+ "gobject-sys 0.18.0",48355081 "gtk-sys",48365082 "js-sys",48375083 "log",@@ -4904,7 +5150,7 @@ dependencies = [
49045150 "errno",49055151 "libc",49065152 "linux-raw-sys",4907- "windows-sys 0.59.0",5153+ "windows-sys 0.61.2",49085154 ]49095155 49105156 [[package]]@@ -4962,7 +5208,7 @@ dependencies = [
49625208 "security-framework",49635209 "security-framework-sys",49645210 "webpki-root-certs",4965- "windows-sys 0.59.0",5211+ "windows-sys 0.61.2",49665212 ]49675213 49685214 [[package]]@@ -5115,7 +5361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
51155361 checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"51165362 dependencies = [51175363 "libc",5118- "windows-sys 0.59.0",5364+ "windows-sys 0.61.2",51195365 ]51205366 51215367 [[package]]@@ -5393,6 +5639,9 @@ dependencies = [
53935639 "bytes",53945640 "clap",53955641 "getrandom 0.4.3",5642+ "gstreamer",5643+ "gstreamer-app",5644+ "gstreamer-video",53965645 "input-linux",53975646 "iroh",53985647 "iroh-moq",@@ -5497,7 +5746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
54975746 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"54985747 dependencies = [54995748 "libc",5500- "windows-sys 0.60.2",5749+ "windows-sys 0.61.2",55015750 ]55025751 55035752 [[package]]@@ -5536,7 +5785,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
55365785 dependencies = [55375786 "futures-channel",55385787 "gio",5539- "glib",5788+ "glib 0.18.5",55405789 "libc",55415790 "soup3-sys",55425791 ]@@ -5547,11 +5796,11 @@ version = "0.5.0"
55475796 source = "registry+https://github.com/rust-lang/crates.io-index"55485797 checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"55495798 dependencies = [5550- "gio-sys",5551- "glib-sys",5552- "gobject-sys",5799+ "gio-sys 0.18.1",5800+ "glib-sys 0.18.1",5801+ "gobject-sys 0.18.0",55535802 "libc",5554- "system-deps",5803+ "system-deps 6.2.2",55555804 ]55565805 55575806 [[package]]@@ -5587,6 +5836,12 @@ version = "1.2.1"
55875836 source = "registry+https://github.com/rust-lang/crates.io-index"55885837 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"55895838 5839+[[package]]5840+name = "static_assertions"5841+version = "1.1.0"5842+source = "registry+https://github.com/rust-lang/crates.io-index"5843+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"5844+55905845 [[package]]55915846 name = "string_cache"55925847 version = "0.9.0"@@ -5662,7 +5917,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
56625917 checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"56635918 dependencies = [56645919 "proc-macro2",5665- "quote",56665920 "unicode-ident",56675921 ]56685922 @@ -5735,13 +5989,26 @@ version = "6.2.2"
57355989 source = "registry+https://github.com/rust-lang/crates.io-index"57365990 checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"57375991 dependencies = [5738- "cfg-expr",5992+ "cfg-expr 0.15.8",57395993 "heck 0.5.0",57405994 "pkg-config",57415995 "toml 0.8.2",57425996 "version-compare",57435997 ]57445998 5999+[[package]]6000+name = "system-deps"6001+version = "7.0.8"6002+source = "registry+https://github.com/rust-lang/crates.io-index"6003+checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"6004+dependencies = [6005+ "cfg-expr 0.20.8",6006+ "heck 0.5.0",6007+ "pkg-config",6008+ "toml 1.1.3+spec-1.1.0",6009+ "version-compare",6010+]6011+57456012 [[package]]57466013 name = "tagptr"57476014 version = "0.2.0"@@ -5805,6 +6072,12 @@ version = "0.12.16"
58056072 source = "registry+https://github.com/rust-lang/crates.io-index"58066073 checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"58076074 6075+[[package]]6076+name = "target-lexicon"6077+version = "0.13.5"6078+source = "registry+https://github.com/rust-lang/crates.io-index"6079+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"6080+58086081 [[package]]58096082 name = "tauri"58106083 version = "2.11.5"@@ -6142,10 +6415,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
61426415 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"61436416 dependencies = [61446417 "fastrand",6145- "getrandom 0.3.4",6418+ "getrandom 0.4.3",61466419 "once_cell",61476420 "rustix",6148- "windows-sys 0.59.0",6421+ "windows-sys 0.61.2",61496422 ]61506423 61516424 [[package]]@@ -6605,7 +6878,7 @@ dependencies = [
66056878 "png 0.18.1",66066879 "serde",66076880 "thiserror 2.0.19",6608- "windows-sys 0.60.2",6881+ "windows-sys 0.61.2",66096882 ]66106883 66116884 [[package]]@@ -6634,7 +6907,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
66346907 dependencies = [66356908 "memoffset",66366909 "tempfile",6637- "windows-sys 0.60.2",6910+ "windows-sys 0.61.2",66386911 ]66396912 66406913 [[package]]@@ -7016,10 +7289,10 @@ dependencies = [
70167289 "gdk",70177290 "gdk-sys",70187291 "gio",7019- "gio-sys",7020- "glib",7021- "glib-sys",7022- "gobject-sys",7292+ "gio-sys 0.18.1",7293+ "glib 0.18.5",7294+ "glib-sys 0.18.1",7295+ "gobject-sys 0.18.0",70237296 "gtk",70247297 "gtk-sys",70257298 "javascriptcore-rs",@@ -7038,15 +7311,15 @@ dependencies = [
70387311 "bitflags 1.3.2",70397312 "cairo-sys-rs",70407313 "gdk-sys",7041- "gio-sys",7042- "glib-sys",7043- "gobject-sys",7314+ "gio-sys 0.18.1",7315+ "glib-sys 0.18.1",7316+ "gobject-sys 0.18.0",70447317 "gtk-sys",70457318 "javascriptcore-rs-sys",70467319 "libc",70477320 "pkg-config",70487321 "soup3-sys",7049- "system-deps",7322+ "system-deps 6.2.2",70507323 ]70517324 70527325 [[package]]@@ -7141,7 +7414,7 @@ version = "0.1.11"
71417414 source = "registry+https://github.com/rust-lang/crates.io-index"71427415 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"71437416 dependencies = [7144- "windows-sys 0.59.0",7417+ "windows-sys 0.61.2",71457418 ]71467419 71477420 [[package]]@@ -7677,8 +7950,8 @@ dependencies = [
76777950 "log",76787951 "serde",76797952 "thiserror 2.0.19",7680- "windows 0.61.3",7681- "windows-core 0.61.2",7953+ "windows 0.62.2",7954+ "windows-core 0.62.2",76827955 ]76837956 76847957 [[package]]@@ -39,6 +39,11 @@ Steam and games.
3939 dependency.4040 - The Bazzite path captures the exact Gamescope PipeWire node and uses AMD4141 GstVA H.264 at the fixed 1280×800/60 first target.42+- Gamescope video keeps the proven external `gst-launch` pipeline as its43+ configuration default. Linux builds made explicitly with44+ `in-process-gstreamer` can opt into an in-process pipeline for bounded45+ bitrate and force-keyframe control; ordinary release packages do not enable46+ that feature yet.4247 - Portal delivers encoded frames to WebCodecs through a raw Tauri binary4348 channel. The handoff is capped at four frames, the decode queue and4449 presentation queue at two, and a bounded watchdog recovers a suspended@@ -83,9 +88,10 @@ not authentication.
8388 5. **Upstream Iroh/MoQ transport implemented; hardware proof pending:** an8489 authenticated control lease gates a session-scoped upstream MoQ broadcast;8590 bounded native groups cancel stale GOPs and recover through explicit86- keyframe requests. The external encoder currently responds at its next87- natural configured IDR; in-process force-key-unit control and induced-loss88- relay proof remain in issue #7.91+ keyframe requests. The external encoder responds at its next natural92+ configured IDR. An opt-in in-process encoder-control foundation exists, but93+ its Bazzite/package acceptance and induced-loss relay proof remain in issue94+ #7.8995 6. **Host hardware-proven; physical client controller pending:** the virtual9096 Xbox-style controller negotiated over Iroh and produced the expected9197 button, stick, trigger, D-pad, and neutral-release events on the Bazzite@@ -106,6 +112,12 @@ Requirements:
106112 - Tauri v2 system dependencies107113 - A FIDO2 key with `hmac-secret` support for the normal identity flow108114 115+The optional Linux `in-process-gstreamer` host feature additionally needs the116+GStreamer core, app, and video development libraries. Its matching runtime117+libraries and plugins, including `appsink`, must be installed on the host. The118+published Sigil package remains a default-feature build until native Bazzite119+and cross-build linking are proven.120+109121 ### Enroll one Portal110122 111123 On first launch, enter the security-key PIN and choose **show portal id**. On@@ -9,6 +9,10 @@ rust-version = "1.91"
99 autobins = false1010 default-run = "sigil"1111 12+[features]13+default = []14+in-process-gstreamer = ["dep:gstreamer", "dep:gstreamer-app", "dep:gstreamer-video"]15+1216 [[bin]]1317 name = "sigil"1418 path = "src/main.rs"@@ -36,6 +40,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
3640 web-transport-iroh.workspace = true3741 3842 [target.'cfg(target_os = "linux")'.dependencies]43+gstreamer = { version = "=0.24.5", optional = true }44+gstreamer-app = { version = "=0.24.5", optional = true }45+gstreamer-video = { version = "=0.24.5", optional = true }3946 input-linux = "0.7.1"4047 x11rb = { version = "0.13.2", default-features = false }4148 @@ -385,7 +385,8 @@ mod tests {
385385 386386 use super::*;387387 use crate::config::{388- GamescopePipewireConfig, InputMode, PipewireAudioConfig, VaapiRateControl, VideoSource,388+ GamescopeEncoderBackend, GamescopePipewireConfig, InputMode, PipewireAudioConfig,389+ VaapiRateControl, VideoSource,389390 };390391 use crate::source::resolve_pipewire_node_exact;391392 @@ -409,6 +410,7 @@ mod tests {
409410 pw_dump_path: "/usr/bin/pw-dump".into(),410411 gst_launch_path: "/usr/bin/gst-launch-1.0".into(),411412 gst_inspect_path: "/usr/bin/gst-inspect-1.0".into(),413+ encoder_backend: GamescopeEncoderBackend::ExternalGstLaunch,412414 vaapi_encoder: "vah264enc".into(),413415 vaapi_render_node: "/dev/dri/renderD128".into(),414416 rate_control: VaapiRateControl::Cbr,@@ -31,6 +31,18 @@ pub enum VaapiRateControl {
3131 Cqp,3232 }3333 34+35+36+pub enum GamescopeEncoderBackend {37+ /// Preserve the proven child-process pipeline and its natural-IDR control38+ /// limitation for existing configurations.39+ 40+ ExternalGstLaunch,41+ /// Run the video pipeline in-process so Sigil can apply bounded encoder42+ /// controls. This remains an explicit opt-in until hardware acceptance.43+ InProcessGstreamer,44+}45+3446 fn default_width() -> u32 {3547 12803648 }@@ -77,6 +89,10 @@ pub struct GamescopePipewireConfig {
7789 pub pw_dump_path: PathBuf,7890 pub gst_launch_path: PathBuf,7991 pub gst_inspect_path: PathBuf,92+ /// External gst-launch remains the compatibility default. The in-process93+ /// backend is accepted only by Linux builds that contain its feature.94+ 95+ pub encoder_backend: GamescopeEncoderBackend,8096 /// Exact dynamically registered VA encoder factory, such as `vah264enc`.8197 pub vaapi_encoder: String,8298 /// Exact AMD DRM render node expected to back `vaapi_encoder`.@@ -176,6 +192,16 @@ impl GamescopePipewireConfig {
176192 "gamescope_pipewire.{name} must be an absolute path"177193 );178194 }195+ ensure!(196+ self.encoder_backend != GamescopeEncoderBackend::InProcessGstreamer197+ || cfg!(all(target_os = "linux", feature = "in-process-gstreamer")),198+ "gamescope_pipewire.encoder_backend=in-process-gstreamer requires a Linux Sigil build with the in-process-gstreamer feature"199+ );200+ ensure!(201+ self.encoder_backend != GamescopeEncoderBackend::InProcessGstreamer202+ || self.rate_control == VaapiRateControl::Cbr,203+ "gamescope_pipewire.encoder_backend=in-process-gstreamer currently requires CBR"204+ );179205 ensure!(180206 self.vaapi_render_node181207 .file_name()@@ -485,6 +511,7 @@ xwayland_display = ":0"
485511 pw_dump_path = "/usr/bin/pw-dump"486512 gst_launch_path = "/usr/bin/gst-launch-1.0"487513 gst_inspect_path = "/usr/bin/gst-inspect-1.0"514+encoder_backend = "external-gst-launch"488515 vaapi_encoder = "vah264enc"489516 vaapi_render_node = "/dev/dri/renderD128"490517 rate_control = "cbr"@@ -512,6 +539,10 @@ bitrate_bps = 96000
512539 .as_deref(),513540 Some(":0")514541 );542+ assert_eq!(543+ config.gamescope_pipewire.as_ref().unwrap().encoder_backend,544+ GamescopeEncoderBackend::ExternalGstLaunch545+ );515546 }516547 517548 @@ -582,6 +613,7 @@ source = "gamescope-pipewire"
582613 pw_dump_path: "/usr/bin/pw-dump".into(),583614 gst_launch_path: "/usr/bin/gst-launch-1.0".into(),584615 gst_inspect_path: "/usr/bin/gst-inspect-1.0".into(),616+ encoder_backend: GamescopeEncoderBackend::ExternalGstLaunch,585617 vaapi_encoder: "vah264enc".into(),586618 vaapi_render_node: "/dev/dri/renderD128".into(),587619 rate_control: VaapiRateControl::Cbr,@@ -599,6 +631,117 @@ source = "gamescope-pipewire"
599631 assert!(config.validate().is_err());600632 }601633 634+ 635+ fn gamescope_encoder_backend_defaults_external() {636+ let config: HostConfig = toml::from_str(637+ r#"638+identity_path = "/tmp/host.key"639+state_path = "/tmp/state"640+source = "gamescope-pipewire"641+642+[gamescope_pipewire]643+node_name = "gamescope"644+media_class = "Video/Source"645+pw_dump_path = "/usr/bin/pw-dump"646+gst_launch_path = "/usr/bin/gst-launch-1.0"647+gst_inspect_path = "/usr/bin/gst-inspect-1.0"648+vaapi_encoder = "vah264enc"649+vaapi_render_node = "/dev/dri/renderD128"650+rate_control = "cbr"651+bitrate_kbps = 12000652+"#,653+ )654+ .unwrap();655+656+ assert_eq!(657+ config.gamescope_pipewire.unwrap().encoder_backend,658+ GamescopeEncoderBackend::ExternalGstLaunch659+ );660+ }661+662+ 663+ 664+ fn unavailable_in_process_backend_fails_closed() {665+ let mut config: HostConfig = toml::from_str(666+ r#"667+identity_path = "/tmp/host.key"668+state_path = "/tmp/state"669+source = "gamescope-pipewire"670+671+[gamescope_pipewire]672+node_name = "gamescope"673+media_class = "Video/Source"674+pw_dump_path = "/usr/bin/pw-dump"675+gst_launch_path = "/usr/bin/gst-launch-1.0"676+gst_inspect_path = "/usr/bin/gst-inspect-1.0"677+vaapi_encoder = "vah264enc"678+vaapi_render_node = "/dev/dri/renderD128"679+rate_control = "cbr"680+bitrate_kbps = 12000681+"#,682+ )683+ .unwrap();684+ config.gamescope_pipewire.as_mut().unwrap().encoder_backend =685+ GamescopeEncoderBackend::InProcessGstreamer;686+687+ assert!(config.validate().is_err());688+ }689+690+ 691+ 692+ fn feature_built_linux_accepts_in_process_backend() {693+ let mut config: HostConfig = toml::from_str(694+ r#"695+identity_path = "/tmp/host.key"696+state_path = "/tmp/state"697+source = "gamescope-pipewire"698+699+[gamescope_pipewire]700+node_name = "gamescope"701+media_class = "Video/Source"702+pw_dump_path = "/usr/bin/pw-dump"703+gst_launch_path = "/usr/bin/gst-launch-1.0"704+gst_inspect_path = "/usr/bin/gst-inspect-1.0"705+vaapi_encoder = "vah264enc"706+vaapi_render_node = "/dev/dri/renderD128"707+rate_control = "cbr"708+bitrate_kbps = 12000709+"#,710+ )711+ .unwrap();712+ let pipewire = config.gamescope_pipewire.as_mut().unwrap();713+ pipewire.encoder_backend = GamescopeEncoderBackend::InProcessGstreamer;714+715+ config.validate().unwrap();716+ let pipewire = config.gamescope_pipewire.unwrap();717+ assert_eq!(718+ pipewire.encoder_backend,719+ GamescopeEncoderBackend::InProcessGstreamer720+ );721+ assert_eq!(pipewire.rate_control, VaapiRateControl::Cbr);722+ }723+724+ 725+ fn in_process_cqp_fails_closed() {726+ let pipewire = GamescopePipewireConfig {727+ node_name: "gamescope".into(),728+ media_class: "Video/Source".into(),729+ match_properties: BTreeMap::new(),730+ xwayland_display: None,731+ pw_dump_path: "/usr/bin/pw-dump".into(),732+ gst_launch_path: "/usr/bin/gst-launch-1.0".into(),733+ gst_inspect_path: "/usr/bin/gst-inspect-1.0".into(),734+ encoder_backend: GamescopeEncoderBackend::InProcessGstreamer,735+ vaapi_encoder: "vah264enc".into(),736+ vaapi_render_node: "/dev/dri/renderD128".into(),737+ rate_control: VaapiRateControl::Cqp,738+ bitrate_kbps: None,739+ quantizer: Some(24),740+ };741+742+ assert!(pipewire.validate().is_err());743+ }744+602745 603746 fn rejects_odd_dimensions_and_codec_breadth() {604747 let mut config: HostConfig = toml::from_str(@@ -510,6 +510,7 @@ async fn reap_encoded_source_with_timeout(source: EncodedSource, grace_timeout:
510510 current_gop,511511 task,512512 pointer_surface_dimensions: _,513+ encoder_control: _,513514 } = source;514515 // Closing both bounded outputs gives the capture task a chance to kill and515516 // wait for its GStreamer child itself. Abort is only the bounded fallback.@@ -1581,6 +1582,7 @@ async fn serve_control_moq(
15811582 current_gop: mut current_gop_receiver,15821583 task: source_task,15831584 pointer_surface_dimensions,1585+ encoder_control: _encoder_control,15841586 } = match source {15851587 Ok(source) => source,15861588 Err(error) => {@@ -1874,6 +1876,7 @@ async fn serve_media(
18741876 current_gop: mut current_gop_receiver,18751877 task: source_task,18761878 pointer_surface_dimensions,1879+ encoder_control: _encoder_control,18771880 } = match source {18781881 Ok(source) => source,18791882 Err(error) => {@@ -2046,6 +2049,7 @@ async fn serve_media_v2(
20462049 current_gop: mut current_gop_receiver,20472050 task: source_task,20482051 pointer_surface_dimensions,2052+ encoder_control: _encoder_control,20492053 } = match source {20502054 Ok(source) => source,20512055 Err(error) => {@@ -2139,6 +2143,7 @@ async fn serve_media_v3(
21392143 current_gop: mut current_gop_receiver,21402144 task: source_task,21412145 pointer_surface_dimensions,2146+ encoder_control: _encoder_control,21422147 } = match source {21432148 Ok(source) => source,21442149 Err(error) => {@@ -3979,6 +3984,7 @@ mod tests {
39793984 current_gop: gop_receiver,39803985 task,39813986 pointer_surface_dimensions: None,3987+ encoder_control: None,39823988 };39833989 39843990 reap_encoded_source_with_timeout(source, Duration::from_millis(100)).await;@@ -4003,6 +4009,7 @@ mod tests {
40034009 current_gop: gop_receiver,40044010 task,40054011 pointer_surface_dimensions: None,4012+ encoder_control: None,40064013 };40074014 40084015 started_rx.await.unwrap();@@ -3,6 +3,7 @@ use std::ffi::OsString;
33 use std::fs;44 use std::process::{ExitStatus, Stdio};55 use std::sync::Arc;6+use std::sync::atomic::{AtomicU64, Ordering};67 use std::time::{Duration, Instant};78 89 use anyhow::{Context, Result, bail, ensure};@@ -14,7 +15,9 @@ use tokio::sync::watch;
1415 use tracing::{debug, warn};1516 1617 use crate::clock::SessionClock;17-use crate::config::{GamescopePipewireConfig, HostConfig, VaapiRateControl};18+use crate::config::{19+ GamescopeEncoderBackend, GamescopePipewireConfig, HostConfig, VaapiRateControl,20+};1821 1922 2023 use std::os::unix::fs::{FileTypeExt, PermissionsExt};@@ -25,6 +28,165 @@ const MAX_PW_DUMP_OUTPUT: usize = 8 * 1024 * 1024;
2528 const MAX_INSPECT_OUTPUT: usize = 1024 * 1024;2629 const MAX_DIAGNOSTIC_OUTPUT: usize = 512 * 1024;2730 const PREFLIGHT_TIMEOUT: Duration = Duration::from_secs(5);31+const MAX_ENCODER_BITRATE_KBPS: u32 = 100_000;32+33+34+struct RevisedBitrate {35+ revision: u64,36+ kbps: u32,37+}38+39+40+struct EncoderControlDesired {41+ revision: u64,42+ bitrate: Option<RevisedBitrate>,43+ force_keyframe_revision: Option<u64>,44+}45+46+47+pub struct EncoderControlStatus {48+ pub latest_revision: u64,49+ pub applied_bitrate_revision: Option<u64>,50+ pub applied_bitrate_kbps: Option<u32>,51+ pub requested_force_keyframe_revision: Option<u64>,52+ pub acknowledged_force_keyframe_revision: Option<u64>,53+}54+55+/// Bounded latest-state control for an in-process encoder.56+///57+/// Both directions are Tokio watch channels: callers can update control state58+/// arbitrarily often without building a queue, while the encoder applies only59+/// the latest revision. Revisions are process-monotonic and never wrap.60+61+pub struct EncoderControl {62+ next_revision: Arc<AtomicU64>,63+ desired: watch::Sender<EncoderControlDesired>,64+ status: watch::Receiver<EncoderControlStatus>,65+}66+67+68+69+70+71+impl EncoderControl {72+ fn new() -> (73+ Self,74+ watch::Receiver<EncoderControlDesired>,75+ watch::Sender<EncoderControlStatus>,76+ ) {77+ let (desired, desired_rx) = watch::channel(EncoderControlDesired::default());78+ let (status_tx, status) = watch::channel(EncoderControlStatus::default());79+ (80+ Self {81+ next_revision: Arc::new(AtomicU64::new(0)),82+ desired,83+ status,84+ },85+ desired_rx,86+ status_tx,87+ )88+ }89+90+ fn next_revision(&self) -> Result<u64> {91+ self.next_revision92+ .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |revision| {93+ revision.checked_add(1)94+ })95+ .map(|revision| revision + 1)96+ .map_err(|_| anyhow::anyhow!("encoder control revision exhausted"))97+ }98+99+ pub fn request_bitrate_kbps(&self, kbps: u32) -> Result<u64> {100+ ensure!(101+ (1..=MAX_ENCODER_BITRATE_KBPS).contains(&kbps),102+ "encoder bitrate must be between 1 and {MAX_ENCODER_BITRATE_KBPS} kbps"103+ );104+ let revision = self.next_revision()?;105+ self.desired.send_modify(|desired| {106+ desired.revision = revision;107+ desired.bitrate = Some(RevisedBitrate { revision, kbps });108+ });109+ Ok(revision)110+ }111+112+ pub fn request_force_keyframe(&self) -> Result<u64> {113+ let revision = self.next_revision()?;114+ self.desired.send_modify(|desired| {115+ desired.revision = revision;116+ desired.force_keyframe_revision = Some(revision);117+ });118+ Ok(revision)119+ }120+121+ pub fn status(&self) -> EncoderControlStatus {122+ *self.status.borrow()123+ }124+125+ /// Wait until the encoder confirms the exact bitrate revision and exposes126+ /// its property readback. A newer applied revision means this request was127+ /// coalesced before application and therefore fails rather than pretending128+ /// the requested revision committed.129+ pub async fn wait_for_bitrate_applied(130+ &self,131+ revision: u64,132+ timeout: Duration,133+ ) -> Result<EncoderControlStatus> {134+ let mut status = self.status.clone();135+ let wait = async {136+ loop {137+ let snapshot = *status.borrow_and_update();138+ match snapshot.applied_bitrate_revision {139+ Some(applied) if applied == revision => return Ok(snapshot),140+ Some(applied) if applied > revision => bail!(141+ "encoder bitrate revision {revision} was superseded by revision {applied}"142+ ),143+ _ => {}144+ }145+ status146+ .changed()147+ .await148+ .context("encoder control status closed before bitrate was applied")?;149+ }150+ };151+ tokio::time::timeout(timeout, wait)152+ .await153+ .with_context(|| format!("timed out waiting for encoder bitrate revision {revision}"))?154+ }155+156+ /// Wait for a forced-keyframe request to be committed by observing a157+ /// keyframe that also carries codec configuration.158+ pub async fn wait_for_force_keyframe_acknowledged(159+ &self,160+ revision: u64,161+ timeout: Duration,162+ ) -> Result<EncoderControlStatus> {163+ let mut status = self.status.clone();164+ let wait = async {165+ loop {166+ let snapshot = *status.borrow_and_update();167+ match snapshot.acknowledged_force_keyframe_revision {168+ Some(acknowledged) if acknowledged == revision => return Ok(snapshot),169+ Some(acknowledged) if acknowledged > revision => bail!(170+ "force-keyframe revision {revision} was superseded by revision {acknowledged}"171+ ),172+ _ => {}173+ }174+ if snapshot175+ .requested_force_keyframe_revision176+ .is_some_and(|requested| requested > revision)177+ {178+ bail!("force-keyframe revision {revision} was coalesced before application");179+ }180+ status.changed().await.context(181+ "encoder control status closed before forced keyframe was acknowledged",182+ )?;183+ }184+ };185+ tokio::time::timeout(timeout, wait)186+ .await187+ .with_context(|| format!("timed out waiting for force-keyframe revision {revision}"))?188+ }189+}28190 29191 30192 pub struct EncodedFrame {@@ -57,6 +219,7 @@ pub struct EncodedSource {
57219 pub(crate) current_gop: watch::Receiver<Option<EncodedGop>>,58220 pub task: tokio::task::JoinHandle<Result<()>>,59221 pub pointer_surface_dimensions: Option<PointerSurfaceDimensions>,222+ pub encoder_control: Option<EncoderControl>,60223 }61224 62225 pub fn spawn_test_pattern(config: HostConfig, session_clock: SessionClock) -> EncodedSource {@@ -70,6 +233,7 @@ pub fn spawn_test_pattern(config: HostConfig, session_clock: SessionClock) -> En
70233 current_gop,71234 task,72235 pointer_surface_dimensions: None,236+ encoder_control: None,73237 }74238 }75239 @@ -92,13 +256,19 @@ async fn preflight_gamescope_static(config: &HostConfig) -> Result<()> {
92256 93257 for (name, path) in [94258 ("pw_dump_path", &pipewire.pw_dump_path),95- ("gst_launch_path", &pipewire.gst_launch_path),96259 ("gst_inspect_path", &pipewire.gst_inspect_path),97260 ] {98261 validate_executable(name, path)?;99262 }263+ if pipewire.encoder_backend == GamescopeEncoderBackend::ExternalGstLaunch {264+ validate_executable("gst_launch_path", &pipewire.gst_launch_path)?;265+ }100266 validate_amd_render_node(&pipewire.vaapi_render_node)?;101267 268+ let terminal_sink = match pipewire.encoder_backend {269+ GamescopeEncoderBackend::ExternalGstLaunch => "fdsink",270+ GamescopeEncoderBackend::InProcessGstreamer => "appsink",271+ };102272 for element in [103273 "pipewiresrc",104274 "queue",@@ -107,7 +277,7 @@ async fn preflight_gamescope_static(config: &HostConfig) -> Result<()> {
107277 "videorate",108278 pipewire.vaapi_encoder.as_str(),109279 "h264parse",110- "fdsink",280+ terminal_sink,111281 ] {112282 let mut command = Command::new(&pipewire.gst_inspect_path);113283 command.arg(element).env("LC_ALL", "C");@@ -144,7 +314,7 @@ pub async fn spawn_gamescope_pipewire(
144314 session_clock: SessionClock,145315 ) -> Result<EncodedSource> {146316 let preflight = preflight_gamescope_pipewire(&config).await?;147- spawn_gamescope_pipewire_with_target(config, session_clock, preflight)317+ spawn_gamescope_pipewire_with_preflight(config, session_clock, preflight)148318 }149319 150320 /// Start capture after `serve` has completed the static executable/GstVA@@ -155,7 +325,32 @@ pub async fn spawn_gamescope_pipewire_after_static_preflight(
155325 session_clock: SessionClock,156326 ) -> Result<EncodedSource> {157327 let preflight = resolve_gamescope_pipewire_target(&config).await?;158- spawn_gamescope_pipewire_with_target(config, session_clock, preflight)328+ spawn_gamescope_pipewire_with_preflight(config, session_clock, preflight)329+}330+331+fn spawn_gamescope_pipewire_with_preflight(332+ config: HostConfig,333+ session_clock: SessionClock,334+ preflight: GamescopePreflight,335+) -> Result<EncodedSource> {336+ match gamescope_config(&config)?.encoder_backend {337+ GamescopeEncoderBackend::ExternalGstLaunch => {338+ spawn_gamescope_pipewire_with_target(config, session_clock, preflight)339+ }340+ GamescopeEncoderBackend::InProcessGstreamer => {341+ 342+ {343+ spawn_gamescope_pipewire_in_process_with_target(config, session_clock, preflight)344+ }345+ 346+ {347+ let _ = (session_clock, preflight);348+ bail!(349+ "gamescope_pipewire.encoder_backend=in-process-gstreamer requires a Linux Sigil build with the in-process-gstreamer feature"350+ )351+ }352+ }353+ }159354 }160355 161356 fn spawn_gamescope_pipewire_with_target(@@ -181,9 +376,113 @@ fn spawn_gamescope_pipewire_with_target(
181376 current_gop,182377 task,183378 pointer_surface_dimensions: Some(preflight.pointer_surface_dimensions),379+ encoder_control: None,380+ })381+}382+383+384+fn spawn_gamescope_pipewire_in_process_with_target(385+ config: HostConfig,386+ session_clock: SessionClock,387+ preflight: GamescopePreflight,388+) -> Result<EncodedSource> {389+ let description = gamescope_in_process_pipeline_description(&config, &preflight.target_object)?;390+ let max_gop_frames = interactive_gop_frames(config.framerate) as usize;391+ let expected_device_path = Some(392+ gamescope_config(&config)?393+ .vaapi_render_node394+ .to_string_lossy()395+ .into_owned(),396+ );397+ spawn_in_process_pipeline(398+ description,399+ session_clock,400+ max_gop_frames,401+ Some(preflight.pointer_surface_dimensions),402+ expected_device_path,403+ )404+}405+406+407+fn spawn_in_process_pipeline(408+ description: String,409+ session_clock: SessionClock,410+ max_gop_frames: usize,411+ pointer_surface_dimensions: Option<PointerSurfaceDimensions>,412+ expected_device_path: Option<String>,413+) -> Result<EncodedSource> {414+ let (sender, receiver) = watch::channel(None);415+ let (current_gop_sender, current_gop) = watch::channel(None);416+ let (control, desired, status) = EncoderControl::new();417+ let task = tokio::spawn(async move {418+ tokio::task::spawn_blocking(move || {419+ run_in_process_pipeline(420+ &description,421+ session_clock,422+ sender,423+ current_gop_sender,424+ max_gop_frames,425+ desired,426+ status,427+ expected_device_path.as_deref(),428+ )429+ })430+ .await431+ .context("in-process GStreamer worker panicked")?432+ });433+ Ok(EncodedSource {434+ frames: receiver,435+ current_gop,436+ task,437+ pointer_surface_dimensions,438+ encoder_control: Some(control),184439 })185440 }186441 442+443+fn gamescope_in_process_pipeline_description(444+ config: &HostConfig,445+ target_object: &str,446+) -> Result<String> {447+ let pipewire = gamescope_config(config)?;448+ validate_pipewire_target_object(target_object)?;449+ ensure!(450+ pipewire.rate_control == VaapiRateControl::Cbr,451+ "in-process GStreamer requires CBR so bitrate can be changed while playing"452+ );453+ let bitrate = pipewire454+ .bitrate_kbps455+ .context("CBR bitrate is missing after validation")?;456+ let raw_caps = format!(457+ "video/x-raw,format=NV12,width={},height={},framerate={}/1",458+ config.width, config.height, config.framerate459+ );460+ Ok(format!(461+ "pipewiresrc do-timestamp=true min-buffers=1 max-buffers=4 use-bufferpool=false target-object={target_object} \462+ ! queue max-size-buffers=1 max-size-bytes=0 max-size-time=0 leaky=downstream \463+ ! videorate drop-only=true max-rate={} \464+ ! videoconvert ! videoscale ! {raw_caps} \465+ ! {} name=sigil_encoder rate-control=cbr bitrate={bitrate} target-usage=7 key-int-max={} b-frames=0 ref-frames=1 aud=true \466+ ! h264parse config-interval=-1 \467+ ! video/x-h264,stream-format=byte-stream,alignment=au \468+ ! appsink name=sigil_sink max-buffers=1 drop=false sync=false",469+ config.framerate,470+ pipewire.vaapi_encoder,471+ interactive_gop_frames(config.framerate),472+ ))473+}474+475+476+fn validate_pipewire_target_object(target_object: &str) -> Result<()> {477+ ensure!(478+ !target_object.is_empty()479+ && target_object.len() <= 32480+ && target_object.bytes().all(|byte| byte.is_ascii_digit()),481+ "resolved PipeWire object.serial is invalid"482+ );483+ Ok(())484+}485+187486 async fn run_gamescope_pipewire(188487 config: HostConfig,189488 session_clock: SessionClock,@@ -330,7 +629,8 @@ async fn forward_annex_b_stream<R>(
330629 where331630 R: AsyncRead + Unpin,332631 {333- let mut sequence = 0_u64;632+ let mut publisher =633+ AccessUnitPublisher::new(session_clock, sender, current_gop_sender, max_gop_frames);334634 let mut parser = AnnexBAccessUnitParser::default();335635 let mut chunk = [0_u8; 16 * 1024];336636 @@ -344,28 +644,8 @@ where
344644 }345645 346646 for access_unit in parser.push(&chunk[..count])? {347- let observed_at = Instant::now();348- let capture_timestamp_micros = session_clock.micros_at(observed_at);349- let presentation_timestamp_micros =350- i64::try_from(capture_timestamp_micros).unwrap_or(i64::MAX);351- let frame = EncodedFrame {352- sequence,353- capture_timestamp_micros,354- presentation_timestamp_micros,355- observed_at,356- keyframe: is_h264_keyframe(&access_unit),357- codec_config: has_h264_codec_config(&access_unit),358- data: Arc::from(access_unit),359- };360- sequence = sequence.saturating_add(1);361- publish_encoded_frame(362- sender,363- current_gop_sender,364- frame,365- max_gop_frames,366- MAX_CURRENT_GOP_BYTES,367- );368- if sender.is_closed() && current_gop_sender.is_closed() {647+ publisher.publish(access_unit)?;648+ if publisher.receivers_closed() {369649 debug!("encoded source has no receivers");370650 return Ok(());371651 }@@ -373,6 +653,302 @@ where
373653 }374654 }375655 656+struct AccessUnitPublisher<'a> {657+ session_clock: SessionClock,658+ sender: &'a watch::Sender<Option<EncodedFrame>>,659+ current_gop_sender: &'a watch::Sender<Option<EncodedGop>>,660+ max_gop_frames: usize,661+ sequence: u64,662+}663+664+impl<'a> AccessUnitPublisher<'a> {665+ fn new(666+ session_clock: SessionClock,667+ sender: &'a watch::Sender<Option<EncodedFrame>>,668+ current_gop_sender: &'a watch::Sender<Option<EncodedGop>>,669+ max_gop_frames: usize,670+ ) -> Self {671+ Self {672+ session_clock,673+ sender,674+ current_gop_sender,675+ max_gop_frames,676+ sequence: 0,677+ }678+ }679+680+ fn publish(&mut self, access_unit: Vec<u8>) -> Result<bool> {681+ ensure!(!access_unit.is_empty(), "encoded access unit is empty");682+ ensure!(683+ access_unit.len() <= MAX_ENCODE_BUFFER,684+ "encoded access unit exceeds {MAX_ENCODE_BUFFER} bytes"685+ );686+ let observed_at = Instant::now();687+ let capture_timestamp_micros = self.session_clock.micros_at(observed_at);688+ let presentation_timestamp_micros =689+ i64::try_from(capture_timestamp_micros).unwrap_or(i64::MAX);690+ let keyframe = is_h264_keyframe(&access_unit);691+ let codec_config = has_h264_codec_config(&access_unit);692+ let frame = EncodedFrame {693+ sequence: self.sequence,694+ capture_timestamp_micros,695+ presentation_timestamp_micros,696+ observed_at,697+ keyframe,698+ codec_config,699+ data: Arc::from(access_unit),700+ };701+ self.sequence = self702+ .sequence703+ .checked_add(1)704+ .context("encoded frame sequence exhausted")?;705+ publish_encoded_frame(706+ self.sender,707+ self.current_gop_sender,708+ frame,709+ self.max_gop_frames,710+ MAX_CURRENT_GOP_BYTES,711+ );712+ Ok(keyframe && codec_config)713+ }714+715+ fn receivers_closed(&self) -> bool {716+ self.sender.is_closed() && self.current_gop_sender.is_closed()717+ }718+}719+720+721+fn run_in_process_pipeline(722+ description: &str,723+ session_clock: SessionClock,724+ sender: watch::Sender<Option<EncodedFrame>>,725+ current_gop_sender: watch::Sender<Option<EncodedGop>>,726+ max_gop_frames: usize,727+ mut desired: watch::Receiver<EncoderControlDesired>,728+ status: watch::Sender<EncoderControlStatus>,729+ expected_device_path: Option<&str>,730+) -> Result<()> {731+ use gstreamer as gst;732+ use gstreamer::prelude::*;733+ use gstreamer_app as gst_app;734+ gst::init().context("initializing in-process GStreamer")?;735+ let pipeline = gst::parse::launch(description)736+ .context("parsing in-process GStreamer pipeline")?737+ .downcast::<gst::Pipeline>()738+ .map_err(|_| {739+ anyhow::anyhow!("in-process GStreamer description did not create a pipeline")740+ })?;741+ let encoder = pipeline742+ .by_name("sigil_encoder")743+ .context("in-process GStreamer pipeline has no named encoder")?;744+ let sink = pipeline745+ .by_name("sigil_sink")746+ .context("in-process GStreamer pipeline has no named appsink")?747+ .downcast::<gst_app::AppSink>()748+ .map_err(|_| anyhow::anyhow!("named in-process sink is not an appsink"))?;749+ sink.set_max_buffers(1);750+ // Encoded dependent AUs must not be dropped here: the raw queue before the751+ // encoder is the only latest-frame boundary. This one-buffer sink applies752+ // bounded backpressure until the publishing loop consumes the AU.753+ sink.set_drop(false);754+ sink.set_sync(false);755+ validate_mutable_playing_bitrate(&encoder)?;756+757+ pipeline758+ .set_state(gst::State::Playing)759+ .context("starting in-process GStreamer pipeline")?;760+ let result = (|| {761+ let (state_change, state, pending) = pipeline.state(gst::ClockTime::from_seconds(5));762+ state_change.context("in-process GStreamer pipeline failed while waiting for PLAYING")?;763+ ensure!(764+ state == gst::State::Playing,765+ "in-process GStreamer pipeline did not reach PLAYING within 5 seconds (state {state:?}, pending {pending:?})"766+ );767+ if let Some(expected_device_path) = expected_device_path {768+ validate_active_encoder_device_path(&encoder, expected_device_path)?;769+ }770+ let bus = pipeline771+ .bus()772+ .context("in-process GStreamer pipeline has no bus")?;773+ let mut publisher =774+ AccessUnitPublisher::new(session_clock, &sender, ¤t_gop_sender, max_gop_frames);775+ let mut applied_desired_revision = 0_u64;776+ let mut pending_force_keyframe_revision = None;777+778+ loop {779+ if let Some(message) = bus.timed_pop(gst::ClockTime::ZERO) {780+ match message.view() {781+ gst::MessageView::Error(error) => {782+ bail!(783+ "in-process GStreamer pipeline error from {:?}: {} ({:?})",784+ error.src().map(|source| source.path_string()),785+ error.error(),786+ error.debug()787+ );788+ }789+ gst::MessageView::Eos(_) => {790+ bail!("in-process GStreamer pipeline ended unexpectedly");791+ }792+ _ => {}793+ }794+ }795+796+ let desired_state = *desired.borrow_and_update();797+ if desired_state.revision > applied_desired_revision {798+ apply_encoder_control(799+ &encoder,800+ &sink,801+ desired_state,802+ &status,803+ &mut pending_force_keyframe_revision,804+ )?;805+ applied_desired_revision = desired_state.revision;806+ }807+808+ if let Some(sample) = sink.try_pull_sample(gst::ClockTime::from_mseconds(50)) {809+ let buffer = sample810+ .buffer()811+ .context("in-process appsink sample has no buffer")?;812+ let map = buffer813+ .map_readable()814+ .context("mapping in-process encoded access unit")?;815+ ensure!(816+ map.as_slice().len() <= MAX_ENCODE_BUFFER,817+ "in-process encoded access unit exceeds {MAX_ENCODE_BUFFER} bytes"818+ );819+ let configured_idr = publisher.publish(map.as_slice().to_vec())?;820+ acknowledge_force_keyframe_if_configured_idr(821+ configured_idr,822+ &mut pending_force_keyframe_revision,823+ &status,824+ );825+ } else if sink.is_eos() {826+ bail!("in-process GStreamer appsink reached EOS unexpectedly");827+ }828+829+ if publisher.receivers_closed() {830+ debug!("in-process encoded source has no receivers");831+ return Ok(());832+ }833+ }834+ })();835+ if let Err(error) = pipeline.set_state(gst::State::Null) {836+ warn!(%error, "failed to stop in-process GStreamer pipeline cleanly");837+ }838+ result839+}840+841+842+fn validate_active_encoder_device_path(843+ encoder: &gstreamer::Element,844+ expected_device_path: &str,845+) -> Result<()> {846+ use gstreamer as gst;847+ use gstreamer::prelude::*;848+849+ let property = encoder850+ .find_property("device-path")851+ .context("configured in-process VA encoder has no device-path property")?;852+ ensure!(853+ property.flags().contains(gst::glib::ParamFlags::READABLE),854+ "configured in-process VA encoder device-path property is not readable"855+ );856+ let observed_device_path = encoder.property::<Option<String>>("device-path");857+ ensure!(858+ observed_device_path.as_deref() == Some(expected_device_path),859+ "configured in-process VA encoder uses device-path {observed_device_path:?}, expected {expected_device_path:?}"860+ );861+ Ok(())862+}863+864+865+fn validate_mutable_playing_bitrate(encoder: &gstreamer::Element) -> Result<()> {866+ use gstreamer as gst;867+ use gstreamer::prelude::*;868+869+ let property = encoder870+ .find_property("bitrate")871+ .context("configured in-process encoder has no bitrate property")?;872+ ensure!(873+ property.flags().contains(gst::glib::ParamFlags::WRITABLE),874+ "configured in-process encoder bitrate property is not writable"875+ );876+ ensure!(877+ property.flags().contains(gst::PARAM_FLAG_MUTABLE_PLAYING),878+ "configured in-process encoder bitrate property is not mutable while playing"879+ );880+ Ok(())881+}882+883+884+fn apply_encoder_control(885+ encoder: &gstreamer::Element,886+ sink: &gstreamer_app::AppSink,887+ desired: EncoderControlDesired,888+ status: &watch::Sender<EncoderControlStatus>,889+ pending_force_keyframe_revision: &mut Option<u64>,890+) -> Result<()> {891+ use gstreamer as gst;892+ use gstreamer::prelude::*;893+ use gstreamer_video as gst_video;894+895+ let current = *status.borrow();896+ if let Some(bitrate) = desired.bitrate897+ && current898+ .applied_bitrate_revision899+ .is_none_or(|revision| bitrate.revision > revision)900+ {901+ encoder.set_property("bitrate", bitrate.kbps);902+ let readback = encoder.property::<u32>("bitrate");903+ ensure!(904+ readback == bitrate.kbps,905+ "encoder bitrate readback mismatch: requested {} kbps, observed {readback} kbps",906+ bitrate.kbps907+ );908+ status.send_modify(|status| {909+ status.applied_bitrate_revision = Some(bitrate.revision);910+ status.applied_bitrate_kbps = Some(readback);911+ });912+ }913+914+ if let Some(revision) = desired.force_keyframe_revision915+ && current916+ .requested_force_keyframe_revision917+ .is_none_or(|requested| revision > requested)918+ {919+ let event = gst_video::UpstreamForceKeyUnitEvent::builder()920+ .all_headers(true)921+ .build();922+ let sink_pad = sink923+ .static_pad("sink")924+ .context("configured in-process appsink has no sink pad")?;925+ ensure!(926+ sink_pad.push_event(event),927+ "in-process encoder rejected upstream ForceKeyUnit"928+ );929+ *pending_force_keyframe_revision = Some(revision);930+ status.send_modify(|status| {931+ status.requested_force_keyframe_revision = Some(revision);932+ });933+ }934+935+ status.send_modify(|status| status.latest_revision = desired.revision);936+ Ok(())937+}938+939+940+fn acknowledge_force_keyframe_if_configured_idr(941+ configured_idr: bool,942+ pending_revision: &mut Option<u64>,943+ status: &watch::Sender<EncoderControlStatus>,944+) {945+ if configured_idr && let Some(revision) = pending_revision.take() {946+ status.send_modify(|status| {947+ status.acknowledged_force_keyframe_revision = Some(revision);948+ });949+ }950+}951+376952 fn publish_encoded_frame(377953 sender: &watch::Sender<Option<EncodedFrame>>,378954 current_gop_sender: &watch::Sender<Option<EncodedGop>>,@@ -1079,7 +1655,7 @@ fn h264_nal_types(data: &[u8]) -> impl Iterator<Item = u8> + '_ {
10791655 10801656 mod tests {10811657 use super::*;1082- use crate::config::{InputMode, VideoSource};1658+ use crate::config::{GamescopeEncoderBackend, InputMode, VideoSource};10831659 use std::collections::BTreeMap;10841660 use std::path::PathBuf;10851661 @@ -1096,6 +1672,7 @@ mod tests {
10961672 uinput: None,10971673 ffmpeg_path: PathBuf::from("/usr/bin/ffmpeg"),10981674 gamescope_pipewire: Some(GamescopePipewireConfig {1675+ encoder_backend: GamescopeEncoderBackend::ExternalGstLaunch,10991676 xwayland_display: None,11001677 node_name: "gamescope".into(),11011678 media_class: "Video/Source".into(),@@ -1116,6 +1693,167 @@ mod tests {
11161693 }11171694 }11181695 1696+ 1697+ async fn encoder_control_coalesces_latest_state_and_acknowledges_only_configured_idr() {1698+ let (control, mut desired, status) = EncoderControl::new();1699+1700+ let superseded_bitrate_revision = control.request_bitrate_kbps(4_000).unwrap();1701+ let bitrate_revision = control.request_bitrate_kbps(8_000).unwrap();1702+ let force_keyframe_revision = control.request_force_keyframe().unwrap();1703+1704+ assert!(superseded_bitrate_revision < bitrate_revision);1705+ assert!(bitrate_revision < force_keyframe_revision);1706+ let latest = *desired.borrow_and_update();1707+ assert_eq!(latest.revision, force_keyframe_revision);1708+ assert_eq!(1709+ latest.bitrate,1710+ Some(RevisedBitrate {1711+ revision: bitrate_revision,1712+ kbps: 8_000,1713+ })1714+ );1715+ assert_eq!(1716+ latest.force_keyframe_revision,1717+ Some(force_keyframe_revision)1718+ );1719+1720+ status.send_modify(|status| {1721+ status.latest_revision = latest.revision;1722+ status.applied_bitrate_revision = Some(bitrate_revision);1723+ status.applied_bitrate_kbps = Some(8_000);1724+ status.requested_force_keyframe_revision = Some(force_keyframe_revision);1725+ });1726+ let mut pending = Some(force_keyframe_revision);1727+ acknowledge_force_keyframe_if_configured_idr(false, &mut pending, &status);1728+ assert_eq!(pending, Some(force_keyframe_revision));1729+ assert_eq!(control.status().acknowledged_force_keyframe_revision, None);1730+1731+ acknowledge_force_keyframe_if_configured_idr(true, &mut pending, &status);1732+ assert_eq!(pending, None);1733+ let applied = control1734+ .wait_for_bitrate_applied(bitrate_revision, Duration::from_millis(10))1735+ .await1736+ .unwrap();1737+ assert_eq!(applied.applied_bitrate_kbps, Some(8_000));1738+ let acknowledged = control1739+ .wait_for_force_keyframe_acknowledged(1740+ force_keyframe_revision,1741+ Duration::from_millis(10),1742+ )1743+ .await1744+ .unwrap();1745+ assert_eq!(1746+ acknowledged.acknowledged_force_keyframe_revision,1747+ Some(force_keyframe_revision)1748+ );1749+ assert!(control.request_bitrate_kbps(0).is_err());1750+ assert!(1751+ control1752+ .request_bitrate_kbps(MAX_ENCODER_BITRATE_KBPS + 1)1753+ .is_err()1754+ );1755+1756+ let (closed_control, _desired, closed_status) = EncoderControl::new();1757+ let closed_revision = closed_control.request_force_keyframe().unwrap();1758+ drop(closed_status);1759+ assert!(1760+ closed_control1761+ .wait_for_force_keyframe_acknowledged(closed_revision, Duration::from_millis(10))1762+ .await1763+ .unwrap_err()1764+ .to_string()1765+ .contains("status closed")1766+ );1767+ }1768+1769+ 1770+ 1771+ 1772+ async fn in_process_gstreamer_x264_smoke() {1773+ let description = "videotestsrc is-live=true pattern=ball \1774+ ! video/x-raw,width=320,height=180,framerate=30/1 \1775+ ! queue max-size-buffers=1 max-size-bytes=0 max-size-time=0 leaky=downstream \1776+ ! videoconvert \1777+ ! x264enc name=sigil_encoder bitrate=2000 tune=zerolatency speed-preset=ultrafast key-int-max=15 bframes=0 byte-stream=true aud=true \1778+ ! h264parse config-interval=-1 \1779+ ! video/x-h264,stream-format=byte-stream,alignment=au \1780+ ! appsink name=sigil_sink max-buffers=1 drop=false sync=false"1781+ .to_owned();1782+ let mut source =1783+ spawn_in_process_pipeline(description, SessionClock::start(), 15, None, None).unwrap();1784+ let control = source1785+ .encoder_control1786+ .clone()1787+ .expect("x264 source has no encoder control");1788+1789+ tokio::time::timeout(Duration::from_secs(10), async {1790+ loop {1791+ source.frames.changed().await.unwrap();1792+ if source1793+ .frames1794+ .borrow()1795+ .as_ref()1796+ .is_some_and(|frame| frame.keyframe && frame.codec_config)1797+ {1798+ break;1799+ }1800+ }1801+ })1802+ .await1803+ .expect("x264 did not publish an initial configured IDR");1804+1805+ let bitrate_revision = control.request_bitrate_kbps(1_500).unwrap();1806+ let force_keyframe_revision = control.request_force_keyframe().unwrap();1807+ let bitrate_status = control1808+ .wait_for_bitrate_applied(bitrate_revision, Duration::from_secs(10))1809+ .await1810+ .expect("x264 did not apply the bitrate revision");1811+ assert_eq!(bitrate_status.applied_bitrate_kbps, Some(1_500));1812+ control1813+ .wait_for_force_keyframe_acknowledged(force_keyframe_revision, Duration::from_secs(10))1814+ .await1815+ .expect("x264 did not acknowledge a configured forced IDR");1816+1817+ {1818+ let current_gop = source.current_gop.borrow();1819+ let gop = current_gop1820+ .as_ref()1821+ .expect("x264 did not retain a current GOP");1822+ assert!(gop.frames[0].keyframe && gop.frames[0].codec_config);1823+ assert!(gop.frames.len() <= 15);1824+ }1825+ let EncodedSource {1826+ frames,1827+ current_gop,1828+ task,1829+ ..1830+ } = source;1831+ drop(frames);1832+ drop(current_gop);1833+ tokio::time::timeout(Duration::from_secs(2), task)1834+ .await1835+ .expect("x264 pipeline did not stop after its receivers closed")1836+ .expect("x264 pipeline task panicked")1837+ .expect("x264 pipeline returned an error");1838+ }1839+1840+ 1841+ 1842+ fn in_process_pipeline_drops_only_before_encoding() {1843+ let mut config = gamescope_config();1844+ config.gamescope_pipewire.as_mut().unwrap().encoder_backend =1845+ GamescopeEncoderBackend::InProcessGstreamer;1846+ let description = gamescope_in_process_pipeline_description(&config, "1234").unwrap();1847+1848+ assert!(description.contains(1849+ "queue max-size-buffers=1 max-size-bytes=0 max-size-time=0 leaky=downstream"1850+ ));1851+ assert!(1852+ description.contains("appsink name=sigil_sink max-buffers=1 drop=false sync=false")1853+ );1854+ assert!(!description.contains("appsink name=sigil_sink max-buffers=1 drop=true"));1855+ }1856+11191857 11201858 fn splits_access_units_on_aud() {11211859 let first = [0, 0, 0, 1, 9, 0x10, 0, 0, 1, 5, 1, 2, 3];@@ -44,6 +44,13 @@ low-latency encoder properties do not match. This implementation still needs
4444 the attached-display and headless appliance gates below before it is proven on4545 the target Bazzite image.4646 47+The proven compatibility backend launches the video pipeline through the48+configured `gst-launch-1.0` executable and remains the default when49+`encoder_backend` is omitted. `encoder_backend = "in-process-gstreamer"` is an50+explicit test opt-in. Sigil rejects that setting unless it is a Linux binary51+built with the matching Cargo feature; it must never silently fall back to the52+external backend. The public release package does not enable this feature yet.53+4754 The host identity file is part of the normal daemon design. The client-side4855 `--dev-connect` passkey bypass is accepted by debug builds and by an explicitly4956 feature-gated optimized demo build. Ordinary release clients reject that@@ -361,6 +368,7 @@ Inside the container:
361368 ```bash362369 sudo dnf install -y --setopt=install_weak_deps=False \363370 clang cmake curl gcc gcc-c++ git make \371+ gstreamer1-devel gstreamer1-plugins-base-devel \364372 openssl-devel pipewire-devel pkgconf-pkg-config systemd-devel365373 366374 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \@@ -369,6 +377,11 @@ source "$HOME/.cargo/env"
369377 rpm -qa | sort > "$HOME/sigil-dev-rpms.txt"370378 ```371379 380+`gstreamer1-devel` supplies the core API and381+`gstreamer1-plugins-base-devel` supplies the app/video APIs linked by the382+optional in-process backend. They are build dependencies only; do not copy383+headers or development libraries into the Sigil runtime archive.384+372385 The toolchain pinned by the repository must be Rust 1.91 or newer. Save the373386 container digest and exact RPM NEVRAs with the evidence. Distrobox is374387 host-integrated and is not a security sandbox. Build only the protocol and pure@@ -943,8 +956,21 @@ gst-inspect-1.0 | awk '
943956 -e '/^[[:space:]]*device-path[[:space:]]*:/,+4p' \944957 -e '/^[[:space:]]*rate-control[[:space:]]*:/,+20p'945958 done959+960+for element in appsink pipewiresrc queue videoconvert videoscale videorate h264parse fdsink; do961+ gst-inspect-1.0 "$element" >/dev/null962+done963+964+rpm -q gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-bad-free946965 ```947966 967+Record the exact runtime package NEVRAs. `appsink` is mandatory for a binary968+built with the in-process feature, and the package installer must resolve its969+linked GStreamer core/app/video libraries with `ldd`. `pipewiresrc`,970+conversion/parser elements, and the chosen GstVA plugin remain required for971+either video backend. The external `gst-launch` path is also still used for972+audio capture when audio is enabled.973+948974 ### Grant the gaming user only the uinput capability949975 950976 Do not add the gaming user to the broad `input` group and do not run the host@@ -1047,6 +1073,7 @@ xwayland_display = ":0"
10471073 pw_dump_path = "$pw_dump_path"10481074 gst_launch_path = "$gst_launch_path"10491075 gst_inspect_path = "$gst_inspect_path"1076+encoder_backend = "external-gst-launch"10501077 vaapi_encoder = "$vaapi_encoder"10511078 vaapi_render_node = "$render_node"10521079 rate_control = "cbr"@@ -1055,6 +1082,21 @@ EOF
10551082 chmod 0600 "$probe_config"10561083 ```10571084 1085+Omitting `encoder_backend` has the same external default. After the external1086+baseline passes, a locally built Linux binary containing the feature can use:1087+1088+```toml1089+encoder_backend = "in-process-gstreamer"1090+```1091+1092+That selection is not an availability hint: a binary without the feature, a1093+missing `appsink`, or an unavailable encoder factory makes preflight fail. The1094+foundation currently requires CBR, and a bitrate property that is not mutable1095+while PLAYING also fails the control preflight. Runtime bitrate control is1096+acknowledged only after exact property readback. CQP remains on the external1097+compatibility backend until its in-process force-keyframe path is implemented1098+and proven separately.1099+10581100 The configured `:0` is a bootstrap connection, not a fixed input target.10591101 Gamescope may move mouse focus between its `:0` and `:1` Xwayland servers.10601102 Sigil reads `GAMESCOPE_MOUSE_FOCUS_DISPLAY` from the bootstrap root, reconnects@@ -1178,6 +1220,10 @@ rate_control = "cqp"
11781220 quantizer = 2411791221 ```11801222 1223+Keep `encoder_backend = "external-gst-launch"` for CQP. The current1224+in-process foundation rejects CQP rather than implying runtime control that it1225+does not implement.1226+11811227 If `pw-dump` exposes an additional stable GPU identity on the Gamescope node,11821228 add it as an exact match; do not add a changing global ID, node ID, client ID,11831229 or `object.serial`:@@ -15,6 +15,52 @@ optional() {
1515 fi1616 }1717 18+gstreamer_inventory() {19+ if ! command -v gst-inspect-1.0 >/dev/null 2>&1; then20+ echo 'gstreamer_inventory=unavailable'21+ return22+ fi23+24+ local element25+ for element in appsink pipewiresrc queue videoconvert videoscale videorate h264parse fdsink; do26+ if gst-inspect-1.0 "$element" >/dev/null 2>&1; then27+ printf 'gstreamer_element=%s status=present\n' "$element"28+ else29+ printf 'gstreamer_element=%s status=missing\n' "$element"30+ fi31+ done32+ gst-inspect-1.0 pipewiresrc 2>/dev/null || true33+ while IFS= read -r encoder; do34+ printf 'gstreamer_va_h264_factory=%s\n' "$encoder"35+ gst-inspect-1.0 "$encoder" 2>/dev/null || true36+ done < <(37+ gst-inspect-1.0 2>/dev/null | awk '38+ $2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {39+ sub(/:$/, "", $2)40+ print $241+ }42+ '43+ )44+}45+46+gstreamer_development_inventory() {47+ if ! command -v pkg-config >/dev/null 2>&1; then48+ echo 'gstreamer_development_metadata=unavailable'49+ return50+ fi51+52+ local module53+ local version54+ for module in gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0; do55+ if version="$(pkg-config --modversion "$module" 2>/dev/null)"; then56+ printf 'gstreamer_development_module=%s status=present version=%s\n' \57+ "$module" "$version"58+ else59+ printf 'gstreamer_development_module=%s status=missing\n' "$module"60+ fi61+ done62+}63+1864 cold_boot_failures=01965 cold_boot_insufficient=02066 @@ -401,27 +447,10 @@ section capture_backends
401447 if command -v ffmpeg >/dev/null 2>&1; then402448 ffmpeg -hide_banner -devices 2>/dev/null || true403449 fi404-if command -v gst-inspect-1.0 >/dev/null 2>&1; then405- for element in pipewiresrc queue videoconvert videoscale videorate h264parse fdsink; do406- if gst-inspect-1.0 "$element" >/dev/null 2>&1; then407- printf 'gstreamer_element=%s status=present\n' "$element"408- else409- printf 'gstreamer_element=%s status=missing\n' "$element"410- fi411- done412- gst-inspect-1.0 pipewiresrc 2>/dev/null || true413- while IFS= read -r encoder; do414- printf 'gstreamer_va_h264_factory=%s\n' "$encoder"415- gst-inspect-1.0 "$encoder" 2>/dev/null || true416- done < <(417- gst-inspect-1.0 2>/dev/null | awk '418- $2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {419- sub(/:$/, "", $2)420- print $2421- }422- '423- )424-fi450+gstreamer_inventory451+452+section gstreamer_development453+gstreamer_development_inventory425454 426455 section gamescope_pipewire427456 systemctl --user status pipewire.service pipewire.socket wireplumber.service --no-pager || true@@ -23,6 +23,9 @@ an isolated Cargo target directory. The package includes atomic install,
2323 upgrade/rollback support, the systemd user unit, PipeWire sink, staged udev2424 rule, complete checksums, and build provenance. It never includes the source2525 tree, host identity, hardware configuration, environment files, or evidence.26+Product mode intentionally uses Sigil's default feature set; the opt-in27+in-process-gstreamer backend is excluded until its native/cross-build gate is28+approved.2629 2730 Options:2831 --output PATH New .tar.gz bundle path (required; must not exist)@@ -0,0 +1,86 @@
1+2+3+set -euo pipefail4+5+script_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"6+temp_parent="${TMPDIR:-/tmp}"7+temp_parent="${temp_parent%/}"8+temp_root="$(mktemp -d "$temp_parent/sigil-gstreamer-inventory.XXXXXX")"9+10+cleanup() {11+ local status=$?12+ trap - EXIT INT TERM HUP13+ case "$temp_root" in14+ "$temp_parent"/sigil-gstreamer-inventory.??????) rm -rf -- "$temp_root" ;;15+ esac16+ exit "$status"17+}18+trap cleanup EXIT INT TERM HUP19+20+export SIGIL_INVENTORY_SOURCE_ONLY=121+# shellcheck source=../bazzite-inventory.sh22+# shellcheck disable=SC109123+source "$script_dir/../bazzite-inventory.sh"24+25+fail() {26+ printf 'FAIL: %s\n' "$1" >&227+ exit 128+}29+30+install -d -m 0700 "$temp_root/bin"31+cat >"$temp_root/bin/gst-inspect-1.0" <<'EOF'32+#!/usr/bin/env bash33+set -euo pipefail34+case "${1:-}" in35+ appsink)36+ [[ "${SIGIL_TEST_APPSINK:-present}" == present ]]37+ ;;38+ pipewiresrc)39+ printf 'Factory Details: pipewiresrc fixture\n'40+ ;;41+ queue|videoconvert|videoscale|videorate|h264parse|fdsink|vah264enc)42+ exit 043+ ;;44+ '')45+ printf 'va: vah264enc: VA-API H.264 Encoder\n'46+ ;;47+ *)48+ exit 149+ ;;50+esac51+EOF52+chmod 0755 "$temp_root/bin/gst-inspect-1.0"53+cat >"$temp_root/bin/pkg-config" <<'EOF'54+#!/usr/bin/env bash55+set -euo pipefail56+[[ "${1:-}" == --modversion && $# -eq 2 ]]57+if [[ "${SIGIL_TEST_MISSING_MODULE:-}" == "$2" ]]; then58+ exit 159+fi60+printf '1.26.11\n'61+EOF62+chmod 0755 "$temp_root/bin/pkg-config"63+64+PATH="$temp_root/bin:$PATH"65+export PATH66+67+present_output="$(SIGIL_TEST_APPSINK=present gstreamer_inventory)"68+grep -qxF 'gstreamer_element=appsink status=present' <<<"$present_output" || \69+ fail 'appsink presence was not recorded'70+grep -qxF 'gstreamer_va_h264_factory=vah264enc' <<<"$present_output" || \71+ fail 'GstVA factory was not recorded'72+73+missing_output="$(SIGIL_TEST_APPSINK=missing gstreamer_inventory)"74+grep -qxF 'gstreamer_element=appsink status=missing' <<<"$missing_output" || \75+ fail 'appsink absence was not recorded'76+77+development_output="$(SIGIL_TEST_MISSING_MODULE=gstreamer-video-1.0 \78+ gstreamer_development_inventory)"79+grep -qxF \80+ 'gstreamer_development_module=gstreamer-app-1.0 status=present version=1.26.11' \81+ <<<"$development_output" || fail 'GStreamer app development version was not recorded'82+grep -qxF \83+ 'gstreamer_development_module=gstreamer-video-1.0 status=missing' \84+ <<<"$development_output" || fail 'missing GStreamer video development module was not recorded'85+86+printf 'bazzite_inventory_gstreamer_tests=ok\n'@@ -17,6 +17,40 @@ for command_name in cargo rustc node git ffmpeg shellcheck; do
1717 fi1818 done1919 20+case "${GOQ_VERIFY_IN_PROCESS_GSTREAMER:-0}" in21+ 0|1) ;;22+ *)23+ printf 'GOQ_VERIFY_IN_PROCESS_GSTREAMER must be 0 or 1\n' >&224+ exit 125+ ;;26+esac27+28+require_rust_test() {29+ local test_name="$1"30+ shift31+ local test_list32+33+ test_list="$(cargo test --locked "$@" -- --list)"34+ if ! grep -Eq "(^|::)${test_name}: test$" <<<"$test_list"; then35+ printf 'required Rust test is not discoverable: %s\n' "$test_name" >&236+ exit 137+ fi38+}39+40+if [[ "${GOQ_VERIFY_IN_PROCESS_GSTREAMER:-0}" == 1 ]]; then41+ command -v gst-inspect-1.0 >/dev/null 2>&1 || {42+ echo 'in-process GStreamer gate requires gst-inspect-1.0' >&243+ exit 144+ }45+ for gstreamer_element in videotestsrc queue videoconvert x264enc h264parse appsink; do46+ gst-inspect-1.0 "$gstreamer_element" >/dev/null || {47+ printf 'in-process GStreamer gate requires the %s plugin\n' \48+ "$gstreamer_element" >&249+ exit 150+ }51+ done52+fi53+2054 printf 'repo=%s\n' "$repo_dir"2155 printf 'revision=%s\n' "$(git rev-parse HEAD)"2256 rustc --version@@ -25,6 +59,17 @@ cargo --version
2559 cargo fmt --all -- --check2660 cargo test --locked --workspace --all-targets2761 cargo clippy --locked --workspace --all-targets -- -D warnings62+if [[ "${GOQ_VERIFY_IN_PROCESS_GSTREAMER:-0}" == 1 ]]; then63+ control_test='encoder_control_coalesces_latest_state_and_acknowledges_only_configured_idr'64+ gstreamer_test='in_process_gstreamer_x264_smoke'65+ require_rust_test "$control_test" -p sigil-host66+ cargo test --locked -p sigil-host "$control_test"67+ cargo check --locked -p sigil-host --all-targets --features in-process-gstreamer68+ require_rust_test "$gstreamer_test" -p sigil-host --features in-process-gstreamer69+ cargo test --locked -p sigil-host --features in-process-gstreamer \70+ "$gstreamer_test" -- --ignored --nocapture71+ echo 'in_process_gstreamer_gate=ok'72+fi2873 if command -v cargo-zigbuild >/dev/null 2>&1 && command -v zig >/dev/null 2>&1; then2974 cargo zigbuild --locked -p sigil-host --target x86_64-unknown-linux-gnu.2.173075 echo 'linux_cross_build=ok'