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..3546f4f --- /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="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. Use the server's HTTPS address with a certificate the Apple TV trusts: over plain HTTP the key travels unencrypted. 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..dcdd7b7 --- /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 "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; } +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