brainstate

ozykhan/iina-airplay · source

Snapshot 0 · 2026-11-04T12:47:28Z · Go ·

Content fingerprinta0e8e0c57f8bf6b640cb7d5f5089147b848896140ef4282a048a054a87ebf986

GitHub description at capture

AirPlay for IINA — cast the video you're watching to an Apple TV from macOS. No screen mirroring, no re-encode. Remuxes to HLS on your LAN; IINA stays the remote.

README at capture

# iina-airplay

**Cast what IINA is playing straight to your Apple TV.** No screen mirroring, no
re-encode — the plugin hands the file to the TV, and IINA stays the remote.

<img src="https://github.com/user-attachments/assets/57781b27-13fe-4c9a-a96c-7818986025c0" width="800" height="365" alt="The iina-airplay sidebar casting to an Apple TV">

<sub>Demo footage: _Sintel_ © [Blender Foundation](https://durian.blender.org), licensed [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/).</sub>

```
IINA → Settings → Plugins → Get Plugins… → AirPlay → Install
```

In IINA's own plugin store since IINA 1.5, under
[Community Plugins](https://github.com/iina/iina#community-plugins). On IINA 1.4,
install by repository instead: `ozykhan/iina-airplay`.

macOS 12+ · any AirPlay 2 receiver · MIT · everything bundled, nothing downloads at runtime

- **The picture is the file.** Remux-first, so in the normal case the TV plays the
  original bitstream bit-for-bit. Only unusual codecs get re-encoded, through VideoToolbox.
- **IINA is the remote, both ways.** Play, pause and seek in IINA drive the Apple TV;
  the Siri Remote comes back the other way.
- **Your subtitles come along.** The text track you selected in IINA (embedded or
  external SRT/ASS) is carried as a WebVTT rendition the TV shows through its own menu.
- **Nothing to install first.** A pinned LGPL ffmpeg build and a small Go helper ship
  inside the package. No Homebrew, no runtime downloads, works offline.

### Limits, up front

|                 |                                                                                                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image subtitles | PGS/VOBSUB are dropped with a notice rather than burned in. SRT/ASS work                                                                                            |
| Start position  | The TV starts from the beginning, then jumps to where IINA was once packaging has reached that point. Best-effort: if the TV ends up elsewhere, IINA follows the TV |
| Disk space      | A cast writes a second copy of the file (the HLS remux) to IINA's temp directory for as long as it runs; it is deleted when the cast stops or IINA quits            |

<details>
<summary><b>Why this is a handoff and not a mirror</b></summary>

IINA cannot AirPlay its own video output: it renders through mpv into a
Metal/OpenGL layer, and macOS only exposes AirPlay video sending through
AVFoundation. So instead of mirroring the window, the plugin remuxes (or, when
needed, transcodes) the current file to a live HLS stream with a Go supervisor
driving `ffmpeg`, serves it on the LAN, and a hidden `<video>` element in the
sidebar hands the stream URL to the TV through WebKit's own AirPlay picker. The
player keeps running muted as a mirror, which is what makes the two-way remote
control work. `docs/feasibility.md` has the full reasoning and the format-handling
matrix.

</details>

## Install

Install **through IINA**, not by downloading the package in a browser.

**IINA 1.5 and later** list the plugin in the built-in store:

IINA → Settings → Plugins → **Get Plugins…** → **AirPlay**, under Community
Plugins → **Install**

**IINA 1.4** has no store, so give it the repository:

IINA → Settings → Plugins → Install → enter `ozykhan/iina-airplay`

The same slug also works in the field at the top of the 1.5 store.

Either way, IINA fetches the `.iinaplgz` from the latest GitHub release and
extracts it without applying `com.apple.quarantine`, so the bundled binaries
run under Gatekeeper with only their ad-hoc signatures. Downloading the package
in a browser and opening it by hand quarantines everything inside it, and the
plugin will tell you to reinstall through IINA when that happens.

## Cast

Play a file, then either:

- pick **Plugins → AirPlay → Cast to TV** from the menu bar, or
- open the sidebar's **Plugins** pane, choose the **AirPlay** tab, and press
  **Start casting**.

The sidebar packages the file, then **Send to TV** opens the AirPlay picker.
Pick the TV and IINA becomes the remote. The plugin's page under
Settings → Plugins only enables it and shows its log; the cast controls are
in the sidebar.

Everything the plugin needs ships inside the package — a pinned LGPL build of
ffmpeg and the Go helper. There are no prerequisites and nothing is downloaded
at runtime.

### Building the package yourself

```sh
make pack       # builds ffmpeg (slow, once), the helper, packs and verifies
```

The result is `build/iina-airplay.iinaplgz`. Install it from IINA → Settings →
Plugins: **Install Local Package…** on IINA 1.5, or the `+` menu → Install from
local package on 1.4.

Building ffmpeg from source needs `nasm` (see Dev quickstart below) — the
x86_64 slice assembles hand-optimized x86 assembly with it; arm64 never
needed it, which is why this only shows up once you build the universal
binary.

## Dev quickstart

```sh
brew install ffmpeg go node
make dev        # builds the helper, symlinks brew's ffmpeg into plugin/bin,
                 # links the plugin into IINA
```

`make dev` only needs the three packages above — it uses brew's `ffmpeg`, it
never builds one from source. `nasm` is a separate prerequisite, needed only
if you'll also run `make pack` / `make ffmpeg` (see above): `brew install nasm`.

Restart IINA to pick up plugin changes (`standaloneWindow`/`sidebar` are
WKWebViews and don't hot-reload JS on their own). Enable the plugin under
IINA Settings → Plugins. Plugin logs go to IINA's log, which is off by default:
turn on **Enable advanced settings** and **Enable logging to file** under
Settings → Advanced, restart IINA, then open **Window → Log Viewer** (⌃⌘L) and
pick the subsystem ending in `- AirPlay`.

If a cast says ready but the TV never plays, set the **LAN address** under
Settings → Plugins → AirPlay → Preferences to the Mac's address on the TV's
network. For VPNs, multiple network interfaces and the helper's manual `-ip`
flag, see [LAN address selection](docs/distribution.md#lan-address-selection).

If the sidebar says **This Mac can't reach its own stream**, the stream server
is running but something between IINA and it — a firewall, a VPN client, a
network filter — refuses the connection, or the advertised address is wrong.
The message carries curl's reason, and the full stream URL is in IINA's log.

## Tests

```sh
make test       # go test ./... in helper/, node --test over plugin/tests/,
                 # and the packaging shell tests
```

CI runs exactly this. The helper's real-media end-to-end tests **skip themselves**
when no `ffmpeg` is on `PATH` and `IINA_AIRPLAY_FFMPEG` is unset — so without
Homebrew's ffmpeg the suite goes green having never driven the pipeline. If you're
touching the transcode path, check those tests actually ran.

## Contributing

Contributions are welcome. [`CONTRIBUTING.md`](CONTRIBUTING.md) covers the dev
loop, the test suite, what you can verify without an Apple TV, the branch and
issue conventions, and the handful of platform constraints that bite everyone
once. New here? Look for
[`good first issue`](https://github.com/ozykhan/iina-airplay/labels/good%20first%20issue).

Trunk-based: branch off `master`, open a PR back into it, squash-merge; CI's
`test` gate must pass. The branch names are not a stylistic choice — IINA's
update check reads `Info.json` from a branch it names itself: `master` on
IINA 1.4, `main` on 1.5. So `master` is the trunk and `main` is a mirror of it
that a workflow keeps in step. Never commit to `main`, and never rename either:
that would cut one generation of IINA off from updates.

## Docs

- [ozykhan.github.io/iina-airplay](https://ozykhan.github.io/iina-airplay/) — the
  landing page, served by GitHub Pages from `docs/` on `master`
- `docs/feasibility.md` — why the direct route is closed, what's possible instead,
  and the codec/subtitle handling matrix
- `docs/prototype.md` — the three throwaway experiments that de-risked the design
  (Apple TV accepts the packaged stream, picker-in-plugin-window,
  picker-in-sidebar)
- `docs/distribution.md` — the packaging and install design for strangers'
  machines
- `docs/releasing.md` — the release runbook, and why install and update are two
  different mechanisms that must both be satisfied