Skip to main content

Stadiacast

CI Python 3.11+ Status: works end to end License: MIT

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)
  1. 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.
  2. 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.
  3. WebRTC. The controller offers a WebRTC session in its DiscoverDisplay request; 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.
  4. Game session. On StartGame the 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, tcpdump for 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

  1. 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).

  2. 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>
    
  3. Start the receiver. tools/start_receiver.sh runs it with the proven settings (see .env.example) and writes a run directory under captures/; or directly:

    stadiacast --preset live --advertise-ip 192.168.2.1 --dns-upstream 192.168.1.1 -v
    
  4. 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.

  5. Watch the input. The console shows one line per report, stadiacast-show-input captures/<stamp> summarizes a run, and GET http://<receiver>/_status shows the session state, the battery level and whether a headset is plugged in.

  6. 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
    
  7. More controllers each get their own session, c0, c1, ... in the log and in /_status; name the target of a control request with ?controller=c1 or a "controller" field in the JSON body.

  8. 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

  1. First-boot autoconnect: detect a controller in BLE setup mode and provision it onto the network automatically (today stadiacast-provision is the manual version).
  2. 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.

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)

Source distribution for stadiacast 1.0.0
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)

Table of built distributions (wheels) for stadiacast 1.0.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page