Player

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.

Build and run

Requires macOS 15+, Xcode's command-line tools with Swift 6, and Homebrew FFmpeg/pkg-config:

brew install ffmpeg pkg-config
make
make run

The local app is .build/Player.app. It links the installed FFmpeg libraries; it is not a standalone distributable bundle. After upgrading FFmpeg, rebuild with make -B.

Open a local file with ⌘O, drop it onto the window, or use Finder's Open With. Files open paused. ⇧⌘W closes the file; closing the window quits and joins the worker.

Playback UI

The layout follows IINA's video-first window and the supplied screenshots: a 32-point frosted titlebar with native traffic lights and file icon, plus a dark frosted 440 × 68-point floating controller. AppKit materials blur the video beneath the chrome; muted icons, an orange volume track, a narrow seek thumb, and zero-padded time labels follow the reference. The controller starts centered with a four-percent bottom inset and keeps its relative position when the window changes size.

Opening video fits the window to its display aspect ratio, including non-square pixels, and constrains subsequent window resizing. Fullscreen retains the normal letterboxed renderer; audio-only playback and close release the aspect constraint. Extreme ratios that cannot fit both the screen and usable controls keep a freely resizable letterboxed window.

Key Action
K / Space Toggle playback
M Toggle mute, restoring the previous audible level
J / L Seek backward / forward ten seconds
, / . Step to the previous / next video frame; leave playback paused
F Toggle fullscreen
P Toggle keep-on-top (windowed mode)
O Open the file picker to switch files
Q Quit through normal application shutdown

Single-key shortcuts apply only to the playback window and do not intercept text editing, file-picker input, or Command/Control/Option combinations. Held toggle keys do not repeatedly toggle; held J/L continue seeking and held ,/. continue stepping. C captions remains unbound.

Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame.

  • Existing shortcuts remain: ⌘O opens files, ←/→ seeks five seconds, ↑/↓ adjusts volume by five percent, ⌃⌘F toggles fullscreen, and Esc leaves fullscreen. Double-clicking video also toggles fullscreen.
  • The timeline slider seeks on release and holds the requested position until queued seeking finishes. Both slider tracks span the native thumb-center travel range; the orange fill ends at the current volume thumb center. The volume thumb is opaque light gray.
  • Single-click video: hide controls. Mouse movement reveals them; they fade after 2.5 seconds, including while paused. Hovering the titlebar/controller, dragging, seeking, an inactive window, errors, or VoiceOver keep them visible.
  • Click the right-hand time label to switch between total and remaining time. The titlebar pin keeps the window on top.

Only implemented actions have controls; there are no placeholder playlist, subtitle, picture-in-picture, or settings buttons. Layout and interaction references: IINA v1.5.0's floating controller, preferences, and titlebar.

Where to change things

  • Sources/Core/player.h: the C interface and authoritative state snapshot. Playback commands enqueue work; poll state for completion. seek_pending is set synchronously when a seek or frame step is accepted and clears when all queued seeks/steps on that timeline finish. player_step_frame accepts only -1 or +1, requires ready video, and publishes the selected frame's timestamp as position. Volume set/get is synchronous and uses a lock-free target gain. Callers own the opaque handle and must stop using it before destruction.
  • Sources/Core/player.cpp: single demux/decode worker, bounded buffers, timestamps, audio-master synchronization, paused previews, exact frame stepping, decoder/resampler drain, seeking, errors, and shutdown. A seek stops output and flushes all old timeline state before resuming.
  • 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/PlayerApp.swift: MainActor state polling, file panel/Finder/drop handling, video host, AppKit glass and native slider cells, floating controls, visibility, video-fitted sizing, and native window integration.
  • Makefile: compiler/linker commands and app bundle construction. No generated Xcode project or additional build dependency.

Runnable check

make check

Generates short fixtures and exercises the real playback interface and native outputs: audio/video, video-only, audio-only, non-square-pixel display aspect, pause, paused/playing and queued seek acknowledgement, exact bidirectional VFR/B-frame stepping across keyframes, queued/mixed steps, seek-to-step transitions, first/last-frame boundaries, stepping pause/resume, completion, replay, close, HDR rejection, failed-open recovery, and destruction while playing. It initializes the actual Metal shaders and uses an offscreen native window. It requires a working audio device and graphical macOS session, and plays short sine-wave audio. It is a runtime smoke check, not a mocked clock/device test.

make check-audio runs just the audio-quality check. A check-only hook captures actual Core Audio callback PCM into preallocated storage and compares every sample against FFmpeg's reference output for PCM/AAC, 44.1/48 kHz, and coarse container timestamps. It checks fractional gain, silent muted startup, smooth live volume changes, and unchanged playback completion. A separate gapped fixture verifies that real timestamp gaps remain silent. Release builds contain no capture hook.

Continuous audio uses resampled sample counts for its output timeline; packet timestamp quantization and rounded resampler delay must not insert silence or drop samples between blocks.

Volume persists across open, seek, and close. Live gain changes ramp over five milliseconds without callback allocation or locking; a stopped output adopts the requested gain before playback starts, so muted startup cannot leak audio.

Visual correctness, perceived A/V synchronization, and control usability still need a check in the visible app.

Current limits

Local regular files, automatically selected best audio/video tracks, software decoding, and SDR output. PQ/HLG HDR is rejected rather than rendered incorrectly. No subtitles, selectable tracks, VideoToolbox decoding, rotation/deinterlacing, or network playback yet. Mid-track audio format changes report an error.

Frame stepping re-decodes from an earlier keyframe and rebuilds audio at the selected timestamp. Long-GOP or unindexed files can therefore step slowly; frames are not cached across the entire file. Stepping requires usable decoded presentation timestamps and reports an error rather than inventing frame times when they are absent.

Audio buffering is bounded to one second plus one decoded block (up to four seconds); video is bounded to 64 frames/128 MiB. Extremely interleaved files can drop far-future video frames to avoid starving audio; the ceiling and upgrade path are marked at queueVideo(). Device/stream latency is accounted for, but device changes during playback are not handled yet.

S
Description
No description provided
Readme 6.1 MiB
Languages
Swift 64.5%
C++ 16%
Objective-C++ 9.3%
Python 8%
Makefile 1.8%
Other 0.4%