The metadata probe grows the two things a host needs to call it on a library rather than on one title: bounds it sets itself, and a way to take it back. Alongside that, HDR10+ stops being a byte match and becomes a parse. Both halves come from @brandomoore in #583, with a follow-up round in #586.
Added
A probe can be given bounds and cancelled. ProbeLimits and ProbeCancellation are optional trailing parameters on every URL and custom-reader probe, plain or detail. They cover the whole operation rather than a pass of it: the monotonic deadline starts before the first read, and the input budget is shared across disc recognition, container open, avformat_find_stream_info, the seeks and both detail passes. Cancellation reaches in-flight HTTP reads and calls cancel() on a cooperating custom IOReader, including during open and seek, and FFmpeg gets an interrupt callback of its own.
A stop throws rather than publishing what it had: ProbeError for a limit, CancellationError for an explicit cancel, and never a partial or late positive. Cancelled HTTP requests finish their task callbacks before the probe releases their origin slots or returns, so a reader the caller means to reuse is never touched by a callback that outlived the call. Caller-owned readers stay caller-owned and are never closed.
The limits are cooperative, not hard network or memory caps, and the documentation says so at the point of use. Input counts the bytes the reader delivers, including rereads after a seek, not wire traffic; a packet is size checked after FFmpeg has already allocated it; a custom reader that does not implement cancellation cannot be unblocked, and the synchronous call waits for it. A call that passes neither parameter keeps its existing open policy byte for byte.
let cancellation = ProbeCancellation()
let worker = Task.detached {
try AetherEngine.probe(
url: mediaURL, detecting: [.hdr10Plus, .atmos],
limits: .init(), cancellation: cancellation)
}
let probe = try await withTaskCancellationHandler {
try await worker.value
} onCancel: {
cancellation.cancel()
}Bounding a probe does not make it answer from a shallower read: the analysis budget stays the ordinary playback one with probesize clamped to maxInputBytes, so probe(url:limits:) reports the same streams as probe(url:). The one detail a bounded probe cannot reach is the recordless Dolby Vision audit, because that opens the source a second time by URL, traffic this probe's budget and cancellation do not police. An untagged 10-bit HEVC source with no container record therefore comes back without one; probe that class without limits, or read the absence as unconfirmed.
Fixed
HDR10+ is confirmed by structure rather than by a byte marker. The previous scan looked for the six-byte T.35 signature anywhere in a packet, which compressed picture data can produce by chance. It now walks H.264 and HEVC NAL units and AV1 metadata OBUs, checks the registered ITU-T T.35 identifiers at the start of the SEI payload, and has FFmpeg parse the complete ST 2094-40 body, whose length has to come out exact. The bit accounting is the same one FFmpeg's own serializer computes, so the exact length is the length a conforming payload must have and not an added rule.
Playback and the probe share that one validator, so a badge raised mid-session and a badge raised before playback cannot disagree about the same file. Matroska's parsed AV_PKT_DATA_DYNAMIC_HDR10_PLUS side data remains the second carriage and is validated as a struct rather than trusted by type. Container-declared Dolby Vision stays primary.
A confirmation is never taken back by the budget that paid for it. Three places retracted an answer they already had. The validator discarded a fully parsed message whenever anything later in the same packet failed the structural walk, so a vendor SEI, a trailing byte or a second unreadable NAL next to the metadata was enough to lose a real badge, on the playback path as well as in the probe. The HDR10+ and Atmos detail passes dropped a detection when their soft wall-clock budget expired mid-pass, on the existing API with nothing to opt into.
That last one is the expensive shape: a caller cannot tell a withheld confirmation apart from a source that carries none, so on a slow origin an overrun read as "no Atmos". The budgets now bound what a pass spends and nothing else. The whole-probe deadline is unaffected, because that one throws and the caller knows.
A bounded HTTP probe waits for its origin slot. It took the slot without waiting and failed with sourceBusy the moment any other request held the origin, which is the ordinary shape when a host probes several items off one server. It now waits until its own deadline, that wait being the one the deadline watchdog cannot interrupt.
Full notes: CHANGELOG · 7.9.0...7.10.0