# AirPlay Casting mode

Available in **4.2.32**, classified **release** (supersedes withdrawn 4.2.31). Distribution publication
does not install or upgrade a player. Casting shares a device's screen and audio through the
player's configured local display or DeckLink SDI output. It is a live source,
separate from ingested DCPs and Show playlists.

## Configuration and operation

1. Open **Config → Casting** as an administrator or another account with
   `config.write`. Enable AirPlay Casting and enter a unique receiver name for
   this screen. Use up to 50 UTF-8 bytes, without full stops or control
   characters, so both AirPlay and RAOP discovery names fit their DNS labels.
2. Select a **network interface / IPv4 address** from the player's live inventory
   and save. Discovery and incoming AirPlay TCP/UDP listeners use that selected
   address. There is no wildcard/all-interface choice, IPv6 fallback or automatic
   move to another network. An unavailable saved address remains unavailable. Casting can still be
   disabled while retaining that saved choice, including when its NIC is absent.
   The receiver setting does not change the web interface's own binding.
3. Stop playback and turn Auto Scheduler off. Cancel any pending Delay Play.
   On the **Player** page, choose **Enter Casting**. The mode is never entered
   automatically after restart, even if the feature remains enabled in Config.
4. The local waiting display and player web panel show the receiver name and a fresh
   six-digit connection code. On an iPhone/iPad/Mac, open **Screen Mirroring** and
   select the receiver. SDI is held black while waiting; use the web panel
   to read its connection code. Windows needs an AirPlay sender such as AirMyPC.
   The sender and selected player interface need reachable networking and mDNS
   discovery; VLAN isolation and multicast filtering can prevent discovery.
5. Present one device at a time. **Leave Casting**, or ordinary **Stop**, ends
   the receiver and releases the outputs. A normal sender disconnect returns to
   the waiting display while the operator's Casting mode stays active. The previously loaded native media list is restored, stopped at its start;
   an empty previous list unloads the cast input. It does not automatically
   play a Show or re-enable Auto Scheduler.

The first implementation requests 1920×1080 at up to 30 fps and receives H.264
with stereo audio. Those are negotiation targets, not guaranteed source sizes.
The current stream descriptor has a maximum 24-hour connected session. Incoming
H.264 is forwarded without a second video encode; audio is converted from
UxPlay's 44.1 kHz stereo PCM transport to 48 kHz stereo PCM for the player input, carried in a loopback NUT stream.
A bounded RTP relay adds picture delimiters so the first frame of a static
desktop is complete without waiting for another picture. The native input
listener is confirmed ready before the bridge can emit its initial header.
Casting uses a small dedicated live buffer and recovers its clock after gaps;
normal media buffering is unchanged. SDI requires the Legacy Display profile;
precision DCP colour profiles are refused before entering the mode.
The configured player audio matrix and local/SDI output remain in charge of
presentation. Physical SDI, phone rotation, sender-version compatibility,
audio/video synchronisation and end-to-end latency require actual-device
qualification before production use.

Protected streaming applications may refuse mirroring. This does not implement
Apple FairPlay DRM, Google Cast, Miracast, AirPlay audio-only/HLS playback, DCP ingest,
recording, or playback of protected content without its normal authorisation.
Native decoder reads remain interruptible on external Stop and internal
presentation exits, including when no new network packet arrives. Live picture
timing uses presentation timestamps with bounded discontinuity handling.
Silent desktops are supplied with a bounded loopback PCM silence timeline;
later audio replaces that silence without restarting the presentation.

Upstream documents AirMyPC interoperability, but Windows/iOS/macOS senders have
not been exercised by the software-only tests in this change.

## Reservation, failures and permissions

Casting reserves the outputs even while waiting for a sender. Other playback,
selection, navigation, calibration, output changes and scheduler toggles are
refused until the mode is left. Normal library/programming reads remain usable.
Native transport starts and legacy scheduler mode changes retain the same
reservation. This does not bypass licences, native command recovery or schedule
conflict checks. The existing playback licence permission applies.

Network-address disappearance, receiver/bridge failure or loss of playback
entitlement stops the mode. If native output shutdown or loaded-list restoration cannot be confirmed,
the reservation stays held and the UI asks the operator to press Stop again. It does
not silently resume a Show or replay a cast session. Closing the application
cancels the receiver and bridge; Linux parent-death signals also terminate them
if the service is killed.

The connection code is generated per entry to Casting mode and held only in
memory/private runtime files (directory 0700, files 0600). It is not an admin
password or licence/certificate secret. Only authenticated accounts with
`playback.control` receive it in the casting status response. Waiting-screen
acknowledgements redact the code before operational/audit logging. Ordinary
UxPlay diagnostic output is not copied into the player logs. Only mode lifecycle
and actionable failures are recorded, without per-frame messages.

## Authenticated web API

All responses use JSON; status/command responses use `Cache-Control: no-store`.
These are player web-service routes, not an addition to the native TMS v1 API.

