All notable changes to PlayolaPlayer are documented here. This project follows Semantic Versioning. Versions correspond to git tags, which Swift Package Manager consumers pin to. Pre-1.0, breaking changes bump the minor version.
This release makes the host app the sole owner of the AVAudioSession. The
SDK previously configured/activated the session and self-handled interruptions
and route changes; it no longer touches AVAudioSession at all. This lets the
SDK coexist cleanly with other audio subsystems in your app (URL streaming,
recording, VoIP) instead of fighting them for the process-global session.
-
The SDK no longer manages the
AVAudioSession.AudioSessionManageris gone,PlayolaMainMixerno longer exposesconfigureAudioSession()/ensureAudioSessionConfigured()/deactivateAudioSession(), andPlayolaStationPlayerno longer registersAVAudioSession.interruptionNotification/routeChangeNotificationobservers. ThehandleAudioSessionInterruption(_:)andhandleAudioRouteChange(_:)methods are removed.Source-breaking — required host changes:
-
Own the session. Configure and activate it before
play(stationId:):try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, policy: .longFormAudio, options: [])thensetActive(true). The SDK no longer does this; without it,play(stationId:)fails when the engine starts (surfaced as.error). -
Drive interruptions yourself. Observe
AVAudioSession.interruptionNotification(and route changes as needed) and call the newpauseForInterruption()/resumeAfterInterruption()(below). Reactivate the session before calling resume.
See the Audio session and migration sections of the README for copy-paste host setup and a full interruption-handling example.
-
-
Host-driven interruption transport.
PlayolaStationPlayer.pauseForInterruption()— silences playback, cancels scheduling/downloads, and is generation-fenced so in-flight work from before the pause can't publish state afterward. Preserves only the station identity (wall-clock radio has no frozen position). Idempotent across a double-pause.PlayolaStationPlayer.resumeAfterInterruption() async throws— restarts the engine and replays the interrupted station re-synced to the live wall clock. Disarms only on success, so a failed resume (e.g. the host hasn't reactivated the session yet) stays retryable.
-
PlayolaStationPlayer.Stategained a.paused(Spin)case. Published while paused for an interruption; theSpincarries display metadata (title, artist, artwork URL) for the track that was playing. Source-breaking for consumers thatswitchexhaustively overState: add acase .pausedarm. -
Engine self-recovery. The SDK now restarts and re-syncs its own engine graph on
AVAudioEngineConfigurationChange(scoped to the SDK's own engine, so a host running a separateAVAudioEngineis unaffected). This is engine ownership, not session ownership — it touches noAVAudioSessionAPI.
- Bounded retry/backoff on the initial schedule fetch.
play(stationId:)now retries a failed initialGET /v1/stations/{id}/scheduleup to 3 times with exponential backoff (0.5s / 1s / 2s) before giving up, matching the recovery behavior the player already used for spin loading and the ongoing schedule poll. Only transient failures are retried — server5xxresponses and an explicit allow-list of connectivityURLErrors (timeouts, host/DNS, connection-lost, etc.). Permanent failures (404, decode errors, empty schedules, and non-connectivityURLErrors such as.badURL) still fail fast. This means a transient backend outage no longer instantly fails a station start with no automatic recovery.
-
PlayolaStationPlayer.Stategained a terminal.error(StationPlayerError)case. A failedplay(...)now emits.loading(0)when the attempt begins and.error(_)when it fails terminally (instead of throwing without ever changingstate, which could leave consumers stuck on a loading spinner with no signal).play(...)stillthrowsas before — the new state is emitted in addition to the thrown error. Cancellation does not produce an.errorstate.Source-breaking for consumers that
switchexhaustively overState: add acase .error(ordefault) arm. A typical handler treats.erroras a recoverable, retry-able state (show the message, let the user tap play again).StationPlayerErroris nowSendable. -
State transitions are now supersession-safe (last
play()wins). Eachplay(...)/stop()takes a new internal generation; work from a prior attempt (its scheduling loop, or a spin whose audio starts later) can no longer publish state into a newer attempt. This closes a race where a lingering session could overwrite a freshly-emitted.error(or.idle) with.playing. Behavioral note: starting a newplay()supersedes the previous session immediately, so a failed station switch lands on.errorrather than rolling back to the previously-playing station. -
playNow(from:to:)andschedulePlay(at:)are nowasync. Bothpublicmethods changed from synchronous toasyncso their audio work can run off the main thread. Source-breaking for direct callers: addawaitat the call site (callers must already be in anasynccontext, e.g. aTask).
-
Network logging seam for observing the library's JSON API traffic (binary audio downloads are intentionally excluded). New public API in
PlayolaCore:PlayolaNetworkLogEvent— aSendablevalue describing a single request/response (method, url, request headers/body, status code, response body, duration, and any error description). Events are raw and unredacted; the consuming app is responsible for redacting sensitive values such asAuthorizationheaders before storing them.PlayolaNetworkLogger.handler— a thread-safe,Sendablehook the consuming app sets at startup to receive events.nil(the default) disables logging, leaving behavior and performance unchanged.PlayolaNetworkLoggingSession— a transparentURLSessionProtocolwrapper that times each call and emits an event on both success and thrown error without altering return values or rethrown errors.
The schedule fetch (
PlayolaStationPlayer) and the listening-session reporter (ListeningSessionReporter) now route their JSON API calls through this wrapper by default. No breaking API changes: existing initializers and call sites compile unchanged.