Playa - Image Sequence Player
Small note: This is a learning project. I'm really excited to discover the Rust universe and the rise of AI agentic coding techniques to quickly learn a new stack. I perfectly know what I want to build and supposed app architecture, but implementing that alone would be probably not possible within some reasonable timeframe (not within a week, definitely). Well, also now Rust users and open source community now have a half-decent cross-platform image sequence player made of a single binary. I really wanted to express my gratitude towards creators and maintainers of exrs and openexr-rs crates and of course the rest - Rust is amazing!
Short list of things resolved while building this tool:

Image sequence player for VFX workflows. Async loading, LRU caching, OpenGL rendering.
Features
- Dual EXR backends: Choose between pure Rust (exrs) for fast builds or OpenEXR C++ for full DWAA/DWAB compression support
- Native Rust Multi-format support: EXR, PNG, JPEG, TIFF, TGA with fast parallel loading
- HDR pixel precision: Support for 8 / 16 / half-float / 32-bit float images
- Drag-and-drop: Drop any image file - automatically detects and loads the entire sequence
- Smart sequence detection: Load one frame (e.g.,
render.0001.exr) - finds all frames automatically - Persistent playlist: Load multiple sequences, auto-saves and restores between sessions
- Color-coded timeline: Visual sequence boundaries with real-time frame load indicators
- Responsive scrubbing: Instant frame navigation - always responsive even during fast scrubbing, cancels stale loads automatically
- Playback controls: Standard transport controls (play/pause, JKL shuttle, loop)
- Viewport controls: Zoom, pan, fit-to-window, 100% pixel-perfect view, cursor-centered zoom
- Custom GLSL shaders: Load display shaders from
shaders/directory - LUTs, color transforms, custom effects - Smart memory management: Automatically manages cache size - never runs out of memory
- Settings dialog: Theme switching, font size, preferences (F3)
- Cinema mode: Fullscreen playback with hidden UI
- Persistent settings: Everything saves automatically - window layout, zoom level, shader selection
Video Support
Playa now supports video playback alongside image sequences:
Supported formats: MP4, MOV, AVI, MKV
Features:
- Frame-by-frame video playback with seek support
- Automatic frame count detection
- Cached decoding with worker pool (same as image sequences)
- FFmpeg-based decoding via
playa-ffmpegcrate
Usage:
- Open video file via drag-and-drop or file browser
- Videos appear in playlist with detected frame count
- Scrub timeline to seek through video frames
- All playback controls work identically to image sequences
Technical details:
- Videos internally use
@Nsuffix notation (e.g.,video.mp4@17for frame 17) - Each frame decoded on-demand with YUV→RGB→RGBA conversion
- FFmpeg logging suppressed to avoid console spam
- Playlist serialization preserves video sequences correctly
Requirements:
- FFmpeg libraries (auto-detected via vcpkg on Windows)
playa-ffmpegcrate handles all FFmpeg bindings
Video Encoding
Playa includes built-in video encoding (F7 hotkey) for exporting image sequences and play ranges to video files.
Features:
- F7 hotkey: Opens encoding dialog with codec/quality settings
- Play range support: Encode only selected frames (B/N markers)
- Hardware acceleration: NVENC (NVIDIA), QSV (Intel), AMF (AMD)
- Software codecs: H.264, H.265, MPEG4
- Containers: MP4, MOV
- Quality modes: CRF (constant quality) or Bitrate
- Progress tracking: Real-time encoding progress with cancel support
Supported Encoders:
| Encoder | Type | Platform | Notes |
|---|---|---|---|
h264_nvenc |
Hardware | Windows/Linux | NVIDIA GPUs (GTX 600+) |
hevc_nvenc |
Hardware | Windows/Linux | NVIDIA GPUs (GTX 900+) |
h264_qsv |
Hardware | Windows/Linux | Intel Quick Sync (HD 2000+) |
hevc_qsv |
Hardware | Windows/Linux | Intel Quick Sync (Skylake+) |
h264_amf |
Hardware | Windows | AMD GPUs |
hevc_amf |
Hardware | Windows | AMD GPUs |
libx264 |
Software | All | CPU-based H.264 |
libx265 |
Software | All | CPU-based H.265 |
mpeg4 |
Software | All | Legacy MPEG-4 Part 2 |
Usage:
- Load image sequence or video
- (Optional) Set play range with B (begin) and N (end) markers
- Press B to mark the start frame
- Press N to mark the end frame
- Visual indicators appear on the timeline showing the active range
- Clear markers to encode the entire sequence
- Press F7 to open encoding dialog
- Select codec, quality settings, and output path
- Click "Encode" - progress shown in real-time with cancel option
- Output file written to selected location
Requirements & Limitations:
- Resolution consistency: All frames must have identical width and height
- Encoder will fail if frame dimensions vary within the sequence
- Ensure source material has uniform resolution before encoding
- Play range encoding: Only frames between B (begin) and N (end) markers are encoded
- If no markers are set, the entire sequence is encoded
- Markers are visually indicated on the timeline
- Frame range is inclusive (both B and N frames are included)
Technical details:
- Automatic pixel format conversion (RGB24 → YUV420P for hardware encoders)
- Uses FFmpeg swscale for color space conversion
- Multi-threaded encoding via background worker thread
- Cancellable operation with atomic flag
- Frame timestamps calculated from sequence frame rate
Installation
Recommended: Download Pre-built Installers
The easiest way to install Playa - download and run the installer for your platform:
Download the latest release from the Releases page:
macOS (recommended: DMG):
- 🎯
playa-x.x.x-exrs.dmg- Recommended - Drag to Applications (code-signed & notarized) playa-x.x.x-openexr.dmg- With DWAA/DWAB compression support (code-signed & notarized)- Portable:
playa-exrs-aarch64-apple-darwin.zip(single binary)
Linux (recommended: AppImage):
- 🎯
playa-x.x.x-exrs.AppImage- Recommended - Universal, runs everywhere playa-x.x.x-exrs.deb- Debian/Ubuntu package- Portable:
playa-exrs-x86_64-unknown-linux-gnu.zip(single binary) - OpenEXR variants:
-openexr.AppImage/-openexr.debwith DWAA/DWAB support
Windows (choose one):
- 🎯
playa-x.x.x-exrs-x64-setup.exe- Installer - System integration playa-x.x.x-exrs-x64.msi- MSI - Enterprise deploymentsplaya-exrs-x86_64-pc-windows-msvc.zip- Portable - Single .exe (no DLLs)- OpenEXR variants:
-openexr-prefix - Include DLLs for DWAA/DWAB compression
macOS Security Note: All DMG releases are code-signed with Developer ID and notarized by Apple. No Gatekeeper warnings - just drag to Applications and run.
Alternative: cargo install
Install from crates.io (requires manual FFmpeg setup):
⚠️ Requirements:
-
vcpkg must be installed and configured:
# Windows # Linux/macOS -
FFmpeg with static linking:
# Windows # Linux # macOS # or x64-osx-release for Intel
See "FFmpeg Setup" section below for complete instructions.
Build from Source (Development)
For most users: Use pre-built installers above or cargo install.
For developers: Use bootstrap scripts that automatically handle all dependencies and environment setup.
Quick Start with Bootstrap
Bootstrap scripts provide the easiest build experience with automatic dependency management:
# Clone repository
# Windows
# Linux/macOS
What bootstrap does:
- Checks Rust installation - Exits with error if missing
- Sets up vcpkg environment variables automatically:
VCPKG_ROOT- Points to vcpkg installationVCPKGRS_TRIPLET- Platform-specific triplet (e.g.,x64-windows-static-md-release)PKG_CONFIG_PATH- For FFmpeg pkg-config files (Linux/macOS)
- Installs dev tools via cargo-binstall:
cargo-release- Version bumping and changelogcargo-packager- Cross-platform installer generation
- Builds xtask - Project build automation helper
- Forwards to xtask - Handles actual compilation with correct configuration
Benefits over manual cargo build:
- ✅ Guaranteed correct FFmpeg linking configuration
- ✅ Same setup as CI/CD builds
- ✅ No manual environment variable setup
- ✅ Handles platform-specific triplets automatically
- ✅ Works identically on Windows, Linux, and macOS
After bootstrap: Continue using bootstrap.{sh|cmd} or use cargo xtask directly.
EXR Backend Options
Playa supports two EXR backends:
| Backend | Build Command | Dependencies | DWAA/DWAB Support |
|---|---|---|---|
| exrs (default) | cargo build --release |
None (pure Rust) | No |
| OpenEXR (optional) | cargo xtask build --release --openexr |
C++ compiler, CMake | Yes |
Option 1: Default Build (exrs - Pure Rust)
Fast build with no external dependencies. Suitable for most workflows:
# Build with exrs backend (pure Rust, no DLLs)
The compiled binary will be in target/release/playa (or playa.exe on Windows).
Limitations: Cannot load EXR files with DWAA/DWAB compression. Will show helpful error message with build instructions.
Option 2: Full OpenEXR Support (C++ Backend)
Supports all EXR compression formats including DWAA/DWAB:
Prerequisites:
- Rust 1.85+ (edition 2024)
- C++ compiler and CMake
# Build with OpenEXR backend (full format support)
Note: OpenEXR backend compiles C++ libraries (~5-10 minutes first build, then cached).
FFmpeg Setup (Video Playback & Encoding)
Playa requires FFmpeg libraries for video support. Install via vcpkg for best compatibility:
Windows
# Install vcpkg (if not already installed)
git clone https://github.com/microsoft/vcpkg.git C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat
# Set environment variables (required for Rust to find FFmpeg)
# Add these permanently to your system environment variables:
setx VCPKG_ROOT "C:\vcpkg"
setx VCPKGRS_TRIPLET "x64-windows-static-md-release"
# Install FFmpeg with static linking and hardware acceleration support
C:\vcpkg\vcpkg install ffmpeg[core,avcodec,avdevice,avfilter,avformat,swresample,swscale,nvcodec]:x64-windows-static-md-release
Important: The VCPKGRS_TRIPLET environment variable tells Rust's vcpkg integration which triplet to use. The x64-windows-static-md-release triplet provides static library linkage with optimized release builds, creating self-contained binaries without requiring FFmpeg DLLs at runtime.
Features explained:
core,avcodec,avformat,swscale,swresample- Core libraries (required)avdevice,avfilter- Device input and filtering supportnvcodec- NVIDIA NVENC hardware encoding (GTX 600+)
Setup Visual Studio environment (before building):
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
Linux
# Install vcpkg
# Set environment variables
# Install FFmpeg with hardware encoder support
Hardware encoders on Linux:
nvcodec- NVIDIA NVENC (requires CUDA drivers)
Alternative: System FFmpeg
# Ubuntu/Debian
# Fedora
# Arch
macOS
# Install vcpkg
# Set environment variables (automatically detected by bootstrap.sh)
# M1/M2 Macs
# Intel Macs
Alternative: Homebrew
Note: macOS hardware encoding (VideoToolbox) requires system FFmpeg or manual FFmpeg build with --enable-videotoolbox.
Verifying FFmpeg Installation
# Check FFmpeg availability
# List available encoders (after building playa)
|
# Test encoding (requires playa built)
CI/CD Runner Requirements
GitHub Actions runners need FFmpeg for video support:
Windows runners:
- Install vcpkg during workflow
- Cache:
~\vcpkgand~\AppData\Local\vcpkg - Required features:
ffmpeg[core,avcodec,avformat,avutil,swscale,nvcodec,qsv,vpl]:x64-windows-static-md
Linux runners:
- Install vcpkg during workflow OR use system FFmpeg
- Cache:
~/vcpkg - Required features:
ffmpeg[core,avcodec,avformat,avutil,swscale,nvcodec]:x64-linux
macOS runners:
- Use Homebrew FFmpeg (faster than vcpkg)
- Cache: Homebrew bottles
- Command:
brew install ffmpeg
See .github/workflows/warm-cache.yml for reference implementation.
Quick Start (New Contributors)
Start here! Bootstrap scripts handle all dependencies automatically:
Windows
bootstrap.cmd # Show xtask help
bootstrap.cmd build # Build with exrs
bootstrap.cmd build --openexr # Build with full OpenEXR support
bootstrap.cmd test # Run encoding integration test
Linux/macOS
What bootstrap does:
- Checks Rust installation (exits with error if missing)
- Auto-installs dependencies via
cargo-binstall(faster thancargo install):cargo-release- Version bumping and changelog generationcargo-packagerv0.11.7 - Cross-platform installer generation
- Builds
xtaskbinary (project build automation) - Forwards all arguments to
cargo xtask
After bootstrap: Use cargo xtask <command> directly or continue with bootstrap.{sh|cmd} <command>
Using xtask - Project Build Automation
Prerequisites: Run bootstrap.{sh|cmd} first (see Quick Start above)
xtask is an idiomatic Rust pattern for build automation - a workspace helper binary providing cross-platform task automation without external dependencies (no Makefiles, no Python, no shell scripts).
Why xtask?
- Cross-platform: Same commands work identically on Windows, Linux, and macOS
- Type-safe: Catch errors at compile time, not runtime
- Self-documenting: Built-in
--helpwith structured command definitions - Pure Rust: Uses project's existing toolchain, no external tools needed
Available Commands
🏗️ Build & Development
# Windows: %LOCALAPPDATA%\Programs\playa
# Linux/macOS: ~/.local/bin/playa
🧪 Testing
What bootstrap test does:
- Runs
cargo test --release test_encode_placeholder_frames -- --nocapture - Creates 100 placeholder frames (640x480, green color)
- Sets play range to frames 10-49 (40 frames)
- Detects available encoder (NVENC/libx264/mpeg4)
- Encodes to
test_encode_output.mp4in current directory - Verifies RGB24→YUV420P conversion for hardware encoders
- Shows encoder type, output path, file size, and frame count
Example output:
🎬 Using NVENC hardware encoder
Play range set: 10..49 (40 frames)
Encoding frames 10..49 to: C:\projects\playa\test_encode_output.mp4
✓ Encoding test passed!
Encoder: h264_nvenc
Output: C:\projects\playa\test_encode_output.mp4
Size: 2817 bytes (2.75 KB)
Frames: 40/40 (play range: 10..49)
Test file location: ./test_encode_output.mp4 (in project root)
Additional video tests:
# List all video-related tests
|
# Run specific video test
🧹 Maintenance
🚀 Release Management
🔧 Platform-Specific
What cargo xtask build Does
Without --openexr (default - exrs backend):
- Runs
cargo build [--release]with pure Rust exrs backend - Self-contained single binary (no dependencies copied)
With --openexr (OpenEXR C++ backend):
- Linux: Patches OpenEXR headers for GCC 11+ compatibility
- All platforms: Runs
cargo build [--release] --features openexr - All platforms: Copies native libraries (OpenEXR, Imath, zlib, openexr-c) to target directory
- All platforms: Copies shaders from project root
- Linux: Creates necessary symlinks for library loading
Common Workflows
Local development (fast):
Local development (full OpenEXR):
Install to system:
# Now available as: playa
Release workflow:
# 1. Create PR from dev to main
# 2. Merge PR on GitHub
# 3. Tag release on main
&&
# 4. GitHub Actions builds installers and creates Release
CI/CD Workflows
Complete Workflow
1. Development on main branch:
- Commits to
main→ push triggerswarm-cache.yml warm-cache.ymlchecks cache age (threshold: 12 hours)- If cache is stale/missing → warms cache for all platforms (Windows, Linux, macOS)
- Cache is saved under
refs/heads/main
2. Creating a release:
- Create git tag:
git tag v0.1.109→git push origin v0.1.109 - Triggers
release.yml→ verifies tag is onmainbranch - Runs builds for all platforms via
_build-platform.yml - Cache is read from main (automatic fallback via
actions/cache@v4) - For macOS: imports Developer ID certificate, signs
.app - Builds installers:
.msi(Windows),.deb/.AppImage(Linux),.dmg/.app.tar.gz(macOS) - Creates GitHub Release with artifacts
3. Manual cache warming:
- Actions → Warm Cache → Run workflow
- Choose backends:
openexr,exrs, orboth
Cache strategy:
- Cache is created only on main
- Tags read cache from main (don't create their own)
- No duplication, no isolation between tags
macOS code signing:
- Certificate: Developer ID Application (stored in GitHub Secrets)
- Workflow imports into temporary keychain
cargo-packagerusessigning-identityfromCargo.toml- Verification: logs show
✅ App is signed with Developer ID
Technical Details
Release Workflow:
- Trigger: pushing a tag matching
v*or manual run - Behavior:
- If tag points to commit on
main→ release path (publishes GitHub Release) - If tag not on
main→ dev path (builds artifacts without publishing)
- If tag points to commit on
- Manual run supports
build_type: auto | release | dev
Warm Cache Workflow:
- Trigger: push to
mainor manual dispatch - Gate: only executes automatically from
mainbranch - Cooldown: skips if successful run happened within last 12 hours
- Manual run ignores cooldown and always executes
- Backends:
openexr(default),exrs, orboth
macOS Packaging:
- Pre-packaging cleanup: detaches stale
/Volumes/Playamount, removes leftover*.dmg - Retries up to 3 times with short delay to avoid
hdiutil: create failed - Resource busy
Permissions:
- Unified workflow configured with
contents: writefor publishing releases
Static FFmpeg Linking Strategy
All CI builds use static FFmpeg linking via custom vcpkg triplets for portable, self-contained binaries:
Platform-specific triplets:
| Platform | Triplet | Configuration | Benefits |
|---|---|---|---|
| Windows | x64-windows-static-md-release |
Static libraries + dynamic CRT | No FFmpeg DLLs required, smaller installer |
| macOS | arm64-osx-release / x64-osx-release |
Static FFmpeg | Universal binary support, portable .app |
| Linux | x64-linux-release |
Static FFmpeg where possible | Reduces runtime dependencies |
Key advantages:
- Portability: Binaries work without installing FFmpeg separately
- Version consistency: Bundled FFmpeg version guaranteed to work
- Reduced installer size: No need to package separate FFmpeg DLLs
- Faster CI builds: vcpkg FFmpeg cache reduces build time from ~20 minutes to ~30 seconds
Technical implementation:
- Custom vcpkg triplets are created before cache check
- FFmpeg is installed with triplet-specific configuration
VCPKGRS_TRIPLETenvironment variable guides Rust's vcpkg integration- Cache includes FFmpeg binaries, headers, and pkg-config files
- Subsequent builds reuse cached FFmpeg (cache key includes triplet name)
Cache optimization:
- Cache paths:
vcpkg/installed,vcpkg/buildtrees,vcpkg/downloads,vcpkg/packages - Cache keys: Include OS, triplet, and FFmpeg feature set
- Hit rate: ~95% on subsequent builds (assuming no dependency updates)
- Storage: ~500MB per platform (compressed)
Cargo Features
Playa uses Cargo features to provide flexible EXR backend selection:
| Feature | Default | Description | Use Case |
|---|---|---|---|
| (none) | ✅ Yes | Pure Rust exrs backend |
Fast builds, no external dependencies |
openexr |
❌ No | C++ OpenEXR backend via openexr-rs |
Full DWAA/DWAB compression support |
Build commands:
# Default (exrs backend)
# OpenEXR backend (full compression support)
# Using xtask (handles dependencies automatically)
Backend comparison:
-
exrs (default):
- ✅ Pure Rust, fast compilation (~2-3 minutes)
- ✅ No external dependencies
- ❌ No DWAA/DWAB compression support
- Use for: Development, quick iterations
-
openexr (feature flag):
- ✅ Full OpenEXR feature support (DWAA/DWAB/etc)
- ✅ Battle-tested C++ implementation
- ❌ Requires C++ compiler, CMake
- ❌ Slower compilation (~3-4 minutes)
- Use for: Production builds, full compatibility
Development Dependencies
Auto-installed by bootstrap script:
cargo-release- Version bumping and tag creationcargo-packager- Cross-platform installer generation (v0.11.7)
Standard Rust tools (usually pre-installed):
rustup- Rust toolchain managercargo- Rust package managerclippy- Linter (rustup component add clippy)rustfmt- Code formatter (rustup component add rustfmt)
Required for PR workflow:
gh- GitHub CLI (used bycargo xtask pr) - Installation
Optional tools:
git-cliff- Changelog generation (used bycargo xtask changelog)cargo-audit- Security vulnerability scanningcargo-llvm-cov- Code coverage
Standard Rust Development
# Testing
# Documentation
# Code quality
# Build variants
Linux-Specific Build Notes
Note: These instructions apply only to the OpenEXR C++ backend (--openexr feature). The default exrs backend requires no external dependencies.
OpenEXR GCC 11+ Header Patching:
OpenEXR 3.0.5 headers are missing #include <cstdint>, causing compilation errors with GCC 11+:
error: 'uint64_t' has not been declared
cargo xtask pre automatically patches 3 header files in ~/.cargo/registry/src/:
ImfTiledMisc.hImfDeepTiledInputFile.hImfDeepTiledInputPart.h
The patching is idempotent and version-agnostic - safe to run multiple times.
See: https://github.com/AcademySoftwareFoundation/openexr/issues/1157
Native Libraries (7 Required):
| Library | Purpose |
|---|---|
| OpenEXR Core (4 libs) | EXR reading/writing, utilities, threading, exceptions |
| Imath | Math library |
| Zlib | Compression |
| OpenEXR-C | C API wrapper from openexr-sys |
Library Copy Process (cargo xtask post):
-
Locate libraries compiled by
openexr-sys:- Searches
target/release/build/openexr-sys-*/out/for versioned.sofiles - Example:
libOpenEXR-3_2.so.31.0.0,libImath-3_1.so.29.9.0
- Searches
-
Copy to target directory:
- Destination:
target/release/(next toplayabinary) - Preserves original versioned filenames
- Destination:
-
Create SONAME symlinks:
libOpenEXR-3_2.so -> libOpenEXR-3_2.so.31.0.0libOpenEXRCore-3_2.so -> libOpenEXRCore-3_2.so.31.0.0libOpenEXRUtil-3_2.so -> libOpenEXRUtil-3_2.so.31.0.0libImath-3_1.so -> libImath-3_1.so.29.9.0- Plus OpenEXR-C wrapper lib
Why this is needed:
openexr-sysbuild creates libraries with full SONAME versions- Rust linker expects generic
.sonames without version suffixes - Without symlinks:
error while loading shared libraries: libOpenEXR-3_2.so: cannot open shared object file
RPATH Configuration:
.cargo/config.toml sets RPATH to $ORIGIN, so the executable searches for .so files in its own directory:
[]
= ["-C", "link-arg=-Wl,-rpath,$ORIGIN"]
No LD_LIBRARY_PATH needed! Combined with symlinks from cargo xtask post, the binary is fully self-contained.
Troubleshooting:
Build fails with "uint64_t has not been declared":
Libraries not found when running:
After cargo clean:
Windows-Specific Build Notes
Note: These instructions apply only to the OpenEXR C++ backend (--openexr feature). The default exrs backend requires no external DLLs.
Native Libraries (DLL Management):
Windows requires .dll files alongside the executable. The same 7 OpenEXR/Imath/zlib libraries are needed, just as .dll instead of .so.
Library Copy Process (cargo xtask post):
-
Locate DLLs compiled by
openexr-sys:- Searches
target/release/build/openexr-sys-*/out/bin/for.dllfiles - Example:
OpenEXR-3_2.dll,Imath-3_1.dll,zlib.dll
- Searches
-
Copy to target directory:
- Destination:
target/release/(next toplaya.exe) - Windows DLLs don't use versioned SONAME - simpler than Linux
- Destination:
Why this is needed:
- Windows searches for DLLs in the same directory as the executable
- Without DLLs:
The code execution cannot proceed because OpenEXR-3_2.dll was not found - No PATH modification needed - self-contained binary
No RPATH equivalent:
- Windows automatically searches the executable's directory first
- No special linker flags required (unlike Linux
$ORIGIN)
macOS Code Signing & Notarization
For Users
All macOS DMG releases are code-signed with Developer ID and notarized by Apple:
- ✅ No Gatekeeper warnings
- ✅ No "unidentified developer" dialogs
- ✅ Double-click DMG → drag to Applications → works immediately
For Maintainers: CI/CD Setup
How it works in CI (_build-backend.yml):
1. Certificate Import
- Decodes
APPLE_CERTIFICATEsecret (base64 .p12 file) - Creates temporary keychain
- Imports Developer ID Application certificate
- Unlocks keychain for build process
2. Signing (automatic via cargo-packager)
- Reads
signing-identityfromCargo.toml:[] = "Developer ID Application: Name (TEAM_ID)" - Signs all executables and frameworks in
.appbundle - Verifies signature with
codesign -dv
3. Notarization (automatic via cargo-packager)
- Requires environment variables:
APPLE_ID- Apple ID emailAPPLE_PASSWORD- App-specific password (NOT iCloud password!)APPLE_TEAM_ID- Team ID from Developer Portal
- Submits signed
.appto Apple notarization service - Waits for approval (~1-5 minutes)
- Staples notarization ticket to DMG
4. Verification Logs Show:
✅ Certificate imported: Developer ID Application: Name (TEAM_ID)
✅ App signed successfully
✅ Notarization submitted (request ID: ...)
✅ Notarization approved
✅ Ticket stapled to DMG
Setting Up Secrets (One-Time):
Run helper script:
Or manually:
Certificate Details:
- Type: "Developer ID Application" (NOT "Apple Development")
- Source: Apple Developer Portal
- App-specific password: https://appleid.apple.com → Security → App-Specific Passwords
Workflow Skip Behavior:
- If
APPLE_CERTIFICATEsecret is empty → adhoc signature (for testing) - If any notarization secret missing → builds but skips notarization
Configuration
Configuration Files
Playa uses platform-specific configuration directories with flexible override options.
Priority order:
- CLI argument:
--config-dir /custom/path - Environment variable:
PLAYA_CONFIG_DIR=/custom/path - Local folder (backward compatibility): Uses current directory IF any config files already exist
- Platform defaults (new installations):
- Linux:
~/.config/playa/(config),~/.local/share/playa/(data) - macOS:
~/Library/Application Support/playa/ - Windows:
%APPDATA%\playa\
- Linux:
Files:
playa.json- Settings (FPS, theme, viewport, etc.)playa_cache.json- Cache state (sequences, current frame)playa.log- Log file (when--logflag is used)
Examples:
# Use custom directory
# Use environment variable
# Default behavior:
# - Existing users: Uses current directory (if files found)
# - New users: Uses platform-specific location
Settings auto-saved to playa.json:
- FPS
- Loop mode
- Shader selection
- Font size (global UI)
- Dark/light theme
- Viewport state (zoom/pan/mode)
- Playlist (sequence references)
- Window position/size
- Panel widths (playlist)
- Settings dialog state (selected category)
Cache state auto-saved to playa_cache.json for instant restoration on restart.
Usage
Launch
Basic Usage
# Start with empty player (drag-and-drop or file dialog)
# Load specific file or sequence
# Load multiple files (detects sequences automatically)
Command-Line Arguments
File Loading:
# Load single file (positional argument)
# Load multiple files (detects sequences for each)
# Load saved playlist
# Combine files and playlist (loaded in command-line order)
Playback Control:
# Start in fullscreen (cinema mode)
# Set starting frame (0-based)
# Auto-start playback
# Disable looping
# Set play range (work area)
Configuration:
# Use custom config directory
# Override memory budget (percentage of system RAM)
# Set worker thread count
Logging:
# Enable file logging (default: playa.log)
# Log to custom file
# Increase verbosity (default: warn)
Full Example:
# Load sequence, start at frame 50, auto-play in fullscreen with debug logging
Help:
# Show all available options
# Show version
Note: When starting without any arguments, help text is automatically printed to console before launching the GUI.
Keyboard Shortcuts
Playback Controls:
Space/K/↑- Play/Pause (unified control)J/,/←- Jog backward (starts playback, increases speed if already playing)L/./→- Jog forward (starts playback, increases speed if already playing)↓- Decrease play speed (only when playing)1/Home- Jump to start2/End- Jump to endCtrl+←- Jump to startCtrl+→- Jump to end[- Jump to previous sequence start]- Jump to next sequence start'/`- Toggle loop
FPS Control:
-- Decrease base FPS (persistent setting)=/+- Increase base FPS (persistent setting)- Base FPS steps through presets: 1, 2, 4, 8, 12, 24, 30, 60, 120, 240
- Play speed (J/L) resets to base FPS on stop
Viewport:
F- Fit to window (auto-fit mode)A/H- 100% zoomMouse Wheel- Zoom in/out (center on cursor)Middle Mouse Drag- PanLeft Click + Drag- Scrub timeline
Play Range (Work Area):
B- Set play range start (begin marker)N- Set play range end (end marker)Ctrl+B- Reset play range to full sequence- Used for:
- Loop playback within selected range
- Encoding only selected frames (F7)
- Timeline highlighting
UI:
F1- Toggle help overlayF2- Toggle playlist panelF3- Toggle settings dialogF7- Open video encoding dialogZ- Toggle fullscreen (cinema mode)ESC- Exit fullscreen / QuitQ- QuitCtrl+R- Reset settings to defaultBackspace- Toggle frame numbers on timeline (shows global range, sequence starts, play range)
Visual Sequence Navigation
The time slider provides visual feedback for multi-sequence playback:
- Color-coded zones: Each loaded sequence is displayed with a unique color on the timeline
- Sequence boundaries: White vertical dividers mark where sequences start/end
- Load indicator bar: Colored blocks below timeline show frame load status:
- Dark gray: Placeholder (not requested)
- Blue: Header only (detected but not loaded)
- Orange: Currently loading
- Green: Fully loaded
- Red: Load error
- Adaptive labels: Sequence names appear on the timeline when space permits
- Instant navigation: Click or drag anywhere on the timeline to jump to that frame
This makes it easy to identify and navigate between different sequences in your playlist at a glance.
Settings Dialog
Press F3 to open the settings dialog with TreeView categories:
UI Category:
- Font Size: Adjust global UI font size (10-18px, default 13px)
- Dark Mode: Toggle between dark and light themes
Settings are automatically persisted to playa.json.
Architecture
Core Components
┌─────────────┐
│ PlayaApp │ Main application (egui/eframe)
└──────┬──────┘
│
├──── Player ───────┐
│ │
│ ┌────▼────┐
│ │ Cache │ LRU cache + async loader + epoch counter
│ └────┬────┘
│ │
│ ┌────▼────────┐
│ │ Sequences │ Pattern-based frame lists
│ └────┬────────┘
│ │
│ ┌────▼────┐
│ │ Frames │ Individual images with status
│ └─────────┘
│
├──── Viewport ────┐
│ │
│ ┌─────▼──────────┐
│ │ ViewportState │ Zoom/pan/fit modes
│ └────────────────┘
│
├──── Scrubber ──── Timeline interaction
│
├──── TimeSlider ── Custom time slider widget + load indicator
│
├──── Shaders ───── OpenGL display shaders
│
└──── Prefs ─────── Settings dialog with TreeView
Module Breakdown
main.rs
Entry point and main application loop. Handles:
- CLI argument parsing
- Window initialization (egui/eframe)
- Event loop and UI rendering
- Keyboard/mouse input routing
- Settings persistence (JSON)
- Global font size application
player.rs
Playback state manager. Controls:
- Play/pause/stop
- Frame navigation (jog, shuttle)
- FPS control with presets
- Loop mode
- Delegates frame access to Cache
cache.rs
Intelligent caching system with multi-threaded architecture:
- LRU eviction: Manages memory budget (default 50% system RAM)
- Epoch counter: Atomic counter for cancelling stale load requests during scrubbing
- Worker pool: 75% of CPU cores for parallel loading
- Load queue: mpsc channel-based task distribution with epoch tagging
- Preload thread: Background spiral loading from current frame
- Sequence management: Multi-sequence playlist support
- Frame status tracking: Provides frame load state for visualization
Caching strategy:
- On-demand loading: Loads frame when accessed
- Spiral preload: Loads frames in order: 0, +1, -1, +2, -2, ...
- Epoch-based cancellation: Workers skip requests with old epoch on scrub/seek
- Memory-aware: Evicts least-recently-used frames when over budget
- Status sync: Updates frame status (Header → Loading → Loaded/Error)
Epoch Counter Pattern:
current_epoch: Arc<AtomicU64>increments on every scrub/seek- Workers check
req.epoch != current_epochand skip stale requests - Prevents wasted work on frames user has already moved past
sequence.rs
Pattern-based frame sequence detection:
- Auto-detects sequences from single file (e.g.,
render.0001.exr→render.*.exr) - Glob pattern matching
- Frame number extraction with padding detection
- Directory scanning for multiple sequences
- Header-only resolution reading (fast)
frame.rs
Individual frame with thread-safe async loading:
- Status states: Placeholder → Header → Loading → Loaded/Error
- Arc<Mutex>: Thread-safe shared ownership
- Format loaders: EXR (OpenEXR), PNG/JPEG/TIFF (image-rs)
- Color conversion: Linear → sRGB for EXR
- Green placeholder: Visible indicator for unloaded frames
- Status API:
frame.status()for load indicator visualization
viewport.rs
Display transformation and interaction:
- Modes: AutoFit (scales to window), Auto100 (1:1 pixels), Manual (user control)
- Zoom: Mouse wheel with cursor-centered scaling
- Pan: Middle-mouse drag
- OpenGL rendering: Custom shader pipeline
scrub.rs
Interactive timeline scrubbing:
- Left-click/drag to navigate frames
- Visual feedback (vertical line + frame number)
- Auto-pauses playback during scrub
- Maps mouse X to frame based on image bounds
- Triggers epoch counter increment for stale request cancellation
timeslider.rs
Custom time slider widget with sequence visualization:
- Color-coded zones: Each sequence rendered with unique color (hash-based)
- Visual dividers: Vertical lines marking sequence boundaries
- Adaptive labels: Sequence names/numbers displayed when space permits
- Load indicator: Colored blocks showing frame status (cached for performance)
- Cache invalidation: Uses
cached_frames_count()to detect when to rebuild - Stateless immediate mode: Fully synchronized with player state
- Interactive: Click/drag to navigate, automatic playhead tracking
- HSV color generation: Stable colors derived from sequence pattern hash
Load Indicator Implementation:
- Queries
cache.get_frame_stats()for all frame statuses - Caches result in
egui::Memorywith version key - Invalidates cache when
cached_frames_count()changes - Draws colored blocks: Dark gray (Placeholder), Blue (Header), Orange (Loading), Green (Loaded), Red (Error)
shaders.rs
OpenGL shader management:
- Built-in shaders (default, checker, etc.)
- Custom shader loading from
shaders/directory - Runtime shader switching
prefs.rs
Settings dialog with TreeView navigation:
- AppSettings struct: Centralizes all user preferences
- SettingsCategory enum: General, UI categories
- TreeView integration: Uses
egui_ltreeviewfor hierarchical navigation - Font size control: Global UI font size (10-18px with live preview)
- Theme toggle: Dark/light mode switching
- Persistence: Selected category and all settings saved to JSON
- Window layout: 700×500 default, resizable with ScrollArea
Data Flow
User Action (drag-drop / file dialog / CLI arg)
│
▼
load_sequence(PathBuf)
│
├──► cache.ingest(paths)
│ │
│ ├──► Sequence::detect() ──► Parse patterns
│ │ Extract frame numbers
│ │ Create Frame objects (status: Header)
│ │
│ └──► append_seq() ──────► Add to cache.sequences
│ Update global frame range
│ Rebuild frame_paths_cache
│
└──► signal_preload() ─────────► Preload thread wakes up
Increments epoch counter
Sends LoadRequests with current epoch
Playback Update Loop
│
▼
player.update()
│
├──► Advance frame based on FPS/direction
│
└──► cache.get_frame(idx)
│
├──► Check LRU cache ───► HIT: update access time, return frame
│
└──► MISS: Send LoadRequest with current epoch
│
▼
Worker threads (75% cores)
│
├──► Check epoch ────► Stale? Skip request
│
├──► frame.load() ─────► Detect format (EXR/PNG/etc)
│ Update status: Loading
│ Load pixels from disk
│ Convert color space
│ Update status: Loaded/Error
│
└──► Send LoadedFrame via channel
│
▼
cache.process_loaded_frames()
│
├──► Ensure space (LRU eviction)
├──► Insert into cache
├──► Update sequence frame reference
└──► Send CacheMessage for UI updates
Scrub/Seek Event
│
▼
├──► Increment epoch counter ────► Cancel all in-flight requests
│
└──► Trigger preload with new epoch
Render Loop
│
▼
UI update
│
├──► Apply global font size from settings
│
├──► Apply theme (dark/light) from settings
│
├──► Get current frame from cache
│
├──► Upload texture to GPU (if frame changed)
│
├──► TimeSlider with load indicator
│ │
│ ├──► Check cached_frames_count()
│ ├──► Rebuild indicator cache if changed
│ └──► Draw colored blocks for each frame
│
└──► ViewportRenderer.render()
│
└──► Apply viewport transform (zoom/pan)
Apply shader
Draw quad with texture
Settings Dialog (F3)
│
▼
├──► TreeView navigation (General / UI)
│
├──► Font size slider ───► Update AppSettings.font_size
│ Apply globally on next frame
│
├──► Dark mode toggle ───► Update AppSettings.dark_mode
│ Switch theme immediately
│
└──► Auto-save to playa.json
Performance Characteristics
- Startup: Instant (lazy loading)
- Sequence detection: Fast (header-only reads, ~1-5ms per file)
- Frame loading: Parallel (75% CPU cores)
- Memory: Self-limiting (50% system RAM, configurable)
- Scrubbing: Responsive (epoch-based cancellation + preloaded cache)
- Playback: Smooth (async loading stays ahead of playback)
- Load indicator: Efficient (cached, O(1) status lookups, rebuilds only on cache changes)
- LRU cache: Optimized (no stale keys in access_order, skips dead entries during eviction)
Technical Stack
- UI: egui 0.33 + eframe
- TreeView: egui_ltreeview 0.6.0 (with persistence feature)
- Graphics: OpenGL via glow + egui_glow
- Image:
- EXR (default): exrs via image 0.25 (pure Rust)
- EXR (optional): openexr 0.11 (C++ bindings,
openexrfeature) - Other formats: image 0.25 (PNG/JPEG/TIFF/TGA/HDR)
- Async: std::thread + crossbeam-channel + mpsc
- Concurrency: AtomicU64 for epoch counter, Arc for shared state
- CLI: clap 4.5
- Logging: env_logger (set
RUST_LOG=debugfor verbose output)
AI Dev experiment:
This project heavily relies on AI agents: Claude Code and Codex. Without them development time could span months instead of a single week (still, pretty intensive).
Human-designed architecture:
- System design and component boundaries
- Performance targets and trade-offs
- UX workflows and user experience
- Security model and threat boundaries
- Release strategy and versioning
AI-implemented components:
- ✅ Build automation (
xtaskworkspace - 11 commands, cross-platform) - ✅ CI/CD workflows (cache warming API, branch detection, unified release)
- ✅ Bootstrap scripts (dependency management, error handling)
- ✅ Installer packaging (NSIS, MSI, DMG, DEB, AppImage)
- ✅ Apple signing pipeline (Developer ID, notarization, keychain management)
- ✅ Documentation (architecture diagrams, data flow, comprehensive README)
Reality check: AI agents make plenty of mistakes - wrong API usage, platform-specific bugs, over-engineered solutions. Human catches these through testing and directs corrections. Iteration is fast because agents are like instant encyclopaedia.
What Works Well
Speed: Implement in minutes what would take days manually
Breadth: Cross-platform knowledge (Windows/Linux/macOS quirks) instantly available
Consistency: Code style, documentation, commit messages uniform across project
Tirelessness: Agents iterate without frustration, test edge cases without boredom
What's not
Logic: "AI" is a great trickster.
It can execute the task perfectly to your description, working completely incorrect and/or unexpected way.
Contributing
I'm not looking for contributors, but if you think you can add some useful feature - be my guest. Fork it, clone it, improve it, PR if you want. Here's the Contributing Guide for details on:
- Commit message conventions (Conventional Commits)
- Development workflow and tools
- Release process
- CI/CD architecture
Acknowledgements
Cool Halloween Cat app icon is taken from this cute Flaticon icon pack by Yasashii std
See CHANGELOG.md for project history.