0.17.0#92
Conversation
Incorporate changes from New Streaming Player
play(stationId:) now clears existing spin players before starting fresh, preventing stale AVPlayers from being reused after audio interruptions. Removed misleading interruption state saves from headphone disconnect handler and dead isSuspended property. Added scheduleFetcher injection for testability and 8 new tests covering all interruption codepaths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-resume Fix audio interruption resume
Revert all source code to the stable 0.14.0 state, removing the streaming player port (0.15.0) which introduced issues. Add STREAMING_APPROACH.md documenting the AVPlayer-based streaming architecture for future reference.
Revert to 0.14.0 and document streaming approach
* Fix app hang by awaiting audio session before engine start The Sentry hang (2000ms+) was caused by AVAudioEngine.start() being called before the audio session was configured. configureAudioSession() fired a Task (fire-and-forget) then engine.start() ran immediately, causing the engine to stall on the main thread. Add ensureAudioSessionConfigured() async method to PlayolaMainMixer and await it in handleSuccessfulDownload before playback begins. Also remove the Thread.sleep retry loop from start() which was masking the unconfigured session issue while blocking the main thread. * Handle audio session config failure and add precondition asserts Replace try? with proper error handling in handleSuccessfulDownload so a failed audio session config bails out instead of proceeding to engine.start() with an unconfigured session. Add assertions in playNow and prepareAudioEngine to catch direct callers that skip ensureAudioSessionConfigured(). * Add .context to .gitignore
* Start audio engine off the main thread on interruption resume AVAudioEngine.start() makes a synchronous mach_msg to the audio server to bring up the hardware. When resuming after a phone-call interruption the hardware is cold and this call can exceed the 2s main-thread watchdog, producing Sentry "App Hanging" reports with AUIOClient_StartIO at the top of the stack. Make PlayolaMainMixer.start() async and dispatch the blocking engine start to a background queue via withCheckedThrowingContinuation, so the main thread is await-suspended rather than blocked. restartEngine() follows suit. resumeAfterInterruption now awaits the restart. AVAudioEngine is documented thread-safe for start/stop; the topology is only mutated at init/setup time on the main actor, so there is no race. @preconcurrency import AVFoundation silences the expected Sendable warning on the AVAudioEngine capture. * Keep restartEngine and start on MainActor Per review: only engine.start() needs to leave the main thread. Restore @mainactor on start() and restartEngine() so engine.stop() and engine.prepare() stay on the main actor — the continuation inside start() still dispatches the blocking engine.start() call to a background queue.
Normalization is now handled server-side, so the client no longer needs to calculate per-file gain or apply it via an EQ stage. Simplifies the audio graph from playerNode -> normalizationEQ -> trackMixer -> mainMixer to playerNode -> trackMixer -> mainMixer. - Delete AudioNormalizationCalculator and its tests - Delete the now-orphaned AudioBufferTestUtilities helper - Drop the zero-band AVAudioUnitEQ gain stage from SpinPlayer - Update hardResetTrackMixer to reconnect playerNode directly to the new mixer - Remove normalization mentions from README features/architecture and CLAUDE.md
Stopgap for an iOS 26 networking bug: URLSession's TLS 1.3 ClientHello includes the X25519MLKEM768 post-quantum hybrid key share (~1.6 KB), and some middleboxes (antivirus SSL inspection, parental-control routers, captive portals) drop the larger ClientHello, surfacing as NSURLErrorSecureConnectionFailed (-1200). Capping to TLS 1.2 keeps the ClientHello small enough to pass through. Routes the library's URLSessions through a new internal tls12Session (or makeTLS12Configuration() when a delegate is needed) so audio downloads from S3, schedule fetches, and listening-session reports all succeed on affected networks. Hardcoded rather than configurable on purpose — this is temporary and should be easy to revert when Apple ships a fix or a supported opt-out for the post-quantum hybrid key share.
Greptile P2: makeTLS12Configuration() only pinned the maximum TLS
version, leaving the minimum at its default (.TLSv10). ATS already
enforces a TLS 1.2 floor for HTTPS on iOS, but pinning the minimum
explicitly removes any ambiguity about the supported range and makes
the intent ("TLS 1.2, exclusively") obvious from the code.
* Cap all URLSessions to TLS 1.2
Stopgap for an iOS 26 networking bug: URLSession's TLS 1.3 ClientHello
includes the X25519MLKEM768 post-quantum hybrid key share (~1.6 KB), and
some middleboxes (antivirus SSL inspection, parental-control routers,
captive portals) drop the larger ClientHello, surfacing as
NSURLErrorSecureConnectionFailed (-1200). Capping to TLS 1.2 keeps the
ClientHello small enough to pass through.
Routes the library's URLSessions through a new internal tls12Session
(or makeTLS12Configuration() when a delegate is needed) so audio
downloads from S3, schedule fetches, and listening-session reports
all succeed on affected networks.
Hardcoded rather than configurable on purpose — this is temporary and
should be easy to revert when Apple ships a fix or a supported opt-out
for the post-quantum hybrid key share.
* Pin TLS minimum to 1.2 as well
Greptile P2: makeTLS12Configuration() only pinned the maximum TLS
version, leaving the minimum at its default (.TLSv10). ATS already
enforces a TLS 1.2 floor for HTTPS on iOS, but pinning the minimum
explicitly removes any ambiguity about the supported range and makes
the intent ("TLS 1.2, exclusively") obvious from the code.
Greptile SummaryThis PR bundles five fixes and a feature removal into the 0.17.0 release: a TLS 1.2 cap for iOS 26 middlebox failures, removal of client-side audio normalization, an off-main-thread audio engine start to prevent
Confidence Score: 5/5All changes are targeted bug fixes and a clean feature removal with no regressions identified. Every changed path was traced end-to-end: TLS configuration is applied consistently across all three network paths, the async engine-start dispatch is correctly implemented with No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant App
participant StationPlayer as PlayolaStationPlayer (@MainActor)
participant SpinPlayer as SpinPlayer (@MainActor)
participant Mixer as PlayolaMainMixer (@MainActor)
participant BG as DispatchQueue.global
participant Engine as AVAudioEngine
App->>StationPlayer: play(stationId:)
StationPlayer->>StationPlayer: fetchSchedule() via tls12Session
StationPlayer->>SpinPlayer: load(spin:)
SpinPlayer->>SpinPlayer: loadFile(localUrl:)
SpinPlayer->>Mixer: ensureAudioSessionConfigured()
Mixer-->>SpinPlayer: session configured
SpinPlayer->>Mixer: start() [async]
Mixer->>BG: DispatchQueue.global.async
BG->>Engine: engine.start()
Engine-->>BG: started
BG-->>Mixer: continuation.resume()
Mixer-->>SpinPlayer: engine running
SpinPlayer-->>StationPlayer: .success(localUrl)
StationPlayer->>SpinPlayer: playNow(from:)
SpinPlayer->>SpinPlayer: check isRunning (skip redundant start)
SpinPlayer->>Engine: playerNode.play()
Reviews (3): Last reviewed commit: "Merge main into develop to resync after ..." | Re-trigger Greptile |
- PlayolaStationPlayer.baseUrl default no longer includes /v1, so the
shared instance constructs schedule URLs as /v1/stations/{id}/schedule
instead of /v1/v1/stations/{id}/schedule when configure() is never
called. Aligns with configure()'s baseURL parameter default and the
/v1 prefix that createScheduleURL still appends.
- SpinPlayer.handleSuccessfulDownload now awaits playolaMainMixer.start()
after ensureAudioSessionConfigured(), so engine.start() runs off-main
on the cold first-play path (not just on interruption resume).
playNow and prepareAudioEngine now skip the sync engine.start() when
the engine is already running.
|
@greptile review this |
The 0.16.0 release (PR #88) was squash-merged into main, which broke git's view of shared history. Conflicts arose in PlayolaMainMixer.swift and SpinPlayer.swift because develop has since evolved those files (PR #89 made PlayolaMainMixer.start async, and the recent P2 fix added off-main engine start to handleSuccessfulDownload). Resolved by taking develop's version in both files — develop has everything main has plus the newer fixes. Also restored the P1 baseUrl fix in PlayolaStationPlayer.swift that the auto-merge silently reverted.
|
@greptile please review this |
Changes
Version Bump
Note: 0.16.1 was tagged on develop after #89 but was never merged to main. All of that work is included in this release.
After Merge
Tag main with
0.17.0:git checkout main && git pull git tag 0.17.0 git push origin 0.17.0