Use a scoped 250 ms fade and top-anchored scale transition with animated recent-row reflow. Respect Reduce Motion, preserve the fixed window size, and exercise native focus, draft retention and rapid reversals.
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:
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.
With the macOS 27 SDK, the player uses a private ViewState alias for the SwiftUI.State<Value> property wrapper to avoid selecting the same-named macro, whose SwiftUIMacros plugin is absent from standalone Command Line Tools. Full Xcode or an older SDK is not required.
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
make install # app: ~/Applications; CLI: ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"
terebi --help
Installation builds a release, places Terebi.app in ~/Applications, registers it with Launch Services, and requests Spotlight indexing. The application display name is Terebi; the CLI remains ~/.local/bin/terebi. Any old app under ~/.local/lib/terebi or lowercase installed bundle is removed. Quit running players before updating.
Use make install APPDIR="/Applications" to change the app directory (it must be writable), PREFIX="/custom/path" to change the CLI prefix (bin/terebi), or both. Use make release for just .build/release/terebi.app.
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.
The start screen uses a fixed 640 × 400-point two-pane window: Terebi's identity and version on the left; Open…, Open URL…, and the six most recently played local files or YouTube videos on the right. Opening media restores normal resizing; closing media returns to the fixed start size. Shortcut hints follow your configured bindings. Open URL… reveals a focused inline YouTube field. When the most recent item has saved progress, it appears once as a Resume row with its saved timestamp. Recent titles and ordering survive relaunches; failed opens are not added. Resolved streams are excluded because reopening them requires a fresh external handoff. The action pane scrolls, and drag-and-drop remains available.
Start-screen actions use 14-point text and begin 64 points below the top edge. Branding uses a 16-point app name and 12-point version; the drop hint uses 13-point text. Open actions have no idle fill; their background appears on hover or press.
URL-field expansion and collapse use a 250 ms ease-in/out fade with a subtle top-anchored scale; recent rows move with the transition without resizing the window. Reduce Motion makes the change immediate. Hiding or rapidly toggling the field preserves its draft, and hiding releases keyboard focus.
| 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 any noninteractive background to move the window, including the start screen, queue, playback chrome, loading/error screens, audio-only playback, casting and compact mode. Buttons, menus, sliders, editable/selectable text and queue rows retain their own mouse input. A single video click hides the controls; double-click toggles fullscreen. Playback controls stay anchored at the bottom center of the video. Native edges remain available for resizing; macOS fullscreen keeps its fixed window bounds.
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 click a row to play it. Dragging does not change playback. 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 matches the rows' 13-point medium-weight filenames, and its icons match the rows' 12-point icons. The + is a 20-point rounded-square button with an accent-tinted background and an 8-point gap after the queue count; navigation buttons retain their 24-point hit areas.
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. The subdued remove button sits beneath the media icon in the left column, leaving the remaining width for the filename. The current item has an accent play marker and a subtle rounded highlight. Full filenames and paths remain available in row tooltips.
The queue expands and collapses with a 250 ms ease-in/out reveal. In a normal window, the sidebar and window resize together while preserving the video's size; hiding it removes that extra width. Repeated toggles reverse from the current width without jumping. The sidebar meets the video directly, without a separate divider gap, and the window shifts left when necessary near the screen's right edge. Fullscreen animates the sidebar within its fixed bounds. Reduce Motion makes the change immediate; entering compact/fullscreen mode or starting a native resize settles an in-flight reveal first.
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.
One window-level top-right overlay holds the pin, Cast and settings controls, keeping them visible at the moving window corner throughout queue expansion, collapse and reversals. Their glass container fades out as the sidebar opens and back in as it closes. With the sidebar open, the controls sit above the list with matching top and right button insets; these and the bottom queue controls do not fade with the video controls. Once the sidebar closes, the top-right controls resume auto-hiding with the video titlebar. The native layout check samples all three rendered header glyphs at the window corner during transitions as well as after opening. 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.
The sidebar, top controls and queue footer share an opaque playlist background. The top backing fades downward over 20 points. A noninteractive 52-point blank row at the start of the list clears the 32-point control row and its fade for the first entry, while the list viewport extends behind the controls so scrolling rows pass beneath the gradient rather than a hard clipping edge. The footer backing fades upward into the list by the same amount, keeping scrolled filenames from interfering with the controls without hard edges.
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:
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.
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 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:
terebi --default-config
Example partial configuration:
[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. Local seeks remain frame-accurate: disposable non-reference pictures strictly before the target are skipped, but reference pictures still require decoding. Seeking and backward frame stepping can remain 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
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. 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.
make check-local-seek measures paused/playing local seek-to-picture latency and compares the resulting timestamps and pixels against full decoding, including B-frame targets. It covers a 1080p H.264 long-GOP file with audio and the six hardware/software decoder fixtures; timing is diagnostic, not a machine-dependent pass threshold.
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 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.