6.26.0 - The error string is a payload, not a key
Drop-in from 6.25.4. One additive API, no change to how anything plays. It comes out of #374, where a shipping dual-engine host set out to build an analytics classifier over state = .error(...) and, incidentally, out of reading our own documentation back against the code.
Half of that sentence was never ours
The docs said the message inside .error is the engine's own sentence, worth logging verbatim. That is true for the messages that name a cause, and false for the ones most failing sessions actually produce. On the native paths the published string is AVPlayerItem.error.localizedDescription, forwarded with no prefix and no wrapper, so it arrives in whatever language the device is set to, and the NSError domain and code that would classify it are gone by the time a host sees it.
Both halves arrive through the same publisher. Nothing on the surface separates them except the text, which is precisely the thing that cannot be a key. A host bucketing failures by English substring therefore files every non-English device under "unknown", and reads that back as "cause unknown" when the truth is "cause untranslated".
What is published
player.$state
.sink { state in
guard case .error = state, let info = player.errorInfo else { return }
analytics.record(failure: info.kind.rawValue, // stable token, carries no origin
domain: info.underlyingDomain, // nil where the engine authored it
code: info.underlyingCode,
route: player.videoRoute.rawValue)
log(info.message) // for a human, not for a bucket
}PlaybackErrorKind is a string-backed struct rather than an enum, deliberately: the set grows whenever the engine learns a new way to fail, and a host switching exhaustively over an enum would stop compiling on a minor release. The raw values are API and do not change. Fifteen kinds ship, from .sourceOpenFailed and .liveSourceUnavailable through .nativeItemFailed, .noPlayableTrackWithinBudget and .masterPlaylistRejected to .reloadFailed and .audioTrackSwitchFailed.
A non-nil underlyingDomain is also the marker for "this message has been through a translator", which is the other question a host could not answer before.
There is a second consumer of this shape, and it is not analytics: a telemetry contract that forbids raw player error strings leaving the device, because those strings can name a stream host. The engine never interpolates a URL or a host into .error, so the engine-authored half is origin-free by construction, and a numeric code is origin-free whatever produced it. Both are shippable where the message is not.
One funnel, and a test that keeps it
errorInfo is assigned before state, so a $state sink reads this failure's own info rather than the previous failure's, and it is cleared by the state's own move away from .error, so the two cannot drift apart. Every failure site now publishes through a single funnel, and a test fails the build if a new state = .error(...) appears anywhere outside it. An unclassifiable error is the shape this release exists to remove, so it should not be reachable by writing ordinary-looking code.
Also in this release
docs/api.md gained the host-side answer to a live retune against a rotating per-session token. The #168 carriage verdict is remembered per exact absolute URL, which a rotated token misses by construction, so the retune re-pays the native mount plus up to 4 s of watchdog grace every lap. The key the memory cannot have is one an IPTV host does have: the channel. $videoRoute publishes the reroute as it happens, so a host can record the verdict against its own channel id and open the next session on HLSLiveIngestReader directly.
Not claimed
The kinds classify what the engine knows about a failure, not what AVFoundation knows: underlyingCode is passed through untouched and this release maps no CoreMedia or AVFoundation codes to meanings of its own. Kinds may be added in later minor releases, which is why they are not an enum.
Thanks to @ksktech-dev and @kskchaitanya1993 on #374, whose classifier is the reason this gap was measured rather than assumed.