| Method and route | Permission | Behaviour |
| --- | --- | --- |
| `GET /api/casting` | `status.read` | Configuration, live interface/address choices, receiver availability and current mode status; pairing code omitted without playback-control permission. No receiver or scan is started by GET. |
| `PUT /api/casting/configuration` | `config.write` | Save `{enabled,name,interface,bind_address}`. Reject unknown fields, invalid bindings and changes during Casting. |
| `POST /api/casting/start` | `playback.control` and playback licence | Asynchronously enter the saved mode; HTTP 202. Requires idle/stopped native player, no pending native command/timeline/Delay Play, and scheduler off. Duplicate entry returns 409. |
| `POST /api/casting/stop` | `playback.control` | Asynchronously stop/reconcile the mode; HTTP 202. Poll GET for completion. Safe to request Stop again if shutdown is uncertain. |

`status.state` is `off`, `starting`, `waiting`, `connecting`, `receiving`,
`stopping`, or `error`. `active` includes the output reservation, including an
uncertain shutdown. `receiving` requires native video progress, not just an
AirPlay connection. API status also includes additive `casting_active` so native
clients can recognise a reserved but otherwise idle player. Confirmed casting
playout reports `state:live_input`, rather than claiming a Show with a known
duration is playing. The service requires the native `casting_input` capability;
a partial upgrade with an older native executable is refused.

## Receiver build, source and network binding

`./scripts/build-uxplay.sh` downloads a SHA-256-pinned upstream snapshot, applies
`packaging/uxplay/dcine.patch`, builds a separate executable and creates an
archive of its exact patched source. It requires CMake, C/C++, OpenSSL, libplist
and GStreamer development packages; `scripts/setup-ubuntu-dev.sh` includes them.
The snapshot is upstream 1.74 development revision
`534e2cc0e558d6c94bcc66964f8fc13ad73a6cdc`, not a claimed stable upstream release.

The distribution builder installs the receiver as
`/usr/libexec/dcp-cine-player/uxplay`, its complete corresponding source as
`/usr/share/doc/dcp-cine-player/uxplay-source.tar.gz`, and upstream/build notes.
GStreamer base/good/bad/libav plugins and FFmpeg are package dependencies. UxPlay
is GPL-3.0-or-later and remains a separate process; it is not linked with Kakadu,
DeckLink or the player executable. The Contributions page acknowledges it.

The service checks a fixed capability handshake and refuses a stock receiver
that cannot enforce interface binding. The patch requires explicit
`DCINE_BIND_IPV4` and `DCINE_BIND_INTERFACE`, binds AirPlay listeners to the
selected IPv4, suppresses IPv6, advertises that IPv4 via mDNS on the selected
interface, and filters incoming discovery packets by their received interface. A dedicated
bound-address mDNS target prevents the machine’s general hostname resolving
casting clients to a management interface.
AirPlay uses TCP/UDP 35000–35002 and mDNS UDP 5353 on the selected interface.
Permit those on the presentation LAN where a firewall requires it.
It never changes NetworkManager, firewall configuration or interface addresses.
The RTP bridge and native media listener bind only to loopback. No root privilege
is used to enter or leave Casting mode.

For local development only, `DCP_UXPLAY_BIN` and `DCP_CAST_FFMPEG_BIN` can select
executables in the service environment. They cannot be changed through HTTP.
Do not start a separate system-wide UxPlay daemon; the player owns its lifetime. The build forces the internal restricted mDNS
backend; the helper refuses an external DNS-SD build. Runtime GStreamer RTP
appsrc buffers receive timestamps even when local sink synchronisation is unused.

## Reproduction

```sh
scripts/build-uxplay.sh
DCP_CASTING_MEDIA_TEST=1 go -C player-service test ./cmd/dcp-player-service -run TestCastingBridgeSilenceAndLateAudioIntegration -count=1 -v
go -C player-service test ./cmd/dcp-player-service -run TestCasting -count=1
go -C player-service test -race ./cmd/dcp-player-service -run 'TestCasting|TestExtendedPlayback' -count=1
cmake --build build --target dcp-player-app -j4
python3 tests/uxplay_binding_test.py --binary build/uxplay/uxplay
python3 tests/uxplay_rtp_test.py --source build/uxplay/source
python3 tests/casting_media_test.py --binary build/dcp-player-app
```

The binding test briefly advertises a uniquely named receiver on the local
computer, checks its actual sockets and mDNS responses, and terminates it. It
changes no host network configuration. The media test uses loopback synthetic
H.264/PCM sources, the real FFmpeg bridge and the compiled engine with dummy
SDL audio/video. The renderer test compiles the exact patched UxPlay renderer
source, feeds synthetic H.264 and ALAC, and inspects loopback RTP timestamps and
PCM. It does not exercise AAC-ELD or an actual AirPlay pairing session. The Go
native integration test also covers enter/receive/disconnect/reconnect/leave,
restoration of a genuinely loaded native list, a repeated stale Stop, and native
process loss while waiting. These are not live phone or cinema hardware
acceptance tests.

The recorded local results and explicit qualification limits are in
[casting-qualification-2026-10-30.json](casting-qualification-2026-10-30.json).
The browser checks use an isolated authenticated fixture; their start/stop
responses are mocked and do not establish sender compatibility.

The follow-up review found defects beyond the original fixture coverage,
including native capability compilation, first-picture buffering, RTP timestamps,
stalled-input cancellation, stale cleanup, loaded-list restoration and legacy
control interlocks. The qualification record includes the corrected checks,
source/archive provenance and the remaining hardware coverage limits.
