tvOS app for playing HLS streams, configurable via AppConfig.
This project contains an Xcode project (AutoSignDisplay.xcodeproj) that builds the AutoSignDisplay tvOS app. To open and run locally:
- Open
AutoSignDisplay.xcodeprojin Xcode. - Select the
AutoSignDisplayscheme (product name will beAutoSignDisplayat runtime) and choose an Apple TV simulator (e.g., "Apple TV 4K (2nd generation)"). - Build and Run (⌘R) to install and launch the app in the tvOS simulator.
Notes:
- The app entrypoint (
AutoSignDisplay) applies managed configuration atinit()by callingAppConfig.applyConfiguration(); to test managed settings locally, add thecom.apple.configuration.manageddictionary to the simulator'sUserDefaultsor useAutoSignDisplay/ManagedAppConfig.example.plistas a reference.
You can run tests using xcodebuild. Example (macOS Terminal):
xcodebuild -project AutoSignDisplay.xcodeproj -scheme AutoSignDisplay -sdk appletvsimulator testThis will build and run the unit and UI tests in the tvOS simulator. Tests in AutoSignDisplayTests prepare UserDefaults directly before instantiating StreamViewModel.
Tip: if xcodebuild appears to hang at the tvOS home screen, explicitly boot a simulator and run tests against its UDID. Example step-by-step:
- List available tvOS simulators and find a suitable UDID:
xcrun simctl list devices --json | jq '.devices["com.apple.CoreSimulator.SimRuntime.tvOS-26-0"][] | {name, udid, state}'- Boot the simulator (replace with the chosen device UDID):
xcrun simctl boot <UDID>
xcrun simctl bootstatus <UDID> -b- Run tests targeting that UDID so xcodebuild uses the booted simulator:
xcodebuild -project AutoSignDisplay.xcodeproj -scheme AutoSignDisplay -sdk appletvsimulator -destination 'id=<UDID>' testNotes & troubleshooting:
- If
simctl bootreports data migration on first boot, wait for it to finish (bootstatus will wait until the device is ready). - If you see warnings about main-actor-isolated properties in tests, ensure tests update
UserDefaultsfrom the main actor (examples inAutoSignDisplayTests/*). - If xcodebuild can't find a simulator runtime, install the matching simulator runtime via Xcode's Components preferences.
AutoSignDisplay/AutoSignDisplayApp.swift— App entrypoint (structAutoSignDisplay); applies managed configuration.AutoSignDisplay/ContentView.swift— main SwiftUI view andUserDefaultskey constants.AutoSignDisplay/StreamViewModel.swift— AVPlayer lifecycle, retry timer, and persisted settings.AutoSignDisplay/AppConfig.swift— maps managed keys intoUserDefaults.AutoSignDisplay/SettingsView.swift— settings UI bound toStreamViewModel.AutoSignDisplay/OnChangeOld.swift— helper providing old+new value for change handlers.
The app implements a small set of focused features for playing and managing HLS streams:
- Enter an HLS stream URL and click "Play Stream" to start playback.
- Channel presets: manage up to 20 saved streams, each with an optional name and a required URL. When a name is set it shows in the Stream Presets list instead of the raw URL. Presets can be locked down via managed configuration.
- Play-on-open: enable or disable "Play on App Open" so the app will automatically play the last-used URL on launch.
- Auto-resume: enable and display an "Auto Resume on Network Interrupt" option so the player will attempt to resume playback after transient network interruptions.
- Retry timeout: how long to wait before retrying a stalled stream.
- Managed App Config: all user-facing options are also configurable via MDM/managed app configuration (see
AutoSignDisplay/ManagedAppConfig.example.plistandAppConfig.applyConfiguration()).
These features are intentionally small and testable; they are exercised by the unit tests in AutoSignDisplayTests and the small UI tests in AutoSignDisplayUITests.
The Settings screen presents Retry Timeout as a row that cycles through a fixed set of intervals — 3, 5, 10, 15, 30, and 60 seconds — rather than a free-text field. Typing digits with the on-screen keyboard is slow on a remote, and this value is normally set once by an administrator.
A value pushed via MDM is always honored, even if it is not one of those six
intervals. The list only governs what pressing the row cycles to; it does not
clamp or validate what RetryTimeout may contain:
AppConfigaccepts any positiveRetryTimeout(a<real>in the managed plist) and writes it straight through toUserDefaults. Non-positive and wrong-typed values are rejected, as before.- The Settings row displays whatever the current value is. A managed value of
7renders as7s. - Pressing the row advances to the first listed interval greater than the
current value, so
7ssteps to10srather than snapping backwards to a listed value. Past60sit wraps to3s. - When
SettingsDisabledis true the row is disabled outright, so a managed value cannot be changed on the device at all.
To offer a different set of intervals, edit SettingsView.retryTimeoutOptions.
That constant affects only the on-device UI — it places no constraint on MDM.
Core behaviors are covered by unit and small integration tests to prevent regressions:
- Playback creation: tests assert a player is created when a valid stream URL exists and "Play on App Open" is enabled.
- Channel presets seeding and managed overrides: tests confirm default presets are seeded, respect the 20-item limit, and that managed configuration enforces a read-only list with a default channel selection.
- Auto-resume logging/behavior: tests inject a test
Loggerimplementation to verify the auto-resume behavior emits expected logs (seeAutoSignDisplayTests/LoggerTests.swift). - Settings persistence: tests set
UserDefaultsvalues (on the main actor) before creating theStreamViewModeland assert persistence and behavior are correct.
How to run tests:
- Use the helper script (recommended). It discovers or boots a simulator, sets the managed/unmanaged state, and pins the run to one device. Runnable from any directory:
./scripts/run-tests.sh # auto-pick a simulator, unmanaged
./scripts/run-tests.sh --udid <UDID> # pin a specific device
./scripts/run-tests.sh --managed # apply the sample managed payload first- Or run directly with xcodebuild and a pinned UDID to avoid cloned simulators:
xcodebuild -project AutoSignDisplay.xcodeproj -scheme AutoSignDisplay -sdk appletvsimulator -destination 'id=<UDID>' -parallel-testing-enabled NO testTo build and launch the app on a simulator:
./scripts/run.sh # build, install, launch
./scripts/run.sh --managed # launch with a managed payload applied
./scripts/run.sh --clean --release # wipe app data, build Release
./scripts/run.sh --dry-run # print the plan without doing itTest guidance:
- When tests need to change
UserDefaultsvalues that are main-actor-isolated (for example keys defined inContentView), updateUserDefaultsfrom the main actor usingawait MainActor.run { ... }to avoid main-actor warnings and Sendable capture issues. - Pin a UDID and disable parallel testing for consistent, single-device test runs (this avoids xcodebuild creating cloned simulator devices).
- If you add features that change persistence or playback lifecycle, add or update unit tests in
AutoSignDisplayTeststhat set upUserDefaults, create aStreamViewModel, and assert the expectedplayerstate and retry behavior.
AutoSignDisplay.xcodeproj/xcshareddata/xcschemes/AutoSignDisplay.xcscheme is
checked in, and needs to stay that way. Without it xcodebuild falls back to an
auto-generated scheme whose buildables resolve no eligible destinations, so any
-scheme … build invocation fails with:
error: Supported platforms for the buildables in the current scheme is empty.
The test action tolerates the auto-generated scheme, which makes this confusing to diagnose — tests pass while builds fail. If you recreate the project or add a target, confirm the scheme is still marked Shared in Product → Scheme → Manage Schemes.
For convenience there's a small helper script that will discover or accept a tvOS simulator UDID, boot it if necessary, wait for readiness, and run the tests:
# Dry-run to preview actions:
./scripts/run-tests.sh --dry-run
# Let the script pick an available tvOS simulator and run tests:
./scripts/run-tests.sh
# Use a specific UDID (replace with the UDID from simctl list):
./scripts/run-tests.sh --udid 2414D6A2-3663-4DFF-9EED-DACA32FB64B5The script is executable by default in the repository (mode 100755). If you ever need to restore exec permission locally:
chmod +x scripts/run-tests.sh