feat: add Chromecast playback support
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# terebi
|
||||
|
||||
A personal native macOS player whose playback code is small enough to own and modify. FFmpeg reads and decodes media; our C++ worker owns playback. SwiftUI owns the controls, Core Audio owns audio output, and Metal presents video.
|
||||
A personal native macOS player whose playback code is small enough to own and modify. FFmpeg reads and decodes media; our C++ worker owns local playback. SwiftUI owns the controls, Core Audio owns local audio output, and Metal presents local video. Chromecast owns remote playback.
|
||||
|
||||
## Build and run
|
||||
|
||||
@@ -17,7 +17,7 @@ make run
|
||||
|
||||
The developer app is `.build/terebi.app`; release builds produce `.build/release/terebi.app`. Both are self-contained for macOS 15+ on the build machine's CPU architecture. Copy the whole app to another compatible Mac; no Homebrew, FFmpeg installation, Python installation, yt-dlp installation, or shell PATH setup is needed to run it. Build natively on Apple Silicon or Intel; cross-architecture and Rosetta builds are rejected, and these are not universal or cross-platform binaries.
|
||||
|
||||
`Dependencies.lock.json` pins SHA256-verified FFmpeg 9.0.1, dav1d 1.5.4, liblzma 5.8.4, standalone yt-dlp 2026.08.19 and Deno 2.9.6 inputs. The first build downloads them into ignored `.build/dependencies/<architecture>`; later builds reuse completed preparations and verified downloads. FFmpeg is built as shared LGPL playback libraries, without programs, encoders, muxers, filters or unrelated autodetected packages; dav1d supplies software AV1 and liblzma retains compressed-media support. Native HTTPS uses macOS SecureTransport, not an external OpenSSL installation. The repo carries recipes and checksums, not generated runtime binaries.
|
||||
`Dependencies.lock.json` pins SHA256-verified FFmpeg 9.0.1, dav1d 1.5.4, liblzma 5.8.4, standalone yt-dlp 2026.08.19 and Deno 2.9.6 inputs. The first build downloads them into ignored `.build/dependencies/<architecture>`; later builds reuse completed preparations and verified downloads. FFmpeg is built as shared LGPL playback libraries, without programs, filters or unrelated autodetected packages. Cast delivery enables only the built-in AAC and VideoToolbox H.264 encoders and MPEG-TS muxer; dav1d supplies software AV1 and liblzma retains compressed-media support. Native HTTPS uses macOS SecureTransport, not an external OpenSSL installation. The repo carries recipes and checksums, not generated runtime binaries.
|
||||
|
||||
Open a local file with **⌘O**, drop it onto the window, or use Finder's **Open With**. Successfully opened files start playback automatically, including when replacing the current file. **⇧⌘W** closes the file; closing the window quits and joins the worker.
|
||||
|
||||
@@ -198,13 +198,29 @@ Miruro's progress identity is independent of its source address. YouTube records
|
||||
|
||||
Events, acknowledged-position reporting, nullable duration and completion eligibility retain the semantics below. Miruro bookmarks use the same stable episode identity. **Tubular coordination required:** its current `src/video_state.rs` accepts only canonical YouTube/version-1 records; it must adopt this version-2 identity before Miruro watched tracking works. This is the implemented Terebi contract, not a claim that Tubular has already adopted it.
|
||||
|
||||
`make check-subtitles`, `make check-resolved` and `make check-network` use deterministic local VTT/HLS fixtures; `make check-startup` exercises the actual CLI-to-GUI handoff. Live playback is never part of the default suite. With a private, freshly resolved source document, explicitly run `.build/resolved-playback-check /absolute/source.json` for native video/audio and subtitle/seek smoke. Visual rendering and perceived synchronization still require a visible-app check.
|
||||
`make check-subtitles`, `make check-resolved` and `make check-network` use deterministic local VTT/HLS fixtures; `make check-startup` exercises the actual CLI-to-GUI handoff. Public-provider live playback is never part of the default suite; Cast's rolling-window check uses generated media and an injected pacing clock. With a private, freshly resolved source document, explicitly run `.build/resolved-playback-check /absolute/source.json` for native video/audio and subtitle/seek smoke. Visual rendering and perceived synchronization still require a visible-app check.
|
||||
|
||||
Verification recorded for the direct `ssub`/`sun` source: a freshly resolved native stream decoded video/audio and advanced, selected English VTT, and matched the model's subtitle text to the expected cues after forward/backward acknowledged seeks. Local encrypted MPEG-TS and fMP4 fixtures also verified disguised image suffixes, headers on master/variant/segment/key/init requests, query preservation, foreign-origin/redirect isolation and redacted errors. `TEREBI_CHECK_RESOLVED_ONLY=1 make check-startup` passed actual CLI launch into two independent GUI episodes, resource-specific subtitle headers and version-2 reports. CLI, subtitle, progress, reporting, YouTube resolver and system-playback checks passed.
|
||||
|
||||
The full `make check-startup` rerun passed, including held-key suppression, YouTube progress/end transitions and independent Miruro launches. Earlier runs failed held-Space and YouTube end-transition assertions; possible manual interaction was reported, but causality is not established. The rerun required no playback-code changes. Visual subtitle appearance and perceived A/V synchronization remain human verification, and Tubular's version-2 consumer change remains external coordination.
|
||||
|
||||
|
||||
## Chromecast
|
||||
|
||||
Open media, then choose the **Cast** icon in the titlebar (or compact controls), or **Playback → Cast to Device → Find Cast Devices…**. Allow macOS Local Network access, then select a discovered Chromecast/Cast-enabled TV on the same LAN. No Chrome, hosted custom receiver, Google developer registration, or external casting runtime is required.
|
||||
|
||||
Terebi pauses local output before transferring the acknowledged position and play/pause intent to Google's Default Media Receiver. The existing controls, keyboard shortcuts and macOS media commands control remote playback, seeking, volume/mute and resolved-source subtitles. **Play on This Mac** restores receiver-confirmed position and intent; local volume is preserved independently. Connection/load failures return to local playback with an error. Opening another source ends casting and opens it locally. Frame stepping and YouTube quality changes are local-only; automatic quality is suspended while casting.
|
||||
|
||||
The Mac serves session-only capability URLs over HTTP. Keep it awake and connected, and allow incoming connections through its firewall. The relay exposes only the chosen media and subtitle resources, never arbitrary filesystem paths or a general URL proxy. Disconnect, replacement and quit cancel delivery and invalidate the listening endpoint.
|
||||
|
||||
Local files, resolved Miruro HLS and YouTube's combined/separate video/audio inputs are delivered as six-second HLS segments. Compatible, independently decodable H.264/AAC is remuxed; other supported inputs use VideoToolbox H.264 (including its platform software fallback) and stereo AAC. Output targets at most **1920×1080 at 30 fps**, H.264 Main/High level 4.1 or lower, with preserved display aspect ratio. Recorded segments are generated on demand, including distant seeks; there is no full-movie download or preprocessing step. Live output keeps eight segments (about 48 seconds) and is not seekable. Audio-only sources are supported.
|
||||
|
||||
Miruro retains its HTTP(S)-only, same-origin media and no-redirect policy. Public YouTube HLS can cross CDN origins, but supplied headers never follow it to another origin. All Cast media redirects are rejected. WebVTT is fetched with each track's own headers and the existing bounded, same-origin subtitle redirect policy, then served with CORS. Private upstream addresses and headers are not sent to the receiver or included in errors.
|
||||
|
||||
The Cast channel accepts the selected receiver's self-signed TLS certificate: encryption on a trusted LAN, **not** Google device authentication or public-CA receiver identity verification. Discovery and receiver control trust the selected LAN endpoint. Native media decoding has a 4096×2160-pixel budget, individual FFmpeg allocations are capped at 128 MiB after Cast initialization, and each outgoing segment is capped at 32 MiB. Recorded inputs longer than fourteen days are rejected.
|
||||
|
||||
`make check-cast` exercises a real self-signed TLS loopback receiver, decoded relay output for all three source paths, header/origin isolation, recorded seeking, VOD/live audio continuity, anamorphic video, capability expiry, controlled protocol deadlines/live pacing, and complete player handoff/back-to-Mac recovery. A production-paced live HLS smoke also decoded A/V across a segment boundary and shut down. **Physical Cast playback, receiver captions and perceived A/V synchronization remain human verification; the TODO stays open until that check.**
|
||||
|
||||
## Resume playback
|
||||
|
||||
Reopening a local video, YouTube video or resolved Miruro episode automatically resumes its saved position, including after quitting and relaunching the app. Local files use their resolved file paths; YouTube uses the canonical video webpage URL; Miruro uses its opaque media ID and episode number. Newly resolved addresses therefore share progress without saving CDN tokens as identity. The app does not automatically reopen the last video on launch.
|
||||
@@ -292,6 +308,10 @@ Playback terms are defined in [CONTEXT.md](CONTEXT.md).
|
||||
- `Sources/Core/player.cpp`: demux/decode worker for local files and decode/timeline worker for network sources, bounded decoded buffers, authored timestamps, audio-master synchronization, paused previews, exact frame stepping, independent decoder/resampler drain, seeking, errors, and shutdown. Seeks stop output and flush decoder/output state; network inputs replay retained packets or seek/refill on cache misses.
|
||||
- `Sources/Core/progress_reporter.hpp` and `.cpp`: shared JSONL schema, canonical identity, acknowledged snapshots, eligibility, clock-injected cadence/threshold detection, lifecycle records, and asynchronous file writer. File creation/write uses POSIX facilities; reporting policy is independent of AppKit/Swift.
|
||||
- `Sources/Core/network_input.hpp`: network-only demux readers with a shared compressed-byte budget, playback-anchored read-ahead/history, keyframe/audio-preroll seek indices, conservative reordered-video timestamp frontiers, cached replay, asynchronous miss seeks, fifteen-second I/O deadlines, and interruption on miss/replacement/close/destruction. Cache hits leave outstanding fetch-frontier I/O intact. Remote input protocols exclude local-file access through manifests.
|
||||
- `Sources/Core/network_policy.hpp`: shared URL, header and origin validation used by local network playback and Cast delivery.
|
||||
- `Sources/Core/cast_media.h` and `.cpp`: bounded native LAN HLS serving, on-demand recorded segments, rolling live delivery, remux/transcode, subtitle capabilities and cancellation.
|
||||
- `Sources/App/CastSession.swift`: Bonjour discovery, bounded Cast v2 TLS/protobuf framing, Default Media Receiver ownership, acknowledged controls, heartbeat and cancellation.
|
||||
- `Sources/App/CastMediaServer.swift` and `CastControls.swift`: off-main native delivery lifetime, shared authenticated WebVTT transport, and device menus.
|
||||
- `Sources/Core/output.hpp` and `Sources/macOS/Output.mm`: native output contract, allocation-free/nonblocking Core Audio callback and bounded audio ring, latency-aware presentation clock, retained frame mailbox, and Metal YUV conversion. The view can outlive the player safely.
|
||||
- `Sources/App/TerebiApp.swift`: MainActor `PlayerModel` retains the control interface. Private `PlaybackTransition` owns extraction lifetime, open/seek acknowledgement, playback intent, local restoration, and checkpoint eligibility. `MediaSource`/`MediaChange` feed one opening path for files, YouTube, and quality changes. Native observations checkpoint before consuming local restoration; network restoration and JSONL reporting remain in C++. Panels, controls, visibility, video sizing, and window integration remain presentation.
|
||||
- `Sources/App/PlayerActions.swift`: native event-to-chord adaptation, editing/accessibility eligibility, explicit context-menu dispatch, and scene-focused SwiftUI commands sharing the model's action bindings.
|
||||
@@ -339,6 +359,8 @@ The gated YouTube check also exercises compact shortcut/control entry and restor
|
||||
|
||||
`make check-reporting` decodes real JSONL files with an injected clock: initial/period boundaries, pending versus acknowledged seeks, threshold/pause/end/close facts, quality/replacement identity, nullable duration and eligibility, exclusive private creation, existing/symlink/concurrent rejection, and isolated OS write failure. `make check-network` also exercises the real backend's acknowledged initial restoration, audio-only/live eligibility, failed/cancelled opens, final drain, and continued playback/seek/replacement after a failed writer. Both are included in `make check`.
|
||||
|
||||
`make check-cast` is included in `make check`; it needs no TV or live provider. `make check-network` also verifies that immediate close drains receiver-confirmed progress rather than the abandoned decoder position.
|
||||
|
||||
`make check-cli` checks relative/escaped paths, option-like filenames, YouTube identity, default reuse versus explicit `--new-instance`, `--no-config` alone and combined in either order, independent empty/local/YouTube launches, help without launch, argument cardinality, unknown/duplicate options, missing files/directories, non-local file URLs, and unsupported stream URLs. It is included in `make check`.
|
||||
|
||||
`make check-runtime` exercises missing/invalid dependency metadata, unsafe paths/versions, unavailable and unloadable dylibs, omitted library families, and successful native loading from an isolated relocated copy of the bundled Frameworks. It is included in `make check`; no installed Homebrew files are modified.
|
||||
|
||||
Reference in New Issue
Block a user