Skip to content

Commit bba07e6

Browse files
author
Vincent Herbst
committed
docs: add Examples/MinimalPlayer drop-in SwiftUI sample
Examples/MinimalPlayer/MinimalPlayerApp.swift is a 90-line @main App that demonstrates the smallest viable AetherEngine integration: load, play, pause, stop, and live readouts of state / currentTime / duration / videoFormat via the engine's Combine publishers. Examples/README.md is the 5-step click-by-click setup (new SwiftUI app, add SPM dep, drop file in, point at URL, run). Pragmatic choice: dropping a .swift file into a host's own Xcode app beats maintaining a separate .xcodeproj in this repo, since .xcodeproj XML rots across Xcode upgrades and a SwiftPM executable target can't cleanly build a tvOS / iOS .app bundle. Adopters who want to see the real-world pattern read Sodalite source.
1 parent 9588d93 commit bba07e6

3 files changed

Lines changed: 156 additions & 1 deletion

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// MinimalPlayerApp.swift
2+
//
3+
// Smallest viable AetherEngine integration. Drop this file into a new
4+
// SwiftUI tvOS / iOS / macOS app, add the AetherEngine Swift Package as
5+
// a dependency, set the file's @main App as the app entry point, and
6+
// run. About 90 lines of host code — everything else (HDR routing,
7+
// Atmos passthrough, codec dispatch, subtitle decoding) is the engine.
8+
//
9+
// See Examples/README.md for the click-by-click setup.
10+
11+
import SwiftUI
12+
import AetherEngine
13+
14+
@main
15+
struct MinimalPlayerApp: App {
16+
17+
/// Engine is created once for the app's lifetime. AetherEngine is
18+
/// designed to be a long-lived instance: hosts call `load(url:)`
19+
/// for each new title against the same engine rather than rebuilding
20+
/// the audio session + display-criteria controller per playback.
21+
let engine: AetherEngine = {
22+
do {
23+
return try AetherEngine()
24+
} catch {
25+
fatalError("AetherEngine init failed: \(error)")
26+
}
27+
}()
28+
29+
var body: some Scene {
30+
WindowGroup {
31+
ContentView(engine: engine)
32+
}
33+
}
34+
}
35+
36+
struct ContentView: View {
37+
let engine: AetherEngine
38+
39+
/// Replace with a real source URL — file://, http://, or https://.
40+
/// AetherEngine probes the container, picks the right pipeline,
41+
/// and starts segment production automatically once `load` returns.
42+
@State private var sourceURL = URL(string: "https://example.com/your-video.mkv")!
43+
@State private var loadError: String?
44+
45+
// Observed engine state. The engine publishes via Combine; SwiftUI
46+
// bridges them through `onReceive` modifiers below.
47+
@State private var playerState: PlaybackState = .idle
48+
@State private var currentTime: Double = 0
49+
@State private var duration: Double = 0
50+
@State private var videoFormat: VideoFormat = .sdr
51+
52+
var body: some View {
53+
VStack(spacing: 16) {
54+
// The render surface. SwiftUI variant; UIKit/AppKit hosts
55+
// use `AetherPlayerView()` and `engine.bind(view:)` instead.
56+
AetherPlayerSurface(engine: engine)
57+
.aspectRatio(16/9, contentMode: .fit)
58+
.background(Color.black)
59+
60+
// Minimal transport. Real hosts replace this with a proper
61+
// focus-driven transport bar; this is just to prove the
62+
// engine reacts to commands.
63+
HStack(spacing: 24) {
64+
Button("Load") {
65+
Task { await load() }
66+
}
67+
Button(playerState == .playing ? "Pause" : "Play") {
68+
playerState == .playing ? engine.pause() : engine.play()
69+
}
70+
.disabled(playerState == .idle || playerState == .loading)
71+
Button("Stop") {
72+
engine.stop()
73+
}
74+
.disabled(playerState == .idle)
75+
}
76+
77+
// State readout. videoFormat tells you what dynamic range
78+
// the panel is currently presenting (already clamped to
79+
// panel capability — a DV source on a non-DV TV reads as
80+
// .hdr10, not .dolbyVision).
81+
VStack(alignment: .leading, spacing: 4) {
82+
Text("State: \(String(describing: playerState))")
83+
Text("Time: \(formatTime(currentTime)) / \(formatTime(duration))")
84+
Text("Format: \(String(describing: videoFormat))")
85+
if let err = loadError {
86+
Text("Error: \(err)").foregroundStyle(.red)
87+
}
88+
}
89+
.font(.system(.body, design: .monospaced))
90+
}
91+
.padding()
92+
.onReceive(engine.$state) { playerState = $0 }
93+
.onReceive(engine.$currentTime) { currentTime = $0 }
94+
.onReceive(engine.$duration) { duration = $0 }
95+
.onReceive(engine.$videoFormat) { videoFormat = $0 }
96+
}
97+
98+
private func load() async {
99+
loadError = nil
100+
do {
101+
try await engine.load(url: sourceURL)
102+
engine.play()
103+
} catch {
104+
loadError = "\(error)"
105+
}
106+
}
107+
108+
private func formatTime(_ seconds: Double) -> String {
109+
guard seconds.isFinite, seconds >= 0 else { return "--:--" }
110+
let s = Int(seconds)
111+
return String(format: "%02d:%02d:%02d", s / 3600, (s / 60) % 60, s % 60)
112+
}
113+
}

