Stadiacast
Google shut Stadia down in January 2023 and, with it, the cloud endpoints the Stadia Controller's Wi-Fi mode depends on. Stadiacast is a small server you run on your own network that stands in for those endpoints, so the controller you own keeps working with your own PC: it joins your Wi-Fi, connects, stays on, streams every button, stick and trigger, and vibrates on request.
It speaks the controller's stock protocol as is: no firmware modification, no Google account, and nothing leaves your LAN.
Status: works end to end (input and rumble) on the controller's last Wi-Fi firmware,
ota.282115, with several controllers at once. Automatic first-boot provisioning and a virtual gamepad on Linux and macOS are on the roadmap; Windows has one already. Expect rough edges; issues and pull requests are very welcome.
How it works
Stadia Controller (Wi-Fi) your PC / Mac running the receiver
───────────────────────── ──────────────────────────────────
DNS *.googleapis.com ─────────────────────▶ DNS sink: answers with the receiver's IP
HTTPS boot: generate_204, stadiaconfig,
Omaha update check, Clearcut logs ─────▶ boot-time endpoints (GTS certificate)
HTTPS v1:RegisterController,
v1:DiscoverDisplay (WebRTC offer) ─────▶ discovery RPCs, answer queued on GMsg
HTTPS /gmsg long poll ◀────────────────────── GMsg push channel: the WebRTC answer
UDP STUN + DTLS (display link) ◀──────────▶ RTC endpoint: display RPC handshake → READY
DTLS WireHeader2 0x74/0x75 RPCs, input ────▶ decoded input events (JSON Lines log)
DTLS second session for game streaming ◀───▶ game session: input + rumble (haptics)
- DNS. The controller must use the receiver's machine as its DNS server. The receiver spawns a small DNS server that points the Google names it needs at itself and forwards everything else upstream.
- HTTPS. The firmware validates the server certificate against its built-in Google Trust Services roots and checks neither the hostname, nor the dates, nor revocation, so one GTS chain, the one the receiver ships, lets it answer every Google name.
- WebRTC. The controller offers a WebRTC session in its
DiscoverDisplayrequest; the receiver answers over the GMsg push channel, terminates STUN and DTLS, and plays the display side of the post-DTLS RPC handshake until the controller reaches READY and starts reporting input. - Game session. On
StartGamethe controller opens a second session for game streaming. That session carries input too, and it is the only path to the controller's rumble motors.
docs/protocol.md describes the protocol in detail and docs/architecture.md how the code is put together.
Requirements
- A Stadia Controller on its Wi-Fi firmware (tested with
ota.282115, the last Wi-Fi build). A controller switched to Bluetooth mode accepts older Google-signed images, so the community's flashing tools put the Wi-Fi firmware back. Stadiacast itself never flashes anything. - A Windows, macOS or Linux computer on the controller's network, acting as its DNS server.
The reference rig is a macOS Internet Sharing hotspot. On Linux, binding ports 53, 80, 123
and 443 needs root or
CAP_NET_BIND_SERVICE; on Windows the firewall may have to let the controller in (docs/setup.md). - Nothing else for the basics: the receiver ships a certificate the controller accepts (serving your own is optional), and Python 3.11 or newer is only needed to install from source.
- Optional: ffmpeg for headset audio, the ViGEmBus driver for the
virtual gamepad on Windows,
tcpdumpfor packet captures.
Install
Executables. Every release carries an
archive for Windows x86_64, macOS (Apple silicon) and Linux x86_64, with stadiacast and
stadiacast-provision as single files, Python included. Unpack it and run stadiacast from a
terminal. The builds are not signed: macOS asks you to allow the program once (right-click,
Open, or xattr -d com.apple.quarantine stadiacast) and Windows SmartScreen may warn.
From source, with uv:
git clone https://github.com/jkfran/stadiacast.git
cd stadiacast
uv sync --all-extras
or with pip, pip install -e ".[ble,passthrough,audio,gamepad]". Either way you get
stadiacast, stadiacast-provision and stadiacast-show-input, and the scripts under
tools/ find the virtualenv on their own.
Quick start
-
Network. Put the controller and the receiver on one network where the receiver's machine is the controller's DNS server; a hotspot hosted by that machine is the simplest (docs/setup.md).
-
First boot only. A controller fresh out of the box, or one that was reset, waits in Bluetooth setup mode for its Wi-Fi credentials:
stadiacast-provision scan stadiacast-provision provision --address <ADDR> --ssid <your SSID>
-
Start the receiver.
tools/start_receiver.shruns it with the proven settings (see.env.example) and writes a run directory undercaptures/; or directly:stadiacast --preset live --advertise-ip 192.168.2.1 --dns-upstream 192.168.1.1 -v
-
Wake the controller with a ~1 s press of the Stadia button, released at the first vibration. Two short rumbles and a steady light mean it found the receiver; a blinking light with no rumble is a short "report wake", so press again.
-
Watch the input. The console shows one line per report,
stadiacast-show-input captures/<stamp>summarizes a run, andGET http://<receiver>/_statusshows the session state, the battery level and whether a headset is plugged in. -
Rumble. Start with
--start-game, since the game session is the only path to the motors, then post levels from 0 to 65535:curl -d '{"left":40000,"right":40000,"ms":1500}' http://192.168.2.1/_rumble # pulse curl -d '{"left":65535,"right":20000}' http://192.168.2.1/_rumble # hold curl -d '{"left":0,"right":0}' http://192.168.2.1/_rumble # stop
-
More controllers each get their own session,
c0,c1, ... in the log and in/_status; name the target of a control request with?controller=c1or a"controller"field in the JSON body. -
Stop with
tools/stop_receiver.sh.
--preset live is the flag set proven end to end; explicit flags override it, and
stadiacast --help lists every knob.
Input
In READY the controller reports every change on the display link. The receiver decodes it into events with named buttons, stick axes (-32768 to 32767, Y up) and triggers (0 to 255), with the button bits in the XInput layout. The full report is in docs/protocol.md.
Headset audio
The receiver can play sound on a headset plugged into the controller. ffmpeg with libopus
encodes every source, so it has to be on PATH:
stadiacast --audio-source tone # 440 Hz, proves the path without any audio setup
stadiacast --audio-source file:x.ogg # anything ffmpeg can decode
stadiacast --audio-source system # what this computer plays
stadiacast --audio-source device:NAME # one capture device; --audio-devices lists them
The stream starts once the display link reaches READY. --audio-bitrate sets the Opus bitrate
(64 kbit/s by default), --audio-application favours delay (lowdelay, the default) or
music (audio), and --audio-volume the gain. /_status reports the stream under playback
and /_audio starts, stops and adjusts it while the receiver runs
(docs/setup.md).
system and device:NAME capture sound and need the audio extra, which the executables and
uv sync --all-extras include (Linux also needs the PortAudio library, apt install libportaudio2). On macOS, send the sound to a virtual output such as BlackHole
(brew install blackhole-2ch), alone or in a Multi-Output Device next to the speakers; the
volume keys do not act on it, so set the loudness in the app or with --audio-volume. On
Windows the default output's loopback is captured and the volume slider and mute reach the
headset (--audio-follow-volume off sends the raw mix). On Linux the default output's
PulseAudio monitor is read, untested on a real machine. How the stream rides the display link
is in docs/protocol.md.
Virtual gamepad (Windows)
--gamepad vigem turns each controller into an ordinary Xbox 360 controller for the games on
the receiver's PC:
stadiacast --gamepad vigem # one virtual pad per controller
Only a driver can create a device that Windows and its games see as a controller, so this
needs one extra install, once and with an admin prompt: the
ViGEmBus driver, the same one behind
DS4Windows, Sunshine and the other tools of this kind. Its author archived the project in 2023
and 1.22.0 is the final version; it still installs and works on current Windows 10 and 11. It
has to be desktop Windows, since Windows Server lacks the Xbox 360 controller driver the pad
binds to. Without the driver, the receiver stops at start-up with one line that says so; from
source, installing the gamepad extra offers to run the driver's installer.
Every report reaches the pad as XInput fields with the standard stick deadzones
(--gamepad-deadzone off for raw sticks); the Stadia button is the guide button, Assistant
and Capture have no XInput equivalent. The game's rumble comes back to the controller through
the game session, which --gamepad starts for that reason. Two controllers are players 1 and
2, and /_status lists the pads under gamepads. The pad has not been run on a real Windows
PC yet.
Development
uv sync --all-extras # the receiver, its extras and the dev tools
make check # ruff, shellcheck, mypy, the schema check and the tests
The tests need no hardware: tests/fake_controller.py reproduces the firmware's network
behaviour. CONTRIBUTING.md has the workflow, how to run the fake by hand
against a receiver in lab mode, and how to test against a real controller safely.
Roadmap
- First-boot autoconnect: detect a controller in BLE setup mode and provision it onto the
network automatically (today
stadiacast-provisionis the manual version). - Virtual gamepad on Linux and macOS: Windows has one (
--gamepad vigem); Linux (uinput) and macOS are open.
Security and privacy
The receiver listens on your LAN with no authentication and is meant for a trusted network
such as a hotspot you host. It never contacts Google except for the optional stadiaconfig
passthrough. See SECURITY.md.
Legal
This is an independent interoperability project for owners of the Stadia Controller. It is not affiliated with, endorsed by or supported by Google. Stadia is a trademark of Google LLC.
Stadiacast contains no Google source code and neither includes nor distributes the controller's firmware: the firmware stays on your controller and the receiver speaks its stock network protocol as-is. The protocol was recovered by observing the controller and by analysing the firmware it runs, for the sole purpose of interoperability. Three protobuf schemas are reproduced from descriptors embedded in that firmware; they are Google's, and NOTICE lists them. Please do not post firmware images, decompiled code or whole packet captures in issues or pull requests.
Stadiacast is released under the MIT License, with the exception stated in NOTICE.
Metadata
Release files for stadiacast 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| stadiacast-1.0.0.tar.gz | 225.8 kB | With provenance | Details for stadiacast-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| stadiacast-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for stadiacast-1.0.0-py3-none-any.whl |
Total release size: 393.3 kB
Release files / stadiacast-1.0.0.tar.gz
| Download URL | stadiacast-1.0.0.tar.gz |
|---|---|
| Size | 225.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8ff0242e896aec26e5af68c8ae8861ac3fb2c94047367f0ca0c6ddf42e7bfee6
|
|
BLAKE2b-256 checksum How to use checksums |
73f3fbc0526fd99b0c16dbf8f5681a860bad9d06217da5b782d127c3367b4c57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logRelease files / stadiacast-1.0.0-py3-none-any.whl
| Download URL | stadiacast-1.0.0-py3-none-any.whl |
|---|---|
| Size | 167.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2023a9f9b11027d330a69ef414eda5030a09c64402b97c48a4f61b9e5c204e88
|
|
BLAKE2b-256 checksum How to use checksums |
74cde0aa2f441bed2bb01f4a8f5d982deefa4901abbc7e6c507d09a914e4952b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logSecurity for stadiacast 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for stadiacast-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.