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.
Airing.startTime/Airing.endTime— the show's real on-air window. Every spin returned byGET /v1/stations/{stationId}/schedulenow carries these two fields on its nestedairingobject.startTimeis the real moment the show goes on air (MIN(spin.airtime)across the airing's spins, accounting for the "lead gap" where preceding content pushes the show later than its slot);endTimeis the real moment it goes off air (MAX(spin.endOfMessageTime), floored atairtime + episode.durationMS). Both are optional ISO-8601 UTC dates — present only on airings from the schedule feed andnilelsewhere — so the change is additive and source-compatible.airtime(the nominal slot) is unchanged. Downstream apps can drive a real-time "next show in ~N min" countdown offendTime. Also available on the 0.19.x line as 0.19.1.
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.