Trey t 85a19fdd71 Fix mid-stream audio loudness jumps
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>
2026-04-14 20:04:39 -05:00
2026-03-26 15:37:31 -05:00

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 ofapp on 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 single GamesViewModel injected into the environment
  • mlbTVOS/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 service
  • mlbTVOS/ViewModels/GameCenterViewModel.swift — per-game live data (at-bats, pitches, box score) used by the Game Center screen
  • mlbTVOS/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.

Description
No description provided
Readme 473 KiB
Languages
Swift 100%