README.md
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user