From c0f614dd64a28d0e34664636574c56665eafa5e4 Mon Sep 17 00:00:00 2001 From: kristofferR <481270+kristofferR@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:39:28 +0200 Subject: [PATCH 1/3] feat: add a minimal tvos example app for testing the remux engine Lumen has no app to run on an Apple TV, so testing the remux engine there, including the APAC path, needed one. The example plays a URL from the launch environment with ProAVPlayer first and KSMEPlayer behind it, through the unchanged KSVideoPlayerView. Signing team and bundle identifier come from the command line, so the project carries neither. --- Examples/LumenPlayer/.gitignore | 4 ++ Examples/LumenPlayer/README.md | 36 ++++++++++++++++ .../LumenPlayer/Sources/LumenPlayerApp.swift | 41 +++++++++++++++++++ Examples/LumenPlayer/build-and-install.sh | 16 ++++++++ Examples/LumenPlayer/project.yml | 41 +++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 Examples/LumenPlayer/.gitignore create mode 100644 Examples/LumenPlayer/README.md create mode 100644 Examples/LumenPlayer/Sources/LumenPlayerApp.swift create mode 100755 Examples/LumenPlayer/build-and-install.sh create mode 100644 Examples/LumenPlayer/project.yml diff --git a/Examples/LumenPlayer/.gitignore b/Examples/LumenPlayer/.gitignore new file mode 100644 index 0000000..1d67241 --- /dev/null +++ b/Examples/LumenPlayer/.gitignore @@ -0,0 +1,4 @@ +DerivedData/ +LumenPlayer.xcodeproj/ +Info.plist +*.log diff --git a/Examples/LumenPlayer/README.md b/Examples/LumenPlayer/README.md new file mode 100644 index 0000000..1434629 --- /dev/null +++ b/Examples/LumenPlayer/README.md @@ -0,0 +1,36 @@ +# Lumen Player for Apple TV + +A minimal tvOS app that plays one URL through `KSVideoPlayerView`, with `ProAVPlayer` as the first engine and `KSMEPlayer` as the fallback. It exists to test the remux engine on a real Apple TV, including TrueHD Atmos and DTS:X tracks converted to APAC. + +## Build and install + +Use macOS with Xcode and [XcodeGen](https://github.com/yonaskolb/XcodeGen). Signing stays out of the project: pass your team and a bundle identifier it can sign. + +```sh +cd Examples/LumenPlayer +LUMEN_DEVELOPMENT_TEAM="TEAM_ID" LUMEN_BUNDLE_ID="com.example.lumen" ./build-and-install.sh "DEVICE_ID" +``` + +`xcrun devicectl list devices` shows the Apple TV's identifier. The script generates the project, builds the app with the repository's Lumen and installs it. + +## Run + +The app reads its configuration from the launch environment. With `devicectl`, prefix each variable with `DEVICECTL_CHILD_`: + +| Variable | Meaning | +|---|---| +| `LUMEN_URL` | The media to play, such as a media server's direct-play URL | +| `LUMEN_TITLE` | Optional title for the player interface | +| `LUMEN_START` | Optional start time in seconds | +| `LUMEN_LOG` | `info` to log the engines' decisions, such as a fallback from APAC to FLAC | +| `OS_ACTIVITY_DT_MODE` | `enable` to mirror the log to the console | + +For Jellyfin, with an API key in `KEY` and the item and media source ID in `ID`: + +```sh +export DEVICECTL_CHILD_LUMEN_URL="http://jellyfin:8096/Videos/$ID/stream?static=true&MediaSourceId=$ID&ApiKey=$KEY" +export DEVICECTL_CHILD_LUMEN_START=420 +xcrun devicectl device process launch --device "DEVICE_ID" --terminate-existing --console "com.example.lumen" +``` + +Keep the key out of shell history and logs. diff --git a/Examples/LumenPlayer/Sources/LumenPlayerApp.swift b/Examples/LumenPlayer/Sources/LumenPlayerApp.swift new file mode 100644 index 0000000..8596e0a --- /dev/null +++ b/Examples/LumenPlayer/Sources/LumenPlayerApp.swift @@ -0,0 +1,41 @@ +import Lumen +import SwiftUI + +/// Plays the URL in the launch environment through the remux engine, with the FFmpeg engine behind it. +@main +struct LumenPlayerApp: App { + init() { + KSOptions.firstPlayerType = ProAVPlayer.self + KSOptions.secondPlayerType = KSMEPlayer.self + if ProcessInfo.processInfo.environment["LUMEN_LOG"] == "info" { + KSOptions.logLevel = .info + } + } + + var body: some Scene { + WindowGroup { + PlayerScreen() + } + } +} + +struct PlayerScreen: View { + private let environment = ProcessInfo.processInfo.environment + + var body: some View { + if let url = environment["LUMEN_URL"].flatMap(URL.init(string:)) { + KSVideoPlayerView(url: url, options: makeOptions(), title: environment["LUMEN_TITLE"]) + .ignoresSafeArea() + } else { + Color.black + .ignoresSafeArea() + .onAppear { print("Launch with LUMEN_URL set to the media to play.") } + } + } + + private func makeOptions() -> KSOptions { + let options = KSOptions() + options.startPlayTime = environment["LUMEN_START"].flatMap(TimeInterval.init) ?? 0 + return options + } +} diff --git a/Examples/LumenPlayer/build-and-install.sh b/Examples/LumenPlayer/build-and-install.sh new file mode 100755 index 0000000..16e8a88 --- /dev/null +++ b/Examples/LumenPlayer/build-and-install.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +# Usage: LUMEN_DEVELOPMENT_TEAM=TEAM_ID LUMEN_BUNDLE_ID=BUNDLE_ID ./build-and-install.sh DEVICE_ID +APP_DIR="$(cd "$(dirname "$0")" && pwd)" +DEVICE_ID="${1:?Usage: ./build-and-install.sh DEVICE_ID}" +TEAM="${LUMEN_DEVELOPMENT_TEAM:?Set LUMEN_DEVELOPMENT_TEAM to the team that signs the app}" +BUNDLE_ID="${LUMEN_BUNDLE_ID:?Set LUMEN_BUNDLE_ID to a bundle identifier that team can sign}" +xcodegen generate --spec "$APP_DIR/project.yml" +xcodebuild -project "$APP_DIR/LumenPlayer.xcodeproj" -scheme LumenPlayer \ + -destination "id=$DEVICE_ID" -derivedDataPath "$APP_DIR/DerivedData" \ + -allowProvisioningUpdates DEVELOPMENT_TEAM="$TEAM" PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \ + build > "$APP_DIR/build.log" 2>&1 \ + || { tail -n 30 "$APP_DIR/build.log"; echo "Build failed; see $APP_DIR/build.log" >&2; exit 1; } +xcrun devicectl device install app --device "$DEVICE_ID" \ + "$APP_DIR/DerivedData/Build/Products/Debug-appletvos/LumenPlayer.app" diff --git a/Examples/LumenPlayer/project.yml b/Examples/LumenPlayer/project.yml new file mode 100644 index 0000000..554ed49 --- /dev/null +++ b/Examples/LumenPlayer/project.yml @@ -0,0 +1,41 @@ +name: LumenPlayer +options: + deploymentTarget: + tvOS: '16.0' +packages: + Lumen: + path: ../.. +settings: + base: + SWIFT_VERSION: '5.0' + CODE_SIGN_STYLE: Automatic + TARGETED_DEVICE_FAMILY: '3' +targets: + LumenPlayer: + type: application + platform: tvOS + sources: + - Sources + dependencies: + - package: Lumen + product: Lumen + info: + path: Info.plist + properties: + NSAppTransportSecurity: + NSAllowsLocalNetworking: true + NSAllowsArbitraryLoadsForMedia: true + settings: + base: + # Signing team and bundle identifier come from build-and-install.sh. + PRODUCT_BUNDLE_IDENTIFIER: com.example.LumenPlayer + GENERATE_INFOPLIST_FILE: YES + INFOPLIST_KEY_CFBundleDisplayName: Lumen + INFOPLIST_KEY_UILaunchScreen_Generation: YES + INFOPLIST_KEY_UIUserInterfaceStyle: Dark + ASSETCATALOG_COMPILER_APPICON_NAME: '' +schemes: + LumenPlayer: + build: + targets: + LumenPlayer: all From dc0e10d0c4f0f47dbdb0409f49831af4d06495d5 Mon Sep 17 00:00:00 2001 From: kristofferR <481270+kristofferR@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:09:43 +0200 Subject: [PATCH 2/3] fix: build the example for the generic tvos destination xcodebuild matches a destination id against the hardware UDID, while devicectl lists a CoreDevice identifier on Xcode releases before 27. Build for any Apple TV and keep the identifier for the install alone. --- Examples/LumenPlayer/build-and-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/LumenPlayer/build-and-install.sh b/Examples/LumenPlayer/build-and-install.sh index 16e8a88..dcdd7b7 100755 --- a/Examples/LumenPlayer/build-and-install.sh +++ b/Examples/LumenPlayer/build-and-install.sh @@ -8,7 +8,7 @@ TEAM="${LUMEN_DEVELOPMENT_TEAM:?Set LUMEN_DEVELOPMENT_TEAM to the team that sign BUNDLE_ID="${LUMEN_BUNDLE_ID:?Set LUMEN_BUNDLE_ID to a bundle identifier that team can sign}" xcodegen generate --spec "$APP_DIR/project.yml" xcodebuild -project "$APP_DIR/LumenPlayer.xcodeproj" -scheme LumenPlayer \ - -destination "id=$DEVICE_ID" -derivedDataPath "$APP_DIR/DerivedData" \ + -destination "generic/platform=tvOS" -derivedDataPath "$APP_DIR/DerivedData" \ -allowProvisioningUpdates DEVELOPMENT_TEAM="$TEAM" PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \ build > "$APP_DIR/build.log" 2>&1 \ || { tail -n 30 "$APP_DIR/build.log"; echo "Build failed; see $APP_DIR/build.log" >&2; exit 1; } From d8ba4635647e05e269d49bf61244fc512c2c5510 Mon Sep 17 00:00:00 2001 From: kristofferR <481270+kristofferR@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:45:52 +0200 Subject: [PATCH 3/3] docs: use an https server in the example's jellyfin command The API key is part of the URL, so a plain HTTP request exposes it to the network. --- Examples/LumenPlayer/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/LumenPlayer/README.md b/Examples/LumenPlayer/README.md index 1434629..3546f4f 100644 --- a/Examples/LumenPlayer/README.md +++ b/Examples/LumenPlayer/README.md @@ -28,9 +28,9 @@ The app reads its configuration from the launch environment. With `devicectl`, p For Jellyfin, with an API key in `KEY` and the item and media source ID in `ID`: ```sh -export DEVICECTL_CHILD_LUMEN_URL="http://jellyfin:8096/Videos/$ID/stream?static=true&MediaSourceId=$ID&ApiKey=$KEY" +export DEVICECTL_CHILD_LUMEN_URL="https://jellyfin.example.com/Videos/$ID/stream?static=true&MediaSourceId=$ID&ApiKey=$KEY" export DEVICECTL_CHILD_LUMEN_START=420 xcrun devicectl device process launch --device "DEVICE_ID" --terminate-existing --console "com.example.lumen" ``` -Keep the key out of shell history and logs. +Keep the key out of shell history and logs. Use the server's HTTPS address with a certificate the Apple TV trusts: over plain HTTP the key travels unencrypted.