Skip to content

6.26.0 - The error string is a payload, not a key

Choose a tag to compare

@superuser404notfound superuser404notfound released this 15 Aug 09:33
· 671 commits to main since this release

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.