‎Examples/README.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Examples
2+
3+
Drop-in samples that show the smallest viable AetherEngine integration.
4+
5+
## MinimalPlayer
6+
7+
[`MinimalPlayer/MinimalPlayerApp.swift`](MinimalPlayer/MinimalPlayerApp.swift) is a complete SwiftUI app entry point that loads, plays, and reports state for a single source URL. About 90 lines including comments and UI state plumbing.
8+
9+
### Try it in 5 minutes
10+
11+
1. **Create an Xcode project.** File › New › Project. Pick the SwiftUI template for the platform you want (tvOS / iOS / macOS App). Any product name; no tests target needed.
12+
13+
2. **Add AetherEngine as a Swift Package dependency.** File › Add Package Dependencies, paste:
14+
```
15+
https://github.com/superuser404notfound/AetherEngine
16+
```
17+
Dependency Rule: Up to Next Major Version, starting from `1.5.0`. Add the `AetherEngine` library product to your app target.
18+
19+
3. **Drop the file in.** Replace the Xcode template's default `App.swift` (or whatever the generated `@main` file is called) with the contents of [`MinimalPlayerApp.swift`](MinimalPlayer/MinimalPlayerApp.swift). The file is self-contained: it defines both the `@main App` struct and the `ContentView`.
20+
21+
4. **Point at a real source URL.** Edit the `sourceURL` line:
22+
```swift
23+
@State private var sourceURL = URL(string: "https://example.com/your-video.mkv")!
24+
```
25+
Use any file://, http://, or https:// URL the engine can demux. MKV / MP4 / WebM / MPEG-TS / AVI all work.
26+
27+
5. **Run.** Hit ▶︎ in Xcode. The Load button kicks off the demux + HLS-fMP4 pipeline; Play / Pause / Stop hit the engine directly. State, time, duration, and detected video format update live via the engine's Combine publishers.
28+
29+
### What's not in the minimal example
30+
31+
To stay readable the sample omits things real apps care about:
32+
33+
- **Subtitles.** `engine.subtitleTracks` lists them; `engine.selectSubtitleTrack(index:)` activates one. `engine.$subtitleCues` publishes the cues — text or `CGImage` — that the host paints over the surface.
34+
- **Audio track switching.** `engine.audioTracks` + `engine.selectAudioTrack(index:)`.
35+
- **Resume position.** `engine.load(url:, startPosition: 347.5)`.
36+
- **HTTP headers** for authenticated sources. Pass them in `LoadOptions(httpHeaders: [...])`.
37+
- **HDR / Dolby Vision routing on tvOS.** Requires the engine-driven sole-writer pattern (see README › Host setup on tvOS). The minimal sample relies on default routing; for production tvOS hosts on HDR content, set `appliesPreferredDisplayCriteriaAutomatically = false` on your `AVPlayerViewController` and pass `LoadOptions(matchContentEnabled:, panelIsInHDRMode:)` populated from the runtime EDR state.
38+
- **Now Playing / lock-screen integration.** Subscribe to `engine.$currentAVPlayer` and feed it to `MPNowPlayingSession`. See `Sodalite` for a reference implementation.
39+
40+
For all of these, read the inline docstrings on `AetherEngine`, `LoadOptions`, and `TrackInfo` in `Sources/AetherEngine/`. They're the canonical contract.

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@ Subtitle cues land in raw source PTS. On the native path, AVPlayer's HLS clock s
111111
Install via Swift Package Manager:
112112

113113
```swift
114-
.package(url: "https://github.com/superuser404notfound/AetherEngine", branch: "main")
114+
.package(url: "https://github.com/superuser404notfound/AetherEngine", from: "1.5.0")
115115
```
116116

117+
See [`Examples/MinimalPlayer/`](Examples/MinimalPlayer/MinimalPlayerApp.swift) for a complete drop-in SwiftUI app that loads, plays, and reports state for a single source URL. About 90 lines of host code — copy the file into a new Xcode tvOS / iOS / macOS app, point at a real URL, run.
118+
117119
## Host setup on tvOS
118120

119121
For HDR / Dolby Vision sources to play reliably on tvOS 26.5+, the

0 commit comments

Comments
 (0)