4.0.0 — End-of-media surfaced as PlaybackState.ended (#63)
End-of-media is now a first-class state
A host that wants end-of-playback behavior (mark-watched, autoplay-next, return to the previous screen) can finally tell "finished" from "idle". Previously the engine tracked didReachEnd per host but consumed it internally and collapsed the public surface to .idle, indistinguishable from pre-load or stop(). On the native path a host could work around it by observing the handed-out AVPlayer for AVPlayerItemDidPlayToEndTime. On the software-decode path there is no public AVPlayer, so there was no recourse at all.
PlaybackState now has a dedicated terminal case, .ended, set on end-of-media across every backend (native, software, audio). stop() still goes to .idle, so the two are distinct: .ended means the source played to completion, .idle means pre-load or stopped. .ended is terminal, so seek and togglePlayPause are no-ops, and the next load(...) clears it.
player.$state.sink { state in
switch state {
case .ended: markWatched(); playNextEpisode()
case .idle: break // pre-load / stopped
default: break
}
}Breaking change (the reason for the major bump)
Adding a case to the non-frozen public PlaybackState enum is source-breaking: an exhaustive switch without an @unknown default will not compile until it handles case .ended. This ships as 4.0.0 specifically so from:-pinned adopters opt into it deliberately instead of being broken on a routine swift package update.
Migration: add case .ended wherever you previously treated .idle as end-of-media (run your end-of-playback handling there), and keep .idle for pre-load and stopped.
Thanks
Thanks to @rrgomes for the clear write-up in #63, including the software-path gap that made the AVPlayer workaround a non-starter.