# MXL Player MXL Player is a desktop and command-line controlled player for local [Media eXchange Layer](https://github.com/dmf-mxl/mxl) audio and video flows. It can play either feed independently, play both simultaneously, or switch a configured pair into and out of native MXL synchronization while running. The application uses SDL3 for its window, input, and audio output; Vulkan for video and ImGui rendering; and [`go-mxl`](https://github.com/qvest-digital/go-mxl) for MXL access. > [!IMPORTANT] > This project is under active development. Builds and feed playback have been > verified in current Ubuntu and Fedora environments and on a separate Ubuntu > machine. macOS builds are in progress; native playback testing and packaging > are still pending. Stable releases and MXL Fabrics support are planned. ## Features - Video-only and audio-only playback. - Independent audio and video feeds, including feeds from different domains. - Native synchronized audio/video playback when both feeds are compatible. - Runtime feed apply, stop, resume, remove, and replacement. - Runtime synchronization enable/disable. - Finite or unlimited reconnect attempts with exponential backoff. - JSON playlists with manual selection, timed advance, looping, and failure policies. - Playback, media, timing, retry, and dropped-frame statistics. - Fullscreen mode and an empty idle window when no video is active. - Embedded UI font; no system font installation is required. ## Current requirements - Go 1.26 or newer. - A C/C++ build toolchain for cgo dependencies. - A compatible shared build of `libmxl` and its development headers. - pkg-config metadata for `libmxl`. - SDL3 shared library. - Vulkan loader and a working Vulkan driver. The project currently pins: ```text github.com/qvest-digital/go-mxl v1.1.0-rc.2 ``` The headers used while building and the `libmxl.so` loaded at runtime must be compatible. In particular, synchronized playback requires the native symbol: ```text mxlCreateFlowSynchronizationGroup ``` Check it with: ```sh nm -D /path/to/libmxl.so | grep mxlCreateFlowSynchronizationGroup ``` ## Building on Linux Verified source-build environments: | Environment | Result | Required distribution packages | |---|---|---| | Ubuntu (`ubuntu:latest`, Distrobox) | Build and feed playback successful | `golang`, `libsdl3-dev`, `spdlog-dev` | | Fedora (`fedora:latest`, Distrobox) | Build and feed playback successful | `golang`, `SDL3-devel`, `spdlog-devel`, `g++` | | Ubuntu, separate physical machine | Build and feed playback successful | SDL3, compiler toolchain, and a compatible libmxl installation | These checks used an existing shared libmxl installation with `PKG_CONFIG_PATH` pointing to a valid `libmxl.pc`. Package names can differ between distribution releases. First verify that pkg-config resolves the intended MXL installation: ```sh pkg-config --modversion libmxl pkg-config --cflags --libs libmxl pkg-config --variable=pcfiledir libmxl pkg-config --variable=libdir libmxl ``` If MXL is installed in a nonstandard prefix: ```sh export MXL_PREFIX=/opt/mxl/1.2.0 export PKG_CONFIG_PATH="$MXL_PREFIX/lib/pkgconfig" export LD_LIBRARY_PATH="$MXL_PREFIX/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ``` Then build and test: ```sh go mod download go test ./... mkdir -p build go build -o build/mxl-player ./cmd/mxl-player ``` Confirm which native library the executable will use: ```sh ldd build/mxl-player | grep libmxl ``` Do not copy a `libmxl.pc` file containing paths from another computer. A relocatable SDK file can use `${pcfiledir}` to describe its own location. ## macOS status The SDL3 and Vulkan loaders contain macOS library-name support. The Vulkan dependency is temporarily replaced by a portability fork while the upstream change is reviewed. A macOS source build has succeeded on real Apple hardware and currently requires: - Xcode Command Line Tools; - Go; - SDL3; - Vulkan loader and MoltenVK; - a macOS build of libmxl with matching headers and pkg-config metadata. Nonstandard loader locations can be supplied explicitly: ```sh export SDL3_LIBRARY=/absolute/path/to/libSDL3.dylib export VULKAN_LIBRARY=/absolute/path/to/libvulkan.dylib ``` The resulting executable may currently need an additional libmxl runtime search path, for example: ```sh install_name_tool -add_rpath /absolute/path/to/mxl/lib build/mxl-player ``` These workarounds should not be necessary once the libraries are installed in a normal dynamic-loader path. Native playback testing is still pending. A self-contained `.app` bundle and downloadable GitHub release are planned but do not exist yet. ## Running Start with no configured feed and enter domains/UUIDs in the GUI: ```sh ./build/mxl-player ``` Video only: ```sh ./build/mxl-player \ --domain /dev/shm/mxl \ --video VIDEO_UUID ``` Audio only: ```sh ./build/mxl-player \ --domain /dev/shm/mxl \ --audio AUDIO_UUID ``` Independent video and audio: ```sh ./build/mxl-player \ --video-domain /dev/shm/video \ --video VIDEO_UUID \ --audio-domain /dev/shm/audio \ --audio AUDIO_UUID ``` Start a compatible pair synchronized: ```sh ./build/mxl-player \ --domain /dev/shm/mxl \ --video VIDEO_UUID \ --audio AUDIO_UUID \ --sync ``` Synchronization is not a permanent startup mode. It can be enabled and disabled at runtime from the settings window. ## Command-line options | Option | Short | Meaning | |---|---:|---| | `--help` | `-h` | Show help and exit. | | `--domain PATH` | `-d` | Default domain for feeds without a specific domain. | | `--video-domain PATH` | | Domain for the video feed. | | `--audio-domain PATH` | | Domain for the audio feed. | | `--video UUID` | `-v` | Initial video flow UUID. | | `--audio UUID` | `-a` | Initial audio flow UUID. | | `--sync` | `-s` | Start a configured A/V pair synchronized. | | `--max-attempts N` | | Attempts per playback lifecycle; `0` means unlimited. | | `--playlist FILE` | | Load a JSON playlist. | | `--fullscreen` | `-f` | Start fullscreen. | | `--gpu-id N` | `-g` | Select GPU ID; currently incomplete. | | `--playback-id N` | `-p` | Select the SDL playback-device ID. | | `--list-playback` | | List SDL playback devices and exit. | | `--list-gpu` | | List Vulkan physical devices and exit. | | `--verbose` | | Reserved; currently incomplete. | Run `./build/mxl-player --help` for the authoritative list. ## GUI controls The settings window accepts separate video/audio domains and UUIDs. - **Apply feeds** applies the complete desired configuration. - **Stop** stops a feed while retaining its UUID and domain. - **Resume** starts a stopped configured feed with a fresh lifecycle. - **Remove** stops a feed and clears its configuration. - **Stop all** and **Resume all** operate on both configured feeds. - **Synchronize** switches a compatible active pair between synchronized and independent playback. Keyboard shortcuts: | Key | Action | |---|---| | `F1` | Show or hide settings. | | `F2` | Show or hide statistics. | | `F` | Toggle fullscreen. | | `Q` or `Esc` | Quit. | ## Retry behavior Each playback lifecycle starts at attempt 1. Failed connections/readers are retried with exponential delay from 500 ms up to 5 seconds. ```sh # Retry indefinitely ./build/mxl-player -d /dev/shm/mxl -v VIDEO_UUID --max-attempts 0 # Stop after three failed attempts ./build/mxl-player -d /dev/shm/mxl -v VIDEO_UUID --max-attempts 3 ``` With independent A/V, one failed feed retries without stopping the other. A synchronized pair reconnects as one lifecycle. ## Playlists Start a playlist with: ```sh ./build/mxl-player --playlist playlist.json ``` Example: ```json { "loop": true, "on_failure": "next", "retry": { "max_attempts": 3, "initial_delay": "500ms", "max_delay": "5s" }, "entries": [ { "name": "Synchronized pair", "video": { "domain": "/dev/shm/mxl", "uuid": "VIDEO_UUID" }, "audio": { "domain": "/dev/shm/mxl", "uuid": "AUDIO_UUID" }, "sync": true, "duration": "10s" }, { "name": "Video only", "video": { "domain": "/dev/shm/mxl", "uuid": "ANOTHER_VIDEO_UUID" }, "duration": "15s" }, { "name": "Audio until manually advanced", "audio": { "domain": "/dev/shm/mxl", "uuid": "ANOTHER_AUDIO_UUID" } } ] } ``` Playlist rules: - `duration` uses Go duration syntax such as `500ms`, `10s`, or `2m`. - Missing/empty duration means manual advance. - `loop: true` returns to the first entry after the last. - `on_failure: "wait"` remains on an exhausted entry. - `on_failure: "next"` advances after retry exhaustion. - A synchronized entry must contain both video and audio. - Playlist retry settings override the CLI defaults unless `--max-attempts` was explicitly supplied. The GUI supports entry selection, previous/next navigation, and pausing or resuming a timed entry. ## Development checks ```sh go fmt ./... go test ./... go test -race ./internal/playback ./cmd/mxl-player go vet ./... ``` Live media and GPU/audio integration still require real MXL producers and are not covered by the unit test suite. ## Architecture Playback workers own individual reader lifecycles and retries. Slots ensure that only one worker lifecycle for a feed or synchronized pair runs at a time. The session controller converts the complete desired feed state into an independent or synchronized execution plan. Playlist logic sends the same session commands as the GUI, so it does not bypass lifecycle rules. ```mermaid flowchart TD CLI[CLI startup configuration] GUI[ImGui controls] PC[PlaylistController] PEC[PlaylistEventCoordinator] CMD[SessionCommand channel] SC[SessionController] PLAN[SessionPlan] VS[VideoSlot] AS[AudioSlot] SS[SyncSlot] VW[VideoWorker] AW[AudioWorker] SW[SyncWorker] VF[VideoFactory] AF[AudioFactory] SF[SyncFactory] MXL[go-mxl / libmxl] VB[VideoBridge] VR[Vulkan renderer] AO[SDL audio sink] STATUS[StatusStore] MEDIA[MediaStatsStore] CLI --> CMD GUI --> CMD PC --> CMD CMD --> SC SC --> PLAN PLAN -->|independent video| VS PLAN -->|independent audio| AS PLAN -->|synchronized pair| SS VS --> VW --> VF --> MXL AS --> AW --> AF --> MXL SS --> SW --> SF --> MXL VW --> VB --> VR SW --> VB AW --> AO SW --> AO VW --> STATUS AW --> STATUS SW --> STATUS VW --> MEDIA AW --> MEDIA SW --> MEDIA STATUS --> GUI MEDIA --> GUI STATUS --> PEC SC --> PEC PC --> PEC PEC --> PC ``` ### Main entities | Entity | Responsibility | |---|---| | `SessionConfig` | Complete desired state of both feeds, synchronization request, and retry policy. | | `SessionCommand` | One user or playlist request that produces a new desired session state. | | `SessionController` | Owns the current desired state and reconciles it with the required topology. | | `SessionPlan` | Validated execution plan: idle, independent feeds, or one synchronized pair. | | `VideoSlot` / `AudioSlot` | Own replacement and cancellation of one independent worker lifecycle. | | `SyncSlot` | Owns replacement and cancellation of the synchronized pair lifecycle. | | Workers | Open readers, consume media, apply retry policy, and publish lifecycle status. | | MXL adapter factories | Translate `go-mxl` readers, metadata, and errors into playback contracts. | | `VideoBridge` | Transfers the latest borrowed video frame safely to the render loop. | | `SDLAudioSink` | Interleaves planar F32 samples and manages SDL audio queueing. | | `StatusStore` | Holds the latest lifecycle status for each playback unit. | | `MediaStatsStore` | Holds feed metadata and received-media counters for diagnostics. | | `PlaylistController` | Owns selection, timing, looping, navigation, and failure policy. | | `PlaylistEventCoordinator` | Observes session/status state and tells the playlist when an entry is ready or exhausted. | ### Ownership rules - Every incoming feed configuration represents the complete desired state for that slot. - A slot cancels and joins its current worker before starting a replacement; two worker lifecycles for one slot must never overlap. - Invalid commands do not disturb the current valid session. - Independent audio and video fail and retry independently. - A synchronized pair fails and retries as one lifecycle. - Runtime synchronization changes topology; it is not a startup-only mode. - Cancellation flows from the application context through controllers, slots, workers, readers, and sinks. - Playback owns lifecycle policy. Adapters know MXL details, while renderer, audio output, GUI, and playlists do not. ### Source layout | Path | Contents | |---|---| | `cmd/mxl-player` | Composition root, CLI, main loop, GUI, and playlist file decoding. | | `internal/playback` | Transport-independent state, workers, slots, controllers, retry, playlist, and statistics. | | `internal/adapter/mxl` | Local MXL implementations of playback reader contracts. | | `internal/source` | Low-level local MXL video/audio access and error translation. | | `internal/output` | SDL audio sink and sample interleaving. | | `internal/renderer` | Vulkan video rendering. | | `internal/imgui` | ImGui SDL input and Vulkan rendering integration. | | `internal/sdl` | Minimal dynamically loaded SDL3 bindings used by the player. | | `internal/assets` | Embedded UI assets and their licenses. | MXL Fabrics is planned as an ingress transport that writes remote data into a local MXL flow; it will not be implemented as a separate playback backend. See [`MXLFABRICS_PLAN.md`](MXLFABRICS_PLAN.md). ## Known limitations - Linux builds and feed playback have been verified on Ubuntu and Fedora; broader runtime and hardware coverage is still limited. - macOS/MoltenVK support is not yet release-tested. - Windows support is not currently claimed. - GPU selection and verbose logging flags are incomplete. - There are no prebuilt packages or application bundles yet. - MXL Fabrics ingress is planned but not implemented. - Native MXL headers and runtime libraries must be ABI-compatible. ## Third-party assets The embedded JetBrainsMonoNL Nerd Font Mono is distributed under the SIL Open Font License 1.1. Its copyright notice, license, and source information are in [`internal/assets/fonts`](internal/assets/fonts/). ## License MXL Player is licensed under the GNU General Public License, version 3 or any later version. See [`LICENSE`](LICENSE). The embedded font retains its separate SIL Open Font License 1.1.