stadiacast 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. stadiacast-1.0.0/.gitignore +28 -0
  2. stadiacast-1.0.0/CHANGELOG.md +72 -0
  3. stadiacast-1.0.0/LICENSE +21 -0
  4. stadiacast-1.0.0/NOTICE +24 -0
  5. stadiacast-1.0.0/PKG-INFO +277 -0
  6. stadiacast-1.0.0/README.md +235 -0
  7. stadiacast-1.0.0/certs/README.md +11 -0
  8. stadiacast-1.0.0/docs/architecture.md +161 -0
  9. stadiacast-1.0.0/docs/protocol.md +339 -0
  10. stadiacast-1.0.0/docs/setup.md +218 -0
  11. stadiacast-1.0.0/pyproject.toml +116 -0
  12. stadiacast-1.0.0/src/stadiacast/__init__.py +11 -0
  13. stadiacast-1.0.0/src/stadiacast/__main__.py +6 -0
  14. stadiacast-1.0.0/src/stadiacast/audio.py +662 -0
  15. stadiacast-1.0.0/src/stadiacast/ble_provisioner.py +374 -0
  16. stadiacast-1.0.0/src/stadiacast/capture.py +238 -0
  17. stadiacast-1.0.0/src/stadiacast/certs.py +198 -0
  18. stadiacast-1.0.0/src/stadiacast/cli.py +474 -0
  19. stadiacast-1.0.0/src/stadiacast/config.py +133 -0
  20. stadiacast-1.0.0/src/stadiacast/controller.py +1069 -0
  21. stadiacast-1.0.0/src/stadiacast/data/identity.bin +0 -0
  22. stadiacast-1.0.0/src/stadiacast/errors.py +15 -0
  23. stadiacast-1.0.0/src/stadiacast/game_session.py +751 -0
  24. stadiacast-1.0.0/src/stadiacast/gamepad.py +172 -0
  25. stadiacast-1.0.0/src/stadiacast/helpers/__init__.py +38 -0
  26. stadiacast-1.0.0/src/stadiacast/helpers/dns_sink.py +241 -0
  27. stadiacast-1.0.0/src/stadiacast/helpers/dnswire.py +147 -0
  28. stadiacast-1.0.0/src/stadiacast/helpers/sntp_server.py +109 -0
  29. stadiacast-1.0.0/src/stadiacast/http_server.py +329 -0
  30. stadiacast-1.0.0/src/stadiacast/input_protocol.py +190 -0
  31. stadiacast-1.0.0/src/stadiacast/log.py +140 -0
  32. stadiacast-1.0.0/src/stadiacast/loopback.py +139 -0
  33. stadiacast-1.0.0/src/stadiacast/netutil.py +63 -0
  34. stadiacast-1.0.0/src/stadiacast/protos.py +349 -0
  35. stadiacast-1.0.0/src/stadiacast/py.typed +0 -0
  36. stadiacast-1.0.0/src/stadiacast/receiver.py +719 -0
  37. stadiacast-1.0.0/src/stadiacast/rtc/__init__.py +108 -0
  38. stadiacast-1.0.0/src/stadiacast/rtc/dtls.py +205 -0
  39. stadiacast-1.0.0/src/stadiacast/rtc/endpoint.py +644 -0
  40. stadiacast-1.0.0/src/stadiacast/rtc/rtp.py +134 -0
  41. stadiacast-1.0.0/src/stadiacast/rtc/srtp.py +192 -0
  42. stadiacast-1.0.0/src/stadiacast/rtc/stun.py +145 -0
  43. stadiacast-1.0.0/src/stadiacast/rtc/wire.py +59 -0
  44. stadiacast-1.0.0/src/stadiacast/schema_compiler.py +313 -0
  45. stadiacast-1.0.0/src/stadiacast/schemas/README.md +37 -0
  46. stadiacast-1.0.0/src/stadiacast/schemas/controller_message.descriptor +0 -0
  47. stadiacast-1.0.0/src/stadiacast/schemas/controller_message.proto +258 -0
  48. stadiacast-1.0.0/src/stadiacast/schemas/discovery_api.descriptor +0 -0
  49. stadiacast-1.0.0/src/stadiacast/schemas/discovery_api.proto +237 -0
  50. stadiacast-1.0.0/src/stadiacast/schemas/input.descriptor +0 -0
  51. stadiacast-1.0.0/src/stadiacast/schemas/input.proto +397 -0
  52. stadiacast-1.0.0/src/stadiacast/schemas/rpc_message.descriptor +0 -0
  53. stadiacast-1.0.0/src/stadiacast/schemas/rpc_message.proto +78 -0
  54. stadiacast-1.0.0/src/stadiacast/schemas/streamer_message.descriptor +16 -0
  55. stadiacast-1.0.0/src/stadiacast/schemas/streamer_message.proto +35 -0
  56. stadiacast-1.0.0/src/stadiacast/schemas/webrtc_sdp.descriptor +0 -0
  57. stadiacast-1.0.0/src/stadiacast/schemas/webrtc_sdp.proto +237 -0
  58. stadiacast-1.0.0/src/stadiacast/schemas/yeti_clientanalytics.descriptor +0 -0
  59. stadiacast-1.0.0/src/stadiacast/schemas/yeti_clientanalytics.proto +95 -0
  60. stadiacast-1.0.0/src/stadiacast/show_input.py +100 -0
  61. stadiacast-1.0.0/src/stadiacast/signaling.py +158 -0
  62. stadiacast-1.0.0/src/stadiacast/sysvolume.py +189 -0
  63. stadiacast-1.0.0/src/stadiacast/virtual_gamepad.py +341 -0
  64. stadiacast-1.0.0/tests/__init__.py +0 -0
  65. stadiacast-1.0.0/tests/conftest.py +35 -0
  66. stadiacast-1.0.0/tests/fake_controller.py +1075 -0
  67. stadiacast-1.0.0/tests/helpers.py +103 -0
  68. stadiacast-1.0.0/tests/test_audio.py +658 -0
  69. stadiacast-1.0.0/tests/test_audio_control.py +212 -0
  70. stadiacast-1.0.0/tests/test_ble.py +194 -0
  71. stadiacast-1.0.0/tests/test_boot_flow.py +350 -0
  72. stadiacast-1.0.0/tests/test_certs.py +139 -0
  73. stadiacast-1.0.0/tests/test_clearcut.py +127 -0
  74. stadiacast-1.0.0/tests/test_cli.py +150 -0
  75. stadiacast-1.0.0/tests/test_control_endpoints.py +110 -0
  76. stadiacast-1.0.0/tests/test_controller_state.py +69 -0
  77. stadiacast-1.0.0/tests/test_dns.py +180 -0
  78. stadiacast-1.0.0/tests/test_game_session.py +346 -0
  79. stadiacast-1.0.0/tests/test_gamepad.py +82 -0
  80. stadiacast-1.0.0/tests/test_http.py +168 -0
  81. stadiacast-1.0.0/tests/test_log.py +55 -0
  82. stadiacast-1.0.0/tests/test_multi_controller.py +139 -0
  83. stadiacast-1.0.0/tests/test_netutil.py +39 -0
  84. stadiacast-1.0.0/tests/test_protos.py +157 -0
  85. stadiacast-1.0.0/tests/test_rtc.py +198 -0
  86. stadiacast-1.0.0/tests/test_schemas.py +189 -0
  87. stadiacast-1.0.0/tests/test_show_input.py +68 -0
  88. stadiacast-1.0.0/tests/test_signaling_legacy.py +79 -0
  89. stadiacast-1.0.0/tests/test_sntp.py +34 -0
  90. stadiacast-1.0.0/tests/test_virtual_gamepad.py +314 -0
  91. stadiacast-1.0.0/tests/test_windows_lab.py +133 -0
  92. stadiacast-1.0.0/tools/pack_credential.py +45 -0
  93. stadiacast-1.0.0/tools/start_receiver.sh +74 -0
  94. stadiacast-1.0.0/tools/stop_receiver.sh +16 -0
@@ -0,0 +1,28 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ .coverage
12
+ coverage.xml
13
+ htmlcov/
14
+
15
+ # Receiver runtime data: run directories (logs, packet captures, dumped request bodies)
16
+ captures/
17
+
18
+ # Certificates and private keys: never commit them
19
+ certs/*
20
+ !certs/README.md
21
+
22
+ # Local overrides for tools/start_receiver.sh
23
+ .env
24
+
25
+ # Editors / OS
26
+ .idea/
27
+ .vscode/
28
+ .DS_Store
@@ -0,0 +1,72 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0] - 2026-11-06
10
+
11
+ Nothing to configure before the first run, sound on the headset, a virtual gamepad on Windows
12
+ and ready-made executables.
13
+
14
+ ### Added
15
+ - A built-in certificate the controller accepts, so there is nothing to obtain: the receiver
16
+ serves it when none is configured. Your own still wins with `--cert`/`--key` or a pair under
17
+ `certs/`.
18
+ - Headset audio: `--audio-source` plays a test tone, a file, what the computer plays
19
+ (`system`) or one capture device on a headset plugged into the controller.
20
+ `--audio-bitrate`, `--audio-application`, `--audio-volume` and `--audio-follow-volume` tune
21
+ it; `/_audio` starts, stops and adjusts the stream while the receiver runs, and `/_status`
22
+ reports it under `playback`.
23
+ - A virtual gamepad on Windows: `--gamepad vigem` turns each controller into an Xbox 360
24
+ controller for the games on the PC, through the ViGEmBus driver, with the game's rumble
25
+ sent back to the controller; `/_status` lists the pads under `gamepads`.
26
+ - Standalone executables for Windows, macOS (Apple silicon) and Linux attached to every
27
+ release: `stadiacast` and `stadiacast-provision`, Python included.
28
+ - The battery level, whether a headset is plugged in and the controller's hardware
29
+ description, in `/_status` and in the log.
30
+
31
+ ### Changed
32
+ - A port 80 or 8008 held by another program no longer stops the receiver; 443 is the only
33
+ port the controller needs.
34
+ - Whatever is missing on the computer (ffmpeg, an audio device, the `audio` or `gamepad`
35
+ extra, the ViGEmBus driver, a Windows that cannot host a pad) stops the receiver with one
36
+ line that says what to install, instead of a traceback.
37
+
38
+ ## [0.1.0] - 2026-10-29
39
+
40
+ First public version of Stadiacast: a LAN-side stand-in for the Stadia cloud endpoints that
41
+ keeps the Stadia Controller's Wi-Fi mode working with your own PC. Everything below was
42
+ confirmed against real controllers running the last Wi-Fi firmware, `ota.282115`.
43
+
44
+ ### Added
45
+ - The receiver (`stadiacast`): the HTTPS boot endpoints (`generate_204`, `stadiaconfig`,
46
+ Omaha, Clearcut), the cloudcast discovery RPCs, the GMsg push channel, protobuf WebRTC
47
+ signaling, STUN/ICE, DTLS-SRTP, WireHeader2 framing and the display RPC state machine up to
48
+ READY, with every button, stick and trigger decoded into a JSON Lines event log.
49
+ - The game session: `StartGame`, the proxied `InitializationRequest`, a second ICE/DTLS
50
+ endpoint, input on its `0x66` channel and rumble (`POST /_rumble`) kept alive across the
51
+ controller's haptics watchdog.
52
+ - Several controllers at once: one session per controller (own UDP ports, DTLS certificates,
53
+ GMsg queue and display link), labelled `c0`, `c1`, ... in the log and in `/_status`;
54
+ `?controller=` selects the target of `/_rumble` and `/_start_game`.
55
+ - `--preset live`: the flag set proven end to end, including the Clearcut TLS block that keeps
56
+ the controller from powering off after its first analytics upload.
57
+ - Helper processes spawned by the receiver: a redirecting DNS sink and an SNTP server.
58
+ - `stadiacast-provision`: Wi-Fi provisioning over Bluetooth LE for a controller in setup mode.
59
+ - `stadiacast-show-input`: a per-controller summary of the input recorded in a run.
60
+ - `tools/start_receiver.sh` / `tools/stop_receiver.sh`: rig settings in `.env`, a fresh run
61
+ directory per start, an optional packet capture.
62
+ - The protobuf schemas with their provenance (`src/stadiacast/schemas/`) and a small proto2
63
+ compiler that keeps the hand-written `.proto` files and their descriptors in sync.
64
+ - A fake controller (`tests/fake_controller.py`) that reproduces the firmware's network
65
+ behaviour, and a test suite that drives the whole path over loopback without hardware.
66
+ - Documentation: setup guide, protocol notes, architecture overview, contributing guide,
67
+ security policy, code of conduct.
68
+ - MIT License, with a `NOTICE` for the three protobuf schemas reproduced from the firmware.
69
+
70
+ [Unreleased]: https://github.com/jkfran/stadiacast/compare/v1.0.0...HEAD
71
+ [1.0.0]: https://github.com/jkfran/stadiacast/compare/v0.1.0...v1.0.0
72
+ [0.1.0]: https://github.com/jkfran/stadiacast/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francisco Jimenez Cabrera and the Stadiacast contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Stadiacast
2
+ Copyright (c) 2026 Francisco Jimenez Cabrera and the Stadiacast contributors
3
+
4
+ Stadiacast is licensed under the MIT License (see LICENSE), with one exception.
5
+
6
+ Three protobuf schemas under src/stadiacast/schemas/ are reproduced from the Stadia
7
+ Controller firmware (ota.282115), which embeds them as serialized descriptors:
8
+
9
+ input.descriptor and its rendering input.proto
10
+ webrtc_sdp.descriptor and its rendering webrtc_sdp.proto
11
+ yeti_clientanalytics.descriptor and its rendering yeti_clientanalytics.proto
12
+
13
+ These schemas, including the names in them, are Google's work. They are not covered by
14
+ the MIT License and the Stadiacast authors claim no rights in them. They are included
15
+ solely so that an independently created program can interoperate with the controller's
16
+ stock firmware, and they are used here for that purpose only.
17
+
18
+ Everything else in this repository is the work of the Stadiacast authors and is
19
+ licensed under the MIT License. That includes the four hand-written schemas
20
+ (discovery_api, rpc_message, controller_message, streamer_message), whose field numbers
21
+ and wire types describe the firmware's protocol but whose names and text are ours.
22
+
23
+ Stadiacast is an independent interoperability project. It is not affiliated with,
24
+ endorsed by or supported by Google. Stadia is a trademark of Google LLC.
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.5
2
+ Name: stadiacast
3
+ Version: 1.0.0
4
+ Summary: A local server for the Stadia Controller's Wi-Fi mode: keeps the controller working with your own PC after the Stadia shutdown
5
+ Project-URL: Homepage, https://github.com/jkfran/stadiacast
6
+ Project-URL: Issues, https://github.com/jkfran/stadiacast/issues
7
+ Project-URL: Changelog, https://github.com/jkfran/stadiacast/blob/main/CHANGELOG.md
8
+ Author: Francisco Jimenez Cabrera
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Keywords: controller,dtls,gamepad,interoperability,srtp,stadia,webrtc
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Games/Entertainment
25
+ Classifier: Topic :: Software Development :: Embedded Systems
26
+ Classifier: Topic :: System :: Networking
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: cryptography>=42
30
+ Requires-Dist: protobuf>=4.25
31
+ Requires-Dist: pyopenssl>=23.2
32
+ Provides-Extra: audio
33
+ Requires-Dist: pyaudiowpatch>=0.2.12; (sys_platform == 'win32') and extra == 'audio'
34
+ Requires-Dist: sounddevice>=0.4.6; (sys_platform != 'win32') and extra == 'audio'
35
+ Provides-Extra: ble
36
+ Requires-Dist: bleak>=0.21; extra == 'ble'
37
+ Provides-Extra: gamepad
38
+ Requires-Dist: vgamepad>=0.1.0; (sys_platform == 'win32') and extra == 'gamepad'
39
+ Provides-Extra: passthrough
40
+ Requires-Dist: aiohttp>=3.9; extra == 'passthrough'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Stadiacast
44
+
45
+ [![CI](https://github.com/jkfran/stadiacast/actions/workflows/ci.yml/badge.svg)](https://github.com/jkfran/stadiacast/actions/workflows/ci.yml)
46
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)
47
+ ![Status: works end to end](https://img.shields.io/badge/status-works%20end%20to%20end-brightgreen)
48
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
49
+
50
+ Google shut Stadia down in January 2023 and, with it, the cloud endpoints the Stadia
51
+ Controller's **Wi-Fi mode** depends on. Stadiacast is a small server you run on your own
52
+ network that stands in for those endpoints, so the controller you own keeps working with your
53
+ own PC: it joins your Wi-Fi, connects, stays on, streams every button, stick and trigger, and
54
+ vibrates on request.
55
+
56
+ It speaks the controller's stock protocol as is: **no firmware modification**, no Google
57
+ account, and nothing leaves your LAN.
58
+
59
+ > **Status:** works end to end (input and rumble) on the controller's last Wi-Fi firmware,
60
+ > `ota.282115`, with several controllers at once. Automatic first-boot provisioning and a
61
+ > virtual gamepad on Linux and macOS are on the [roadmap](#roadmap); Windows has one already.
62
+ > Expect rough edges; issues and pull requests are very welcome.
63
+
64
+ > [!TIP]
65
+ > Switched your controller to Bluetooth mode? It accepts older Google-signed images, so the
66
+ > community's USB flashing tools put the Wi-Fi firmware back ([Requirements](#requirements)).
67
+
68
+ ## How it works
69
+
70
+ ```
71
+ Stadia Controller (Wi-Fi) your PC / Mac running the receiver
72
+ ───────────────────────── ──────────────────────────────────
73
+ DNS *.googleapis.com ─────────────────────▶ DNS sink: answers with the receiver's IP
74
+ HTTPS boot: generate_204, stadiaconfig,
75
+ Omaha update check, Clearcut logs ─────▶ boot-time endpoints (GTS certificate)
76
+ HTTPS v1:RegisterController,
77
+ v1:DiscoverDisplay (WebRTC offer) ─────▶ discovery RPCs, answer queued on GMsg
78
+ HTTPS /gmsg long poll ◀────────────────────── GMsg push channel: the WebRTC answer
79
+ UDP STUN + DTLS (display link) ◀──────────▶ RTC endpoint: display RPC handshake → READY
80
+ DTLS WireHeader2 0x74/0x75 RPCs, input ────▶ decoded input events (JSON Lines log)
81
+ DTLS second session for game streaming ◀───▶ game session: input + rumble (haptics)
82
+ ```
83
+
84
+ 1. **DNS.** The controller must use the receiver's machine as its DNS server. The receiver
85
+ spawns a small DNS server that points the Google names it needs at itself and forwards
86
+ everything else upstream.
87
+ 2. **HTTPS.** The firmware validates the server certificate against its built-in Google Trust
88
+ Services roots and checks neither the hostname, nor the dates, nor revocation, so one GTS
89
+ chain, the one the receiver ships, lets it answer every Google name.
90
+ 3. **WebRTC.** The controller offers a WebRTC session in its `DiscoverDisplay` request; the
91
+ receiver answers over the GMsg push channel, terminates STUN and DTLS, and plays the
92
+ display side of the post-DTLS RPC handshake until the controller reaches READY and starts
93
+ reporting input.
94
+ 4. **Game session.** On `StartGame` the controller opens a second session for game streaming.
95
+ That session carries input too, and it is the only path to the controller's rumble motors.
96
+
97
+ [docs/protocol.md](docs/protocol.md) describes the protocol in detail and
98
+ [docs/architecture.md](docs/architecture.md) how the code is put together.
99
+
100
+ ## Requirements
101
+
102
+ - A Stadia Controller on its Wi-Fi firmware (tested with `ota.282115`, the last Wi-Fi build).
103
+ A controller switched to Bluetooth mode accepts older Google-signed images, so the
104
+ community's flashing tools put the Wi-Fi firmware back. Stadiacast itself never flashes
105
+ anything.
106
+ - A Windows, macOS or Linux computer on the controller's network, acting as its DNS server.
107
+ The reference rig is a macOS Internet Sharing hotspot. On Linux, binding ports 53, 80, 123
108
+ and 443 needs root or `CAP_NET_BIND_SERVICE`; on Windows the firewall may have to let the
109
+ controller in ([docs/setup.md](docs/setup.md#network)).
110
+ - Nothing else for the basics: the receiver ships a certificate the controller accepts
111
+ ([serving your own](docs/setup.md#certificate) is optional), and Python 3.11 or newer is
112
+ only needed to install from source.
113
+ - Optional: ffmpeg for [headset audio](#headset-audio), the ViGEmBus driver for the
114
+ [virtual gamepad](#virtual-gamepad-windows) on Windows, `tcpdump` for packet captures.
115
+
116
+ ## Install
117
+
118
+ **Executables.** Every [release](https://github.com/jkfran/stadiacast/releases) carries an
119
+ archive for Windows x86_64, macOS (Apple silicon) and Linux x86_64, with `stadiacast` and
120
+ `stadiacast-provision` as single files, Python included. Unpack it and run `stadiacast` from a
121
+ terminal. The builds are not signed: macOS asks you to allow the program once (right-click,
122
+ Open, or `xattr -d com.apple.quarantine stadiacast`) and Windows SmartScreen may warn.
123
+
124
+ **From source**, with [uv](https://docs.astral.sh/uv/):
125
+
126
+ ```sh
127
+ git clone https://github.com/jkfran/stadiacast.git
128
+ cd stadiacast
129
+ uv sync --all-extras
130
+ ```
131
+
132
+ or with pip, `pip install -e ".[ble,passthrough,audio,gamepad]"`. Either way you get
133
+ `stadiacast`, `stadiacast-provision` and `stadiacast-show-input`, and the scripts under
134
+ `tools/` find the virtualenv on their own.
135
+
136
+ ## Quick start
137
+
138
+ 1. **Network.** Put the controller and the receiver on one network where the receiver's
139
+ machine is the controller's DNS server; a hotspot hosted by that machine is the simplest
140
+ ([docs/setup.md](docs/setup.md#network)).
141
+ 2. **First boot only.** A controller fresh out of the box, or one that was reset, waits in
142
+ Bluetooth setup mode for its Wi-Fi credentials:
143
+
144
+ ```sh
145
+ stadiacast-provision scan
146
+ stadiacast-provision provision --address <ADDR> --ssid <your SSID>
147
+ ```
148
+
149
+ 3. **Start the receiver.** `tools/start_receiver.sh` runs it with the proven settings (see
150
+ `.env.example`) and writes a run directory under `captures/`; or directly:
151
+
152
+ ```sh
153
+ stadiacast --preset live --advertise-ip 192.168.2.1 --dns-upstream 192.168.1.1 -v
154
+ ```
155
+
156
+ 4. **Wake the controller** with a ~1 s press of the Stadia button, released at the first
157
+ vibration. Two short rumbles and a steady light mean it found the receiver; a blinking
158
+ light with no rumble is a short "report wake", so press again.
159
+ 5. **Watch the input.** The console shows one line per report, `stadiacast-show-input
160
+ captures/<stamp>` summarizes a run, and `GET http://<receiver>/_status` shows the session
161
+ state, the battery level and whether a headset is plugged in.
162
+ 6. **Rumble.** Start with `--start-game`, since the game session is the only path to the
163
+ motors, then post levels from 0 to 65535:
164
+
165
+ ```sh
166
+ curl -d '{"left":40000,"right":40000,"ms":1500}' http://192.168.2.1/_rumble # pulse
167
+ curl -d '{"left":65535,"right":20000}' http://192.168.2.1/_rumble # hold
168
+ curl -d '{"left":0,"right":0}' http://192.168.2.1/_rumble # stop
169
+ ```
170
+
171
+ 7. **More controllers** each get their own session, `c0`, `c1`, ... in the log and in
172
+ `/_status`; name the target of a control request with `?controller=c1` or a `"controller"`
173
+ field in the JSON body.
174
+ 8. **Stop** with `tools/stop_receiver.sh`.
175
+
176
+ `--preset live` is the flag set proven end to end; explicit flags override it, and
177
+ `stadiacast --help` lists every knob.
178
+
179
+ ## Input
180
+
181
+ In READY the controller reports every change on the display link. The receiver decodes it
182
+ into events with named buttons, stick axes (-32768 to 32767, Y up) and triggers (0 to 255),
183
+ with the button bits in the XInput layout. The full report is in
184
+ [docs/protocol.md](docs/protocol.md#input-report-controllertodisplay-3).
185
+
186
+ ## Headset audio
187
+
188
+ The receiver can play sound on a headset plugged into the controller. ffmpeg with libopus
189
+ encodes every source, so it has to be on `PATH`:
190
+
191
+ ```sh
192
+ stadiacast --audio-source tone # 440 Hz, proves the path without any audio setup
193
+ stadiacast --audio-source file:x.ogg # anything ffmpeg can decode
194
+ stadiacast --audio-source system # what this computer plays
195
+ stadiacast --audio-source device:NAME # one capture device; --audio-devices lists them
196
+ ```
197
+
198
+ The stream starts once the display link reaches READY. `--audio-bitrate` sets the Opus bitrate
199
+ (64 kbit/s by default), `--audio-application` favours delay (`lowdelay`, the default) or
200
+ music (`audio`), and `--audio-volume` the gain. `/_status` reports the stream under `playback`
201
+ and `/_audio` starts, stops and adjusts it while the receiver runs
202
+ ([docs/setup.md](docs/setup.md#running)).
203
+
204
+ `system` and `device:NAME` capture sound and need the `audio` extra, which the executables and
205
+ `uv sync --all-extras` include (Linux also needs the PortAudio library, `apt install
206
+ libportaudio2`). On macOS, send the sound to a virtual output such as BlackHole
207
+ (`brew install blackhole-2ch`), alone or in a Multi-Output Device next to the speakers; the
208
+ volume keys do not act on it, so set the loudness in the app or with `--audio-volume`. On
209
+ Windows the default output's loopback is captured and the volume slider and mute reach the
210
+ headset (`--audio-follow-volume off` sends the raw mix). On Linux the default output's
211
+ PulseAudio monitor is read, untested on a real machine. How the stream rides the display link
212
+ is in [docs/protocol.md](docs/protocol.md#audio-to-the-headset-jack).
213
+
214
+ ## Virtual gamepad (Windows)
215
+
216
+ `--gamepad vigem` turns each controller into an ordinary Xbox 360 controller for the games on
217
+ the receiver's PC:
218
+
219
+ ```sh
220
+ stadiacast --gamepad vigem # one virtual pad per controller
221
+ ```
222
+
223
+ Only a driver can create a device that Windows and its games see as a controller, so this
224
+ needs one extra install, once and with an admin prompt: the
225
+ [ViGEmBus](https://github.com/nefarius/ViGEmBus/releases) driver, the same one behind
226
+ DS4Windows, Sunshine and the other tools of this kind. Its author archived the project in 2023
227
+ and 1.22.0 is the final version; it still installs and works on current Windows 10 and 11. It
228
+ has to be desktop Windows, since Windows Server lacks the Xbox 360 controller driver the pad
229
+ binds to. Without the driver, the receiver stops at start-up with one line that says so; from
230
+ source, installing the `gamepad` extra offers to run the driver's installer.
231
+
232
+ Every report reaches the pad as XInput fields with the standard stick deadzones
233
+ (`--gamepad-deadzone off` for raw sticks); the Stadia button is the guide button, Assistant
234
+ and Capture have no XInput equivalent. The game's rumble comes back to the controller through
235
+ the game session, which `--gamepad` starts for that reason. Two controllers are players 1 and
236
+ 2, and `/_status` lists the pads under `gamepads`. The pad has not been run on a real Windows
237
+ PC yet.
238
+
239
+ ## Development
240
+
241
+ ```sh
242
+ uv sync --all-extras # the receiver, its extras and the dev tools
243
+ make check # ruff, shellcheck, mypy, the schema check and the tests
244
+ ```
245
+
246
+ The tests need no hardware: `tests/fake_controller.py` reproduces the firmware's network
247
+ behaviour. [CONTRIBUTING.md](CONTRIBUTING.md) has the workflow, how to run the fake by hand
248
+ against a receiver in lab mode, and how to test against a real controller safely.
249
+
250
+ ## Roadmap
251
+
252
+ 1. **First-boot autoconnect:** detect a controller in BLE setup mode and provision it onto the
253
+ network automatically (today `stadiacast-provision` is the manual version).
254
+ 2. **Virtual gamepad on Linux and macOS:** Windows has one (`--gamepad vigem`); Linux (uinput)
255
+ and macOS are open.
256
+
257
+ ## Security and privacy
258
+
259
+ The receiver listens on your LAN with no authentication and is meant for a trusted network
260
+ such as a hotspot you host. It never contacts Google except for the optional `stadiaconfig`
261
+ passthrough. See [SECURITY.md](SECURITY.md).
262
+
263
+ ## Legal
264
+
265
+ This is an independent interoperability project for owners of the Stadia Controller. It is
266
+ not affiliated with, endorsed by or supported by Google. Stadia is a trademark of Google LLC.
267
+
268
+ Stadiacast contains no Google source code and neither includes nor distributes the
269
+ controller's firmware: the firmware stays on your controller and the receiver speaks its
270
+ stock network protocol as-is. The protocol was recovered by observing the controller and by
271
+ analysing the firmware it runs, for the sole purpose of interoperability. Three protobuf
272
+ schemas are reproduced from descriptors embedded in that firmware; they are Google's, and
273
+ [NOTICE](NOTICE) lists them. Please do not post firmware images, decompiled code or whole
274
+ packet captures in issues or pull requests.
275
+
276
+ Stadiacast is released under the [MIT License](LICENSE), with the exception stated in
277
+ [NOTICE](NOTICE).
@@ -0,0 +1,235 @@
1
+ # Stadiacast
2
+
3
+ [![CI](https://github.com/jkfran/stadiacast/actions/workflows/ci.yml/badge.svg)](https://github.com/jkfran/stadiacast/actions/workflows/ci.yml)
4
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)
5
+ ![Status: works end to end](https://img.shields.io/badge/status-works%20end%20to%20end-brightgreen)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+
8
+ Google shut Stadia down in January 2023 and, with it, the cloud endpoints the Stadia
9
+ Controller's **Wi-Fi mode** depends on. Stadiacast is a small server you run on your own
10
+ network that stands in for those endpoints, so the controller you own keeps working with your
11
+ own PC: it joins your Wi-Fi, connects, stays on, streams every button, stick and trigger, and
12
+ vibrates on request.
13
+
14
+ It speaks the controller's stock protocol as is: **no firmware modification**, no Google
15
+ account, and nothing leaves your LAN.
16
+
17
+ > **Status:** works end to end (input and rumble) on the controller's last Wi-Fi firmware,
18
+ > `ota.282115`, with several controllers at once. Automatic first-boot provisioning and a
19
+ > virtual gamepad on Linux and macOS are on the [roadmap](#roadmap); Windows has one already.
20
+ > Expect rough edges; issues and pull requests are very welcome.
21
+
22
+ > [!TIP]
23
+ > Switched your controller to Bluetooth mode? It accepts older Google-signed images, so the
24
+ > community's USB flashing tools put the Wi-Fi firmware back ([Requirements](#requirements)).
25
+
26
+ ## How it works
27
+
28
+ ```
29
+ Stadia Controller (Wi-Fi) your PC / Mac running the receiver
30
+ ───────────────────────── ──────────────────────────────────
31
+ DNS *.googleapis.com ─────────────────────▶ DNS sink: answers with the receiver's IP
32
+ HTTPS boot: generate_204, stadiaconfig,
33
+ Omaha update check, Clearcut logs ─────▶ boot-time endpoints (GTS certificate)
34
+ HTTPS v1:RegisterController,
35
+ v1:DiscoverDisplay (WebRTC offer) ─────▶ discovery RPCs, answer queued on GMsg
36
+ HTTPS /gmsg long poll ◀────────────────────── GMsg push channel: the WebRTC answer
37
+ UDP STUN + DTLS (display link) ◀──────────▶ RTC endpoint: display RPC handshake → READY
38
+ DTLS WireHeader2 0x74/0x75 RPCs, input ────▶ decoded input events (JSON Lines log)
39
+ DTLS second session for game streaming ◀───▶ game session: input + rumble (haptics)
40
+ ```
41
+
42
+ 1. **DNS.** The controller must use the receiver's machine as its DNS server. The receiver
43
+ spawns a small DNS server that points the Google names it needs at itself and forwards
44
+ everything else upstream.
45
+ 2. **HTTPS.** The firmware validates the server certificate against its built-in Google Trust
46
+ Services roots and checks neither the hostname, nor the dates, nor revocation, so one GTS
47
+ chain, the one the receiver ships, lets it answer every Google name.
48
+ 3. **WebRTC.** The controller offers a WebRTC session in its `DiscoverDisplay` request; the
49
+ receiver answers over the GMsg push channel, terminates STUN and DTLS, and plays the
50
+ display side of the post-DTLS RPC handshake until the controller reaches READY and starts
51
+ reporting input.
52
+ 4. **Game session.** On `StartGame` the controller opens a second session for game streaming.
53
+ That session carries input too, and it is the only path to the controller's rumble motors.
54
+
55
+ [docs/protocol.md](docs/protocol.md) describes the protocol in detail and
56
+ [docs/architecture.md](docs/architecture.md) how the code is put together.
57
+
58
+ ## Requirements
59
+
60
+ - A Stadia Controller on its Wi-Fi firmware (tested with `ota.282115`, the last Wi-Fi build).
61
+ A controller switched to Bluetooth mode accepts older Google-signed images, so the
62
+ community's flashing tools put the Wi-Fi firmware back. Stadiacast itself never flashes
63
+ anything.
64
+ - A Windows, macOS or Linux computer on the controller's network, acting as its DNS server.
65
+ The reference rig is a macOS Internet Sharing hotspot. On Linux, binding ports 53, 80, 123
66
+ and 443 needs root or `CAP_NET_BIND_SERVICE`; on Windows the firewall may have to let the
67
+ controller in ([docs/setup.md](docs/setup.md#network)).
68
+ - Nothing else for the basics: the receiver ships a certificate the controller accepts
69
+ ([serving your own](docs/setup.md#certificate) is optional), and Python 3.11 or newer is
70
+ only needed to install from source.
71
+ - Optional: ffmpeg for [headset audio](#headset-audio), the ViGEmBus driver for the
72
+ [virtual gamepad](#virtual-gamepad-windows) on Windows, `tcpdump` for packet captures.
73
+
74
+ ## Install
75
+
76
+ **Executables.** Every [release](https://github.com/jkfran/stadiacast/releases) carries an
77
+ archive for Windows x86_64, macOS (Apple silicon) and Linux x86_64, with `stadiacast` and
78
+ `stadiacast-provision` as single files, Python included. Unpack it and run `stadiacast` from a
79
+ terminal. The builds are not signed: macOS asks you to allow the program once (right-click,
80
+ Open, or `xattr -d com.apple.quarantine stadiacast`) and Windows SmartScreen may warn.
81
+
82
+ **From source**, with [uv](https://docs.astral.sh/uv/):
83
+
84
+ ```sh
85
+ git clone https://github.com/jkfran/stadiacast.git
86
+ cd stadiacast
87
+ uv sync --all-extras
88
+ ```
89
+
90
+ or with pip, `pip install -e ".[ble,passthrough,audio,gamepad]"`. Either way you get
91
+ `stadiacast`, `stadiacast-provision` and `stadiacast-show-input`, and the scripts under
92
+ `tools/` find the virtualenv on their own.
93
+
94
+ ## Quick start
95
+
96
+ 1. **Network.** Put the controller and the receiver on one network where the receiver's
97
+ machine is the controller's DNS server; a hotspot hosted by that machine is the simplest
98
+ ([docs/setup.md](docs/setup.md#network)).
99
+ 2. **First boot only.** A controller fresh out of the box, or one that was reset, waits in
100
+ Bluetooth setup mode for its Wi-Fi credentials:
101
+
102
+ ```sh
103
+ stadiacast-provision scan
104
+ stadiacast-provision provision --address <ADDR> --ssid <your SSID>
105
+ ```
106
+
107
+ 3. **Start the receiver.** `tools/start_receiver.sh` runs it with the proven settings (see
108
+ `.env.example`) and writes a run directory under `captures/`; or directly:
109
+
110
+ ```sh
111
+ stadiacast --preset live --advertise-ip 192.168.2.1 --dns-upstream 192.168.1.1 -v
112
+ ```
113
+
114
+ 4. **Wake the controller** with a ~1 s press of the Stadia button, released at the first
115
+ vibration. Two short rumbles and a steady light mean it found the receiver; a blinking
116
+ light with no rumble is a short "report wake", so press again.
117
+ 5. **Watch the input.** The console shows one line per report, `stadiacast-show-input
118
+ captures/<stamp>` summarizes a run, and `GET http://<receiver>/_status` shows the session
119
+ state, the battery level and whether a headset is plugged in.
120
+ 6. **Rumble.** Start with `--start-game`, since the game session is the only path to the
121
+ motors, then post levels from 0 to 65535:
122
+
123
+ ```sh
124
+ curl -d '{"left":40000,"right":40000,"ms":1500}' http://192.168.2.1/_rumble # pulse
125
+ curl -d '{"left":65535,"right":20000}' http://192.168.2.1/_rumble # hold
126
+ curl -d '{"left":0,"right":0}' http://192.168.2.1/_rumble # stop
127
+ ```
128
+
129
+ 7. **More controllers** each get their own session, `c0`, `c1`, ... in the log and in
130
+ `/_status`; name the target of a control request with `?controller=c1` or a `"controller"`
131
+ field in the JSON body.
132
+ 8. **Stop** with `tools/stop_receiver.sh`.
133
+
134
+ `--preset live` is the flag set proven end to end; explicit flags override it, and
135
+ `stadiacast --help` lists every knob.
136
+
137
+ ## Input
138
+
139
+ In READY the controller reports every change on the display link. The receiver decodes it
140
+ into events with named buttons, stick axes (-32768 to 32767, Y up) and triggers (0 to 255),
141
+ with the button bits in the XInput layout. The full report is in
142
+ [docs/protocol.md](docs/protocol.md#input-report-controllertodisplay-3).
143
+
144
+ ## Headset audio
145
+
146
+ The receiver can play sound on a headset plugged into the controller. ffmpeg with libopus
147
+ encodes every source, so it has to be on `PATH`:
148
+
149
+ ```sh
150
+ stadiacast --audio-source tone # 440 Hz, proves the path without any audio setup
151
+ stadiacast --audio-source file:x.ogg # anything ffmpeg can decode
152
+ stadiacast --audio-source system # what this computer plays
153
+ stadiacast --audio-source device:NAME # one capture device; --audio-devices lists them
154
+ ```
155
+
156
+ The stream starts once the display link reaches READY. `--audio-bitrate` sets the Opus bitrate
157
+ (64 kbit/s by default), `--audio-application` favours delay (`lowdelay`, the default) or
158
+ music (`audio`), and `--audio-volume` the gain. `/_status` reports the stream under `playback`
159
+ and `/_audio` starts, stops and adjusts it while the receiver runs
160
+ ([docs/setup.md](docs/setup.md#running)).
161
+
162
+ `system` and `device:NAME` capture sound and need the `audio` extra, which the executables and
163
+ `uv sync --all-extras` include (Linux also needs the PortAudio library, `apt install
164
+ libportaudio2`). On macOS, send the sound to a virtual output such as BlackHole
165
+ (`brew install blackhole-2ch`), alone or in a Multi-Output Device next to the speakers; the
166
+ volume keys do not act on it, so set the loudness in the app or with `--audio-volume`. On
167
+ Windows the default output's loopback is captured and the volume slider and mute reach the
168
+ headset (`--audio-follow-volume off` sends the raw mix). On Linux the default output's
169
+ PulseAudio monitor is read, untested on a real machine. How the stream rides the display link
170
+ is in [docs/protocol.md](docs/protocol.md#audio-to-the-headset-jack).
171
+
172
+ ## Virtual gamepad (Windows)
173
+
174
+ `--gamepad vigem` turns each controller into an ordinary Xbox 360 controller for the games on
175
+ the receiver's PC:
176
+
177
+ ```sh
178
+ stadiacast --gamepad vigem # one virtual pad per controller
179
+ ```
180
+
181
+ Only a driver can create a device that Windows and its games see as a controller, so this
182
+ needs one extra install, once and with an admin prompt: the
183
+ [ViGEmBus](https://github.com/nefarius/ViGEmBus/releases) driver, the same one behind
184
+ DS4Windows, Sunshine and the other tools of this kind. Its author archived the project in 2023
185
+ and 1.22.0 is the final version; it still installs and works on current Windows 10 and 11. It
186
+ has to be desktop Windows, since Windows Server lacks the Xbox 360 controller driver the pad
187
+ binds to. Without the driver, the receiver stops at start-up with one line that says so; from
188
+ source, installing the `gamepad` extra offers to run the driver's installer.
189
+
190
+ Every report reaches the pad as XInput fields with the standard stick deadzones
191
+ (`--gamepad-deadzone off` for raw sticks); the Stadia button is the guide button, Assistant
192
+ and Capture have no XInput equivalent. The game's rumble comes back to the controller through
193
+ the game session, which `--gamepad` starts for that reason. Two controllers are players 1 and
194
+ 2, and `/_status` lists the pads under `gamepads`. The pad has not been run on a real Windows
195
+ PC yet.
196
+
197
+ ## Development
198
+
199
+ ```sh
200
+ uv sync --all-extras # the receiver, its extras and the dev tools
201
+ make check # ruff, shellcheck, mypy, the schema check and the tests
202
+ ```
203
+
204
+ The tests need no hardware: `tests/fake_controller.py` reproduces the firmware's network
205
+ behaviour. [CONTRIBUTING.md](CONTRIBUTING.md) has the workflow, how to run the fake by hand
206
+ against a receiver in lab mode, and how to test against a real controller safely.
207
+
208
+ ## Roadmap
209
+
210
+ 1. **First-boot autoconnect:** detect a controller in BLE setup mode and provision it onto the
211
+ network automatically (today `stadiacast-provision` is the manual version).
212
+ 2. **Virtual gamepad on Linux and macOS:** Windows has one (`--gamepad vigem`); Linux (uinput)
213
+ and macOS are open.
214
+
215
+ ## Security and privacy
216
+
217
+ The receiver listens on your LAN with no authentication and is meant for a trusted network
218
+ such as a hotspot you host. It never contacts Google except for the optional `stadiaconfig`
219
+ passthrough. See [SECURITY.md](SECURITY.md).
220
+
221
+ ## Legal
222
+
223
+ This is an independent interoperability project for owners of the Stadia Controller. It is
224
+ not affiliated with, endorsed by or supported by Google. Stadia is a trademark of Google LLC.
225
+
226
+ Stadiacast contains no Google source code and neither includes nor distributes the
227
+ controller's firmware: the firmware stays on your controller and the receiver speaks its
228
+ stock network protocol as-is. The protocol was recovered by observing the controller and by
229
+ analysing the firmware it runs, for the sole purpose of interoperability. Three protobuf
230
+ schemas are reproduced from descriptors embedded in that firmware; they are Google's, and
231
+ [NOTICE](NOTICE) lists them. Please do not post firmware images, decompiled code or whole
232
+ packet captures in issues or pull requests.
233
+
234
+ Stadiacast is released under the [MIT License](LICENSE), with the exception stated in
235
+ [NOTICE](NOTICE).