452 lines
13 KiB
Markdown
452 lines
13 KiB
Markdown
# 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. Linux is the currently tested
|
|
> platform. macOS support through Vulkan/MoltenVK is being tested. Packaging,
|
|
> 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
|
|
|
|
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 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
|
|
```
|
|
|
|
These variables should not be necessary once the libraries are installed in a
|
|
normal dynamic-loader path. A self-contained `.app` bundle is planned but does
|
|
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 is the only platform currently exercised regularly.
|
|
- 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.
|