Metadata-Version: 2.4
Name: cidermill-airplay
Version: 0.1.0
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: System :: Networking
License-File: LICENSE
Summary: Sans-io AirPlay 2 building blocks: RTSP codec, HAP pairing (both sides), cipher framing, /info parsing.
Keywords: airplay,hap,homekit,rtsp,protocol
License-Expression: GPL-3.0-or-later
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# cidermill-airplay (Python)

Python bindings for [cidermill-airplay](https://gitlab.com/timrs2998/cidermill-airplay), a sans-io
AirPlay 2 implementation in Rust: the RTSP/HTTP codec, HAP pairing on both sides, the
ChaCha20-Poly1305 framing and `/info` parsing. Nothing here opens a socket; you own the I/O.

```python
import socket
import cidermill_airplay as ap

def post(sock, path, body):
    request = ap.Request("POST", path, ap.Protocol.RTSP, body)
    request.set_header("X-Apple-HKP", "3")
    sock.sendall(request.encode())
    decoder = ap.Decoder()
    while (response := decoder.next_response()) is None:
        decoder.push(sock.recv(65536))
    return response.body()

credentials = ap.HapCredentials.parse(open("credentials.txt").read().strip())
with socket.create_connection(("192.0.2.10", 7000)) as sock:
    verify = ap.PairVerify(credentials)
    step = verify.handle(post(sock, "/pair-verify", verify.start()))
    while step.send is not None:
        step = verify.handle(post(sock, "/pair-verify", step.send))
    channel = step.keys.transport()  # seal requests, push and open replies from here on
```

| | |
|---|---|
| Codec | `Request`, `Response`, `Decoder`, `Protocol`, `Tlv8`, `DeviceInfo`, `PlaybackInfo` |
| Pairing, controller | `PairSetup` (PIN, transient), `PairVerify`, `PairingStep`, `TransientStyle` |
| Pairing, accessory | `PairSetupServer`, `PairVerifyServer`, `PairingOutcome`, `PinGate` |
| Keys and framing | `SessionKeys`, `EncryptedTransport` |
| Credentials | `HapCredentials`, `LegacyCredentials` (pyatv's storage strings) |
| Errors | `AirPlayException` and subclasses; `PairingError.code`, `StatusError.code/.path/.reason`, `PairVerifyRefusedError`, `DecryptError` |

A receiver shares one `PinGate` across every connection's `PairSetupServer`, so PIN guesses
are limited per source address (pass `source=`) with a receiver-wide net behind it. Not wrapped: the blocking control channel, audio and mirroring.

## Building and testing

```sh
maturin develop --release                    # into the active virtualenv
python -m unittest discover -s tests -v
maturin build --release                      # a wheel
```

License: GPL-3.0-or-later, the same as the core (see LICENSE).

