176 lines
14 KiB
Markdown
176 lines
14 KiB
Markdown
# terebi
|
|
|
|
A native macOS player for local media, YouTube videos, and resolved Miruro streams, with compact mode, saved playback positions, and Chromecast support.
|
|
|
|
## Build and run
|
|
|
|
Requires macOS 15+, Xcode command-line tools with Swift 6, Python 3.9+, and these build tools:
|
|
|
|
```sh
|
|
brew install pkg-config meson ninja
|
|
# Intel Macs also need: brew install nasm
|
|
make
|
|
make run
|
|
```
|
|
|
|
The first build downloads pinned dependencies. Build natively on Apple Silicon or Intel; cross-architecture and Rosetta builds are not supported.
|
|
|
|
The resulting `.build/terebi.app` is self-contained: copy the whole app to a compatible Mac of the same architecture. No separate FFmpeg, Python, or yt-dlp installation is needed to run it.
|
|
|
|
### Install
|
|
|
|
```sh
|
|
make install # builds a release and installs under ~/.local
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
terebi --help
|
|
```
|
|
|
|
Use `make install PREFIX="/custom/path"` to change the prefix, or `make release` for just `.build/release/terebi.app`. Installation places the app in `lib/terebi/terebi.app` and its CLI symlink in `bin/terebi`. Quit running players before updating.
|
|
|
|
Builds are ad-hoc signed for personal use, not notarized. Bundled licenses and source archives are in the app's Resources; public binary redistribution requires a separate dependency/license audit, including the GPLv3+ standalone yt-dlp bundle.
|
|
|
|
## Usage
|
|
|
|
Open one or more local files with **⌘O**, drag them onto the video, or use Finder's **Open With**. A selection replaces the session's queue and plays in natural filename order (for example, episode 2 before episode 10). Open YouTube with **⇧⌘O** or drop a video link. Media starts automatically and resumes its saved position; finished videos restart from the beginning. Live and audio-only media do not save progress.
|
|
|
|
| Action | Default shortcut |
|
|
| --- | --- |
|
|
| Play / pause | Space or K |
|
|
| Seek backward / forward 5 seconds | ← / → |
|
|
| Seek backward / forward 10 seconds | J / L |
|
|
| Step one frame backward / forward (pauses playback) | , / . |
|
|
| Volume down / up | ↓ / ↑ |
|
|
| Mute / unmute | M |
|
|
| Fullscreen | F or ⌃⌘F; Esc to leave |
|
|
| Compact mode | P or ⌃⌘P |
|
|
| Always on top | ⇧P |
|
|
| Close media | ⇧⌘W |
|
|
| Quit | Q or ⌘Q |
|
|
|
|
Compact mode keeps playback in the same floating window; drag to move it and double-click to restore. Long window titles truncate at the end in both normal and compact mode; hover the title to see the full name. The titlebar pin keeps the normal window on top. macOS media keys also control playback; previous/next seek five seconds rather than changing tracks.
|
|
|
|
Compact entry and exit animate asynchronously over 0.25 seconds, preserving the original window frame and queue visibility even with rapid toggles. Control fades animate only their opacity rather than the whole window update, avoiding competing SwiftUI window-size animations. Reduce Motion disables these animations.
|
|
|
|
The normal titlebar control container uses matching 4-point top and right margins. Its horizontal padding accounts for the buttons' internal spacing to visually match the title container.
|
|
|
|
Drag the video or unused playback background to move the window, including with the queue open. A single video click hides the controls; double-click toggles fullscreen. Playback controls stay anchored at the bottom center of the video and cannot be dragged. Native window edges remain available for resizing; the open queue allows free resizing with letterboxed video.
|
|
|
|
Active playback prevents idle system sleep. Local video also keeps the display awake; audio-only playback allows display sleep. Pause, end, close, playback failure and quit release sleep prevention.
|
|
|
|
### Local-file queue
|
|
|
|
Use the **Queue** icon immediately left of the compact-mode button in the playback controls, or **View → Show Queue**, to show the editable sidebar. The **+** button beside the queue count opens **Add to Queue…**; it and dropping files onto the sidebar append without replacing the queue or interrupting the current item. Drag rows to reorder, remove them with the minus button, and double-click a row to play it. The current item is marked; context menus also offer Play, Remove and Move Up/Down.
|
|
|
|
The fixed bottom row holds the queue label/count and **+** on the left, with previous/next controls on the right. Its text and icons share a 15-point size. The **+** is a 20-point rounded-square button with an accent-tinted background and an 8-point gap after the queue count.
|
|
|
|
Queue rows show a medium-weight, two-line filename without its extension, with a smaller format label beneath it. Rows have a 56-point minimum height, 8-point vertical and 4-point horizontal internal padding, and no horizontal outer insets; leading and remove icons are smaller and subdued. The current item has an accent play marker and a subtle rounded highlight. Full filenames and paths remain available in row tooltips.
|
|
|
|
In a normal window, showing the queue expands the window outward while preserving the video's size; hiding it removes that extra width. The sidebar meets the video directly, without a separate divider gap. The window shifts left when necessary near the screen's right edge. Fullscreen keeps its fixed bounds.
|
|
|
|
Resizing with the queue open preserves the video's aspect ratio while keeping the sidebar at a fixed width; closing the queue retains ordinary aspect-locked video resizing.
|
|
|
|
While the sidebar is open, its persistent top-right overlay holds the pin, Cast and settings controls above the list without a surrounding glass container, with matching top and right button insets; these and the bottom queue controls do not fade with the video controls. The native layout check verifies that all three header glyphs actually render. Closing the sidebar returns the pin, Cast and settings controls to the auto-hiding video titlebar with its glass container. The queue toggle stays in the playback controls. Space and configured playback shortcuts continue working with queue-list focus; Tab navigation, deliberately focused buttons/sliders and text entry keep their native keyboard behavior.
|
|
|
|
Playback advances automatically and stops at the end, without wrapping. **Previous Queue Item** and **Next Queue Item** are available in the sidebar and Playback menu and are disabled at their respective ends. Reordering or removing another item does not interrupt playback; removing the current item plays its next neighbor, or stops if there is none. A playback error stays on that item rather than skipping it.
|
|
|
|
The queue contains local files only and lasts until it is replaced, closed or the app quits; it has no persistence, shuffle, repeat or playlist-file import. Opening a YouTube video or resolved stream clears it. The sidebar is hidden in compact mode; Playback-menu queue navigation remains available. Sidebar appearance, drag/drop interaction and physical-receiver queue transitions still need manual verification.
|
|
|
|
### YouTube
|
|
|
|
Playback defaults to the best available SDR resolution up to **1080p**. Use **gear → Quality** to select another available resolution, including 4K, or enable automatic quality (off by default). **Show Buffer Info** displays the retained network buffer.
|
|
|
|
Only individual videos are supported, not playlists or channels. Private, age-restricted, region-blocked, or sign-in-required videos may fail; browser cookies are not read. The extractor is bundled, so YouTube compatibility updates require rebuilding/reinstalling terebi, not updating an external yt-dlp.
|
|
|
|
### Chromecast
|
|
|
|
Open media, select the **Cast** icon or **Playback → Cast to Device → Find Cast Devices…**, and choose a receiver on the same LAN. Allow macOS Local Network access and incoming firewall connections. Terebi keeps the Mac awake during the handoff and active Cast playback while allowing the screensaver and display sleep once local playback stops. Keep the Mac connected: it relays media to the receiver. Closing the lid, explicitly sleeping the Mac or critical-battery sleep can still interrupt playback.
|
|
|
|
Existing controls operate remote playback. **Play on This Mac** returns to local playback at the receiver's position without changing the local-file queue. Queue navigation and automatic advance keep the selected receiver; opening a replacement selection or different stream ends casting. Frame stepping and YouTube quality changes are local-only; live Cast playback is not seekable. Cast output is limited to 1080p/30 fps. The screen-and-triangle icon does not provide AirPlay support.
|
|
|
|
External Cast subtitles use white sans-serif text with a black outline, transparent background and no enclosing window. Font scale currently uses the receiver default (1.0), pending physical-TV tuning. Language selection and Off remain independent of video delivery; no subtitle burn-in is used.
|
|
|
|
Cast trusts the selected LAN endpoint's self-signed certificate; use it only on a trusted network. Receiver captions and perceived A/V synchronization still need physical-device verification.
|
|
|
|
## Command line
|
|
|
|
Before installation, use `.build/terebi` instead of `terebi`:
|
|
|
|
```sh
|
|
terebi "/path/to/video.mp4"
|
|
terebi 'https://youtu.be/jNQXAC9IVRw'
|
|
terebi --new-instance --url 'https://www.youtube.com/watch?v=jNQXAC9IVRw'
|
|
terebi --new-instance --no-config "/path/to/video.mp4"
|
|
terebi -- -video.mp4
|
|
terebi --help
|
|
```
|
|
|
|
The CLI opens one file or video, reusing the running player by default. With no arguments it opens or activates the player without replacing media. Put `--new-instance` and `--no-config` before media arguments. Launch success means macOS accepted the request, not that playback succeeded; playback errors appear in the window.
|
|
|
|
### Resolved sources and progress reporting
|
|
|
|
Miruro playback requires a freshly resolved source document from an external caller such as Tubular; terebi does not discover episodes or accept Miruro page URLs directly.
|
|
|
|
```sh
|
|
terebi --new-instance --source-file '/private/session/source.json' \
|
|
--progress-file '/private/session/progress.jsonl'
|
|
```
|
|
|
|
Both options require `--new-instance`; `--progress-file` also works with YouTube. Use a private session directory and a nonexistent report destination. Keep the source file present and unchanged during playback; the caller owns cleanup. Reports contain playback facts, not watched-state decisions.
|
|
|
|
See the [integration contract](docs/miruro-playback-handoff.md) for source JSON, resource-header/origin restrictions, and progress schemas. Tubular still needs to adopt Miruro's version-2 progress identity.
|
|
|
|
## Configuration
|
|
|
|
The optional config is `$XDG_CONFIG_HOME/terebi/config.toml` when `XDG_CONFIG_HOME` is absolute, otherwise `~/.config/terebi/config.toml`. Omitted settings use defaults. To print all supported settings and key actions:
|
|
|
|
```sh
|
|
terebi --default-config
|
|
```
|
|
|
|
Example partial configuration:
|
|
|
|
```toml
|
|
[playback]
|
|
automatic_quality = true
|
|
|
|
[ui]
|
|
buffer_info = true
|
|
compact_snap_to_edges = true
|
|
compact_title_progress = "percentage" # "time", "percentage" (one decimal; 99.9 rounds up to 100.0), or "none"
|
|
|
|
[keys]
|
|
play = "space"
|
|
pause = "space"
|
|
quit = "cmd+q"
|
|
quality_2160p = "ctrl+4"
|
|
```
|
|
|
|
Shortcut strings or arrays replace an action's defaults; `[]` unbinds it. Modifiers use `cmd`, `ctrl`, `alt`/`option`, and `shift`, joined with `+`. A chord may be shared by inverse actions such as play/pause, but not unrelated actions. Invalid settings and conflicting shortcuts are errors.
|
|
|
|
Settings load once at startup. Menu changes are session-only and never write the file; start a fresh player to apply edits. `--no-config` uses built-in settings on a fresh launch, not in an already-running player, and does not clear saved progress.
|
|
|
|
## Limits
|
|
|
|
- SDR output only; PQ/HLG HDR is rejected. No stream playlists, playlist-file import, embedded/YouTube captions, manual audio-track selection, rotation, or deinterlacing. The session-only local-file queue and external resolved-source WebVTT subtitles are supported.
|
|
- H.264, HEVC, and VP9 prefer VideoToolbox decoding with software fallback; other codecs, including AV1, use software. Backward frame stepping can be slow on long-GOP or unindexed media.
|
|
- Network buffering is bounded and memory-only; live/non-seekable sources disable seeking and frame stepping. Expired stream URLs are not automatically refreshed.
|
|
- Audio-device changes during playback are not handled.
|
|
|
|
## Development checks
|
|
|
|
```sh
|
|
brew install ffmpeg # fixture generation only; not required to build or run terebi
|
|
make check
|
|
```
|
|
|
|
The full suite requires a graphical macOS session and a working audio device; it launches isolated app instances and plays short test audio. Targeted `check-*` targets are listed in the [Makefile](Makefile). Visual appearance, physical media-key routing, and receiver behavior still need manual checks.
|
|
|
|
The startup checks verify that playing video remains attached, unhidden and sized to its native drag surface, including with the queue open; playback-state checks alone do not establish renderer visibility.
|
|
|
|
Queue-open resize checks require positive native resize increments and exercise continuous horizontal/vertical resize-delegate sequences, including reversals, to preserve video aspect without snap-back. The driving axis is fixed for each live resize gesture so aspect corrections do not feed back into axis selection. Physical edge-drag resizing still needs manual verification.
|
|
|
|
`make benchmark-playback` measures cold source startup, cached/uncached seeks, and sustained native/Cast playback with repeatable loopback fixtures. It requires the same macOS session/audio device and fixture-generation FFmpeg; results exclude live extraction, WAN/TLS, and physical-receiver latency.
|
|
The [benchmark runner](Checks/playback-benchmark.py) accepts `--compare-native <baseline-binary>` for alternating native-only comparisons; add `--seek-work` to isolate cache-miss seeks when both binaries support that mode. Preserve the baseline executable before rebuilding.
|
|
|
|
Playback terminology: [CONTEXT.md](CONTEXT.md).
|