Architecture overview¶
Labstream is a SwiftUI media client for Plex, Jellyfin, and Emby. The app code in
Labstream/ owns UI and most live orchestration; the local Swift package in PMSKit/
owns reusable request builders, wire models, and policies. Most PMSKit behavior is pure
and can be tested without an app process, simulator, Keychain, filesystem, or media
server; a small set of reusable infrastructure is intentionally effectful.
flowchart TD
accTitle: App and package composition
accDescr: Platform entry points create long-lived app services used by SwiftUI. App-owned playback, downloads, music, and SharePlay use PMSKit policies and requests while retaining their own framework and persistence effects.
Entry[Platform App entry point] --> Runtime[AppRuntime]
Runtime --> Model[AppModel]
Runtime --> Auth[AuthManager]
Runtime --> Downloads[DownloadManager except tvOS]
Runtime --> Music[MusicPlayerController]
Runtime --> Bootstrap[SessionBootstrap]
Runtime --> Catalog[LibraryCatalogRepository]
Runtime --> Metadata[MetadataRepository]
Runtime --> Artwork[ArtworkPipeline]
Entry --> SharePlay[visionOS live WatchTogetherCoordinator]
UI[SwiftUI UI] --> Model
UI --> Auth
UI -->|download-capable products| Downloads
UI --> Music
UI --> Player[PlaybackController]
UI --> SharePlay
SharePlay --> Player
Auth --> PMSKit[PMSKit requests, models, policies]
Player --> PMSKit
Downloads -->|download-capable products| PMSKit
Music --> PMSKit
SharePlay --> PMSKit
Player --> AV[AVFoundation]
Downloads --> Store[DownloadStore]
Downloads --> BG[BackgroundDownloadSession]
Store --> Files[Application Support]
Native targets and release trains¶
The repository contains four native application targets in one coordinated pre-release
universal-purchase set: visionOS, universal iOS/iPadOS, streaming-only tvOS, and native macOS. All four
attach the file-system-synchronized Labstream/Shared/ root plus exactly one root under
Labstream/Platforms/. Vision Pro, mobile, and Mac additionally attach the non-overlapping
Labstream/Capabilities/Downloads/ root; tvOS cannot compile or construct that capability.
| Target / scheme | Entry point | Platform |
|---|---|---|
Labstream |
Labstream/Platforms/visionOS/App/Labstream.swift |
visionOS |
LabstreamMobile |
Labstream/Platforms/Mobile/App/LabstreamMobile.swift |
iOS and iPadOS |
LabstreamMac |
Labstream/Platforms/macOS/App/LabstreamMac.swift |
native macOS, not Catalyst |
LabstreamTV |
Labstream/Platforms/tvOS/App/LabstreamTV.swift |
tvOS |
Marketing versions remain independently configurable even when a coordinated release aligns them. The Xcode project is the source of truth for current version and deployment settings; synchronized numbers do not prove that any platform has passed its hardware, TestFlight, or review gates.
Shared files still use conditional compilation for genuinely inline framework and presentation
differences. Capability and build variants also use #if canImport(...),
#if targetEnvironment(simulator), and #if DEBUG; these are compile-time conditions, not
runtime feature flags. Whole-platform entrypoints/adapters instead rely on exclusive target
membership and contain no redundant whole-file platform guard. Authenticated navigation no longer
embeds four shells in one conditional view: RootView owns common composition,
RootNavigationCoordinator owns shared transitions, and each exclusive
Labstream/Platforms/*/UI/*RootShell.swift file owns native presentation. The densest remaining
shared-platform branches are in the shared player and login/detail UI.
App-lifetime composition¶
Every target guards the optional result of AppRuntime.make() in its App initializer
and has a SecureStorageUnavailableView fallback. The current factory returns a service
graph even when the durable client identifier cannot be read by using a process-local
identifier for that launch; credentials themselves still fail closed. AppRuntime
constructs the common long-lived services and the one launch bootstrap:
AppModel: active backend and the three live server/session lanes.AuthManager: authentication, restore, backend switching, and Keychain writes. It privately ownsEmbyConnectAuthFlow(secret-bearing pending Connect state and Emby server exchange/commit) andAuthorizationPollingCoordinator(exact-attempt ownership of the one live Plex PIN, Jellyfin Quick Connect, or Emby Connect polling task). Global attempt admission remains onAuthAttemptAuthority.DownloadManager: the cross-backend offline queue and transfer orchestration, absent on tvOS.MusicPlayerController: the app-lifetime audio queue and player.SessionBootstrap: one-time restore and browse-gate state shared across scene recreation.LibraryCatalogRepository: exact-authority section/view enumeration shared across browse, search, music, system-entry, and SharePlay consumers.MetadataRepository: exact-authority item hydration with separate display and action-authority policies.ArtworkPipeline: actor-owned authenticated/local artwork scheduling, joining, and bounded memory caches shared by presentation and system-media consumers.ArtworkShimmerClock: one app-lifetime, reference-counted placeholder ticker.
Only the visionOS entry point additionally owns the real CustomCinemaSessionStore and
WatchTogetherCoordinator; it injects the same live instances into the main window and Custom
Cinema. Non-vision products construct neither capability. Keeping the live visionOS objects above the main window matters
because entering Cinema can dismiss the window while the authenticated session, active player,
and SharePlay coordination must survive until it reopens.
ContentView receives the exact AppRuntime and is the launch gate. It registers system-entry
routing, restores saved sessions once, and presents restoring, login, authenticated browse, or the
restricted Offline launch surface on download-capable products; tvOS has no Offline state. It also
starts download reconciliation where that capability exists.
RootView receives the same runtime as the authenticated navigation shell and injects the app
model, supported download manager, and music player into the view environment. Its visionOS-only SharePlay consumers read
the coordinator inherited from the visionOS scene environment.
Session and backend model¶
AppModel keeps separate Plex, Jellyfin, and Emby session state. Switching the active
backend does not overwrite another backend's credentials. At launch,
AuthManager.restoreSession() runs under one global generation-scoped authorization authority and
restores only the selected user-facing lane. After that completes, launch derives the distinct
inactive backends that own durable active download rows and demand-hydrates only those lanes before
download reconcile. Paused, failed, and completed rows stay cold until their explicit action edge;
tvOS has neither downloads nor inactive-download hydration.
Login, Quick Connect/Connect, restore, and server selection cannot publish stale results
from an older authorization attempt. System-entry fallback restore uses
restoreSessionIfNoAuthorizationInProgress() rather than taking authority from a login the
user is completing. AuthorizationPollingCoordinator separately owns polling-task lifetime and
rejects stale exact-owner finish/cancel requests, while AuthAttemptAuthority remains the only
authority that admits state or credential publication.
Token-free identities serve two different purposes:
- a stable server/user key scopes persisted preferences such as library visibility;
- a revisioned browse-session key invalidates navigation, search, paging, and music queues after a backend, server, user, or authentication-session change.
Online navigation is session-scoped. The Offline library is deliberately cross-backend: its records carry their backend identity and remain available when the active browse backend changes.
App/PMSKit boundary¶
The dominant boundary keeps app-lifecycle effects in the app target:
- SwiftUI observation, navigation, presentation, and target lifecycle;
- Keychain and UserDefaults access;
- live
URLSessionexecution and background-session delegates; - app-owned file mutation, offline-index persistence, and filesystem orchestration;
AVPlayer, audio sessions, Picture in Picture, Now Playing, RealityKit, and live GroupActivities/AVPlayerPlaybackCoordinator attachment;- MetricKit, Spotlight, App Intents, and share/export UI.
PMSKit primarily owns behavior that can be expressed as input-to-output decisions:
- Plex, Jellyfin, and Emby request construction and response decoding;
- shared media and offline models;
- playback, download, retry, paging, routing, redaction, and SharePlay identity/readiness policies;
- state-machine decisions that can be exercised by
swift test.
There are deliberate package-side exceptions. They include the MediaBrowser
URLSession request executor, the reusable loopback HLS proxy and upstream connection
machinery in PMSKit/Sources/PMSKit/MediaSession/, the locked diagnostic ring buffer, and
the shared protected-file writes in
PMSKit/Sources/PMSKit/Security/CredentialArtifactStorage.swift.
These types keep framework effects behind narrow, injectable/testable APIs; they do not
move SwiftUI, AVPlayer ownership, background-session delegation, or app persistence into
the package. The app still owns background-session delegates and most live URLSession
work; “all live URLSession execution lives in the app” is not true.
The boundary is not a mandate to erase backend differences. Plex has native hub, optimizer, and music behavior; Jellyfin and Emby share MediaBrowser-shaped providers where their APIs genuinely align, while their authentication, playback, and download details remain explicit.
Browse, search, and music¶
- Pure Plex hubs, search, and metadata requests are built by
PlexBrowseRequestin PMSKit. Video-library section pages, filters, and sorts usePlexLibraryBrowseRequest. The small app facade inLabstream/Shared/Backend/PlexBrowseAPI.swiftcovers both.PlexBrowseServicepins one immutable backend session and client identity, retains transport isolation through the sharedPlexClient, and moves response decoding and normalization off the main actor before returning completed values to its MainActor callers. - Jellyfin and Emby retain concrete facades under
Labstream/Shared/Backend/Jellyfin/andLabstream/Shared/Backend/Emby/, backed byMediaBrowserBrowseCoreonly for their genuinely shared browse behavior. The MainActor facades snapshot an immutable authenticated context and transport; the Sendable core executes requests, decodes JSON, and maps DTOs off the main actor, returning only completed values to UI owners. - Backend-neutral paging models live in
Labstream/Shared/Backend/Paging/. The grouped, deduplicated search-result model lives inPMSKit/Sources/PMSKit/Search/SearchResults.swift, whileLabstream/Shared/UI/SearchView.swiftexecutes the active backend search, renders the sections, and routes music versus standard results. MediaBrowser search and video/music A-Z probes reuse one ordered bounded fan-out primitive with a four-request ceiling; search remains fail-fast while letter-probe failures continue to degrade individually. - Sparse library pages use one model-owned flight per page: exact-page callers join, each waiter can cancel independently, last-waiter/reset cancellation retires the work, and stale completions cannot publish. Movie-version collapse keeps stable first-seen groups and updates only the dense projection positions touched by each arriving page. Long playlists use a separate positional paging model so duplicate tracks and native server order survive page boundaries; page zero can render early, but Play and queue actions wait for the complete list.
LibraryCatalogLoaderis the behavior-neutral native section/view enumeration seam.LibraryCatalogRepository, owned once byAppRuntime, caches and joins reads only for an exact backend plus opaque authenticated authority. It evicts failed loads, serializes force refresh, and rejects stale queued/completed work. Libraries, Home, Search, Music, the Mac sidebar, visibility editing, system entries, and Watch Together consume this shared enumeration; their visibility, ordering, destination, query, and presentation policies remain outside it.- Plex Home remains its native server-composed
/hubsresponse. Jellyfin/Emby Home executes one duplicate-safe canonical rail plan under a four-request ceiling, publishes successful rails as each request completes, preserves empty/successful results across partial failure, and performs one failed-key-only retry. Exact authority, generation, and attempt fences reject late results; only a complete non-degraded snapshot becomes the pinned loaded identity. MusicProvideris the backend-neutral music boundary. Plex supplies richer native artist metadata; Jellyfin and Emby shareMediaBrowserMusicProvider.MusicPlayerControllersurvives navigation. Its queue is tied to the browse-session identity so stale media IDs are never resolved against a different server.
AppModel also vends an exact AuthenticatedBrowseSessionContext for catalog and metadata
repositories. It carries an opaque process-local authority generation alongside the immutable
backend session and client identity; server, user, credential, or client-identity replacement
mints a new authority without placing credentials or raw server identity in cache keys.
MetadataRepository keys item reads by backend, opaque authority, and item identifier. Display
reads reuse a value for ten seconds, then may paint it stale for up to sixty seconds while one
repository-owned refresh runs. Authoritative reads ignore completed cache values, although they
may join the exact current native read. Provenance and repository admission prevent reused, stale,
expired, locally watched-patched, or superseded values from authorizing Play, Download, or watched
mutation; an exact fresh native Detail value can therefore support immediate Play without a second
read. A successful watched mutation patches only the exact presentation entry and never upgrades
its authority.
AppRuntime also owns one ArtworkPipeline. MediaArtwork keeps authenticated requests private
behind token-free descriptors keyed by backend, opaque authority, purpose, source digest, and
requested pixels; local descriptors replace auth authority with an opaque persisted owner plus
monotonic poster generation. The actor core joins exact requests, gives each waiter independent
cancellation, schedules at most four active loads per canonical origin with priority, downsamples
and eagerly decodes through ImageIO off the main actor, and bounds compressed, decoded, and
definitive-4xx negative caches by cost. Its ephemeral remote transport disables URL cache, cookies,
and credential storage. PosterImage, music/video system Now Playing (including visionOS scoped
metadata), AVPlayerItem external metadata, offline rows, and offline player artwork all use this
pipeline; native framework bridges receive only completed DecodedImage or original encoded bytes.
Asynchronous success and terminal-failure publication is fenced by the exact descriptor, pipeline
instance, and owning playback/view generation rather than cancellation alone. Settings clears
positive and negative memory state on that same app-lifetime pipeline and advances its epoch so
pre-clear work cannot repopulate those caches. Persisted offline
posterGeneration changes on publish, replacement, and clear so a same-path, same-byte-count
replacement cannot reuse stale pixels. One reference-counted app-lifetime shimmer clock serves all
visible placeholders in the main UI and visionOS Custom Cinema ImmersiveSpace, while Reduce Motion
starts no animation work.
AVKit-hosted chapter stills remain request-backed because their independent hosting environment
does not inject the pipeline or an exact pixel contract. BIF and sprite-sheet providers, Emby
generated per-position frames, Emby online/offline chapter fallback, and the player nearest-frame
cache remain provider-scoped time-indexed exceptions rather than ArtworkPipeline consumers.
Authenticated requests use the nonpersistent side-asset transport, and their leaf caches are
memory-only and bounded by both byte cost and entry count. Sprite sheets and final scrub previews
cross a detached, eager ImageIO decode boundary before provider or MainActor cache publication; one
BIF backing payload is retained, safe offline files are mapped, and normal seek lookup copies only
the selected frame; the source-compatible frames accessor materializes all payloads only when
explicitly read. Largest-real-BIF and tile-sheet peak-RSS measurement is not a current release
gate; the caches remain bounded by the policies described above.
Their DecodedImage conversion is not shared-pipeline migration. Downloaded poster/chapter/BIF/subtitle
payloads are validated before promotion: DownloadSideAssetService.validate decodes and structurally
checks each payload kind ahead of the atomic staging write.
Video playback and theater surfaces¶
Three input lanes converge on one PlaybackController and its shared item observation,
transport state, diagnostics, seek UI, and chrome, while source negotiation, reopen,
progress, and server cleanup remain lane-specific:
- Plex streaming resolved by the controller's Plex path;
- an already-negotiated Jellyfin or Emby remote stream with reopen/progress/cleanup callbacks;
- a local downloaded file.
PlaybackController owns the AVPlayer; CustomPlayerView and the Cinema attachment own
their AVPlayerLayer presenters, with CustomPlayerChrome as the only shipping video chrome.
There is no selectable native AVPlayerViewController path. iOS/iPadOS and macOS platform
coordinators use the process-wide music/video system-media lease, while visionOS video uses a
controller-scoped MPNowPlayingSession and routes its commands directly back to the controller.
See Playback architecture for lifecycle and cleanup invariants.
The visionOS Custom Cinema immersive space is the user-visible app-owned Cinema path and reuses the same controller and chrome.
Downloads and offline ownership¶
The download pipeline has five layers:
- backend-specific
DownloadManagerextensions select a source and perform any Plex optimize, Jellyfin transcode/remux, or Emby Convert preparation; DownloadManagerowns queue policy, retries, storage limits, diagnostics, the observable Offline snapshot, and attempt-scoped work/cleanup coordination;DownloadKeepaliveCoordinatorprivately owns exact-attempt Jellyfin/Emby control-plane keepalive tasks and credential-generation quarantine;BackgroundDownloadSessionowns background URLSession work and durable static byte-range recovery, with every adoptable task stamped by its exact download attempt;DownloadStoreowns the locked relative-path JSON index and transactional artifact state in Application Support.DownloadArtifactLifecycleCoordinatororders filesystem work with index persistence, whileDownloadCleanupIntentJournalindependently keeps credential-free server cleanup durable across deletion and process death.
Device builds use a background URLSession that can relaunch the app. Simulator builds normally substitute a foreground session because the visionOS simulator background daemon is unreliable. Every downloads-capable platform uses the same static-range planner: known totals use the bounded closed-segment train, while unknown totals retain one open-ended request from the attempt-owned durable checkpoint.
Download startup installs every session callback and registers the dormant session with the background-completion registry before transport activation can submit work. For a healthy current store only, initial transport submission crosses one bounded MainActor turn; that task retains the manager until submission, and an explicit retry before the turn cancels the deferred edge and owns the sole immediate submission rather than overtaking it. Unsupported, unreadable, or malformed recovery does not enter that deferred edge: unsupported schemas retain their explicit reset path, while unreadable indexes and malformed current ownership remain fail-closed. This is critical-path scheduling of the existing app-owned transport, not removal of transport work or any persistence, recovery, or background-completion durability.
System integration and diagnostics¶
SystemEntryRouter bridges App Intents, Spotlight, Cinema exit, and SharePlay launches that
have already been resolved on the participant's device back into SwiftUI navigation.
Identifier-based system entries are backend/server scoped and refetch authoritative metadata
before navigation; SharePlay routing carries the participant-locally resolved item. Music
is intentionally excluded from the current system-video surface. Spotlight indexing is
best-effort and index-as-you-browse rather than a full-library crawl. The cross-device SharePlay
privacy and authenticated local-resolution boundary is canonical in
System integration.
Structured app diagnostic event logging is local, bounded, redacted, and opt-in. On non-tvOS
products, MetricKit is a separate passive crash/hang channel: it keeps at most five redacted
summaries, uploads nothing automatically, and includes them only in a user-generated feedback
report. Debug performance
signposts compile to no-op implementations in Release. RuntimeLifecycleCoordinator consumes the
existing aggregate-scene 500 ms handoff grace and emits typed active/inactive recovery reasons;
best-effort diagnostic flushing happens only on genuine aggregate inactivity.
Documentation rule¶
Published docs describe current behavior. Active implementation plans and acceptance journals live
in docs/plans/; unresolved investigations in docs/research/; immutable audit and profiling
observations in docs/evidence/; and completed or superseded context in docs/archive/. None of
those internal lanes belongs in the public navigation.