Root cause: the quality upgrade path called replaceCurrentItem mid-stream, which re-loaded the HLS master manifest and re-picked an audio rendition, producing a perceived loudness jump 10-30s into playback. .moviePlayback mode amplified this by re-initializing cinematic audio processing on each variant change. - Start streams directly at user's desiredResolution; remove scheduleQualityUpgrade, qualityUpgradeTask, and the 504p->best swap. - Switch AVAudioSession mode from .moviePlayback to .default in both MultiStreamView and SingleStreamPlayerView. - Pin the HLS audio rendition by selecting the default audible MediaSelectionGroup option on every new AVPlayerItem, preventing ABR from swapping channel layouts mid-stream. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mlbTVOS
Personal MLB streaming app for Apple TV and iPhone/iPad, built with SwiftUI. Streams live and archived MLB games, plus a few non-MLB channels (MLB Network, an authenticated private feed).
Targets
mlbTVOS— tvOS application (primary)mlbIOS— iOS/iPadOS application (shares most sources with the tvOS target)mlbTVOSTests— Swift Testing unit tests
The Xcode project is generated from project.yml via
XcodeGen. After editing
project.yml or adding/removing source files, run:
xcodegen generate
Building
Requires Xcode 26.3+ and tvOS/iOS 18.0 SDKs.
# tvOS
xcodebuild build -project mlbTVOS.xcodeproj -scheme mlbTVOS \
-destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)'
# Tests
xcodebuild test -project mlbTVOS.xcodeproj -scheme mlbTVOSTests \
-destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)'
Features
Dashboard
Today's live/scheduled/final games, standings ticker, and special channel entry points. Tapping a game opens the Game Center with at-bat timeline, pitch sequence, spray chart, and strike zone overlays.
Multi-stream view
Up to four simultaneous streams in an adaptive grid. Only one stream holds audio focus at a time; tap any tile to transfer focus. Streams can be single broadcasts, team-filtered feeds, or special channels.
Single-stream player
Full-screen AVPlayer with HLS adaptive streaming, custom HTTP headers, and automatic retry on failure.
Special playback channels
- MLB Network — 24/7 channel via the public MLB stream
- Werkout NSFW — authenticated private feed served by
ofappon the home server; uses a JWT cookie and rotates clips through a per-model random shuffle
Video shuffle (per-model random)
VideoShuffle groups clips by source folder (e.g. which model/creator
the video came from) and picks randomly one bucket at a time, excluding
the previously-played bucket. This guarantees no back-to-back same
source and produces a roughly uniform distribution across sources even
when one source dominates the raw data by a large factor. See
mlbTVOS/Services/VideoShuffle.swift and
mlbTVOSTests/VideoShuffleTests.swift for the pure functions and
their test coverage.
Architecture
mlbTVOS/mlbTVOSApp.swift— app entry point, creates a singleGamesViewModelinjected into the environmentmlbTVOS/ViewModels/GamesViewModel.swift— observable state store for games, standings, active streams, audio focus, and video shuffle bags; fetches from MLB Stats API and the auxiliary stream servicemlbTVOS/ViewModels/GameCenterViewModel.swift— per-game live data (at-bats, pitches, box score) used by the Game Center screenmlbTVOS/Services/— API clients (MLBStatsAPI,MLBServerAPI) and pure helpers (VideoShuffle)mlbTVOS/Views/— SwiftUI screens and component library (Components/AtBatTimelineView,PitchSequenceView,SprayChartView,StrikeZoneView, etc.)
All new Swift code uses Swift 6 strict concurrency
(SWIFT_STRICT_CONCURRENCY: complete).
Configuration
No secret management is set up. The authenticated feed cookie is
hardcoded in mlbTVOS/Views/DashboardView.swift
(SpecialPlaybackChannelConfig.werkoutNSFWCookie) and must be refreshed
manually when the JWT expires — generate a new one from the ofapp
server's jwt_secret using the same userId and a longer exp.