What happened?
A remote HLS VOD source loaded through the default AetherEngine.load(url:options:) path failed after about 23 seconds with:
HLSVideoEngine: open failed (HLS playlist supplied to the VOD loopback path)
The source is playable. A manual retry of the same episode and same CDN media path (with a refreshed signed URL) routed to native AVPlayer, reached readyToPlay, entered status=playing, and showed continuous media-time progress.
This is an AetherEngine route-admission gap, not a host/adapter routing responsibility. AE#154 already promises automatic rerouting of remote HLS VOD onto the nativeRemoteHLS bypass when AetherEngine detects AVIOReaderError.hlsPlaylistOnVODPath.
The gap occurs when:
- The first
probe.open fails inconclusively for a transient network/read reason, so probeOpened == false and probeFailure is not hlsPlaylistOnVODPath.
- AetherEngine logs
proceeding without criteria and falls through to the native VOD loopback path with a nil preopened demuxer.
HLSVideoEngine.start() reopens the same URL and this second open successfully sees the playlist body, producing AVIOReaderError.hlsPlaylistOnVODPath.
HLSVideoEngine.start() wraps that typed error as HLSVideoEngineError.openFailed(reason: String).
- The load terminates instead of returning to the existing AE#154 remote-HLS reroute.
The HLS classification is based on the playlist body detected by AetherEngine, not on a .m3u8 suffix. A suffix should remain only a hint and should not be required to fix this.
Expected: once either open attempt confirms that the same remote VOD source is HLS, AetherEngine should perform the existing bounded AE#154 reroute to the native remote-HLS path while preserving the URL, headers, source identity, and start position.
Actual: HLS confirmed on the second open is wrapped into a string error and surfaced as terminal playback failure.
Steps to reproduce
- Use a remote HLS VOD origin where the first content probe can fail transiently but a subsequent open can return the playlist body.
- Call
engine.load(url:options:) with default routing (nativeRemoteHLS == false, isLive == false) and the required HTTP headers.
- Allow the first
probe.open to fail with a non-HLS transient error.
- Allow the reopen inside
HLSVideoEngine.start() to read the playlist and produce AVIOReaderError.hlsPlaylistOnVODPath.
- Observe that load fails with the wrapped
HLSVideoEngineError.openFailed instead of invoking the AE#154 reroute.
A deterministic test can inject the two open outcomes in sequence:
- open 1: inconclusive/transient failure;
- open 2:
hlsPlaylistOnVODPath.
AetherEngine version or commit SHA
5.20.2 / e2dcbb9711439867eca13ba30fc45f70d7631162
Host app
Custom / my own integration
Platform
tvOS
OS version
Not captured in the incident log
Device / chip
Physical Apple TV; exact model/chip not captured in the incident log
Playback path
Native AVPlayer
Source media (for playback bugs)
Remote HLS VOD. The origin returns an HLS playlist and uses expiring signed URLs plus request headers. The private signed URL is omitted; the engine itself confirmed the response body as HLS by throwing hlsPlaylistOnVODPath during the second open.
Error codes / log lines
[AetherEngine] probe failed (...); proceeding without criteria
[AetherEngine] probe failed; falling through to the native video path (HLSVideoEngine will reopen and discover the stream) rather than degrading to audio-only
HLSVideoEngine: open failed (HLS playlist supplied to the VOD loopback path)
request.end phase=route result=rejected elapsed=23.471s
Anything else
Relevant current flow:
AetherEngine.load checks RemoteHLSMediaSelection.shouldReroute(probeFailure:...) only after the first probe.
- On an inconclusive first probe it intentionally calls
loadNative(... preopenedDemuxer: nil ...) so HLSVideoEngine reopens the source.
HLSVideoEngine.start() catches every reopen error and converts it to openFailed(reason: "\(error)"), losing the typed hlsPlaylistOnVODPath cause before the load orchestrator can apply AE#154.
Suggested acceptance criteria:
- Preserve or classify the second-open typed cause so confirmed HLS reaches the existing AE#154 reroute.
- Reroute at most once for the same URL/request; do not ping-pong back to loopback if native remote HLS fails.
- Preserve
httpHeaders, source identity, and startPosition.
- Do not use URL suffix as sufficient HLS evidence.
- If native remote HLS also fails, surface a terminal error that includes the initial probe outcome, reroute attempt
1/1, and final cause.
- Cover the two-open regression sequence and a terminal native-route failure in tests.
No SyncnextHybrid workaround is requested: route selection for this default AetherEngine.load flow belongs to AetherEngine and is already part of the AE#154 behavior.
What happened?
A remote HLS VOD source loaded through the default
AetherEngine.load(url:options:)path failed after about 23 seconds with:The source is playable. A manual retry of the same episode and same CDN media path (with a refreshed signed URL) routed to native AVPlayer, reached
readyToPlay, enteredstatus=playing, and showed continuous media-time progress.This is an AetherEngine route-admission gap, not a host/adapter routing responsibility. AE#154 already promises automatic rerouting of remote HLS VOD onto the
nativeRemoteHLSbypass when AetherEngine detectsAVIOReaderError.hlsPlaylistOnVODPath.The gap occurs when:
probe.openfails inconclusively for a transient network/read reason, soprobeOpened == falseandprobeFailureis nothlsPlaylistOnVODPath.proceeding without criteriaand falls through to the native VOD loopback path with a nil preopened demuxer.HLSVideoEngine.start()reopens the same URL and this second open successfully sees the playlist body, producingAVIOReaderError.hlsPlaylistOnVODPath.HLSVideoEngine.start()wraps that typed error asHLSVideoEngineError.openFailed(reason: String).The HLS classification is based on the playlist body detected by AetherEngine, not on a
.m3u8suffix. A suffix should remain only a hint and should not be required to fix this.Expected: once either open attempt confirms that the same remote VOD source is HLS, AetherEngine should perform the existing bounded AE#154 reroute to the native remote-HLS path while preserving the URL, headers, source identity, and start position.
Actual: HLS confirmed on the second open is wrapped into a string error and surfaced as terminal playback failure.
Steps to reproduce
engine.load(url:options:)with default routing (nativeRemoteHLS == false,isLive == false) and the required HTTP headers.probe.opento fail with a non-HLS transient error.HLSVideoEngine.start()to read the playlist and produceAVIOReaderError.hlsPlaylistOnVODPath.HLSVideoEngineError.openFailedinstead of invoking the AE#154 reroute.A deterministic test can inject the two open outcomes in sequence:
hlsPlaylistOnVODPath.AetherEngine version or commit SHA
5.20.2 /
e2dcbb9711439867eca13ba30fc45f70d7631162Host app
Custom / my own integration
Platform
tvOS
OS version
Not captured in the incident log
Device / chip
Physical Apple TV; exact model/chip not captured in the incident log
Playback path
Native AVPlayer
Source media (for playback bugs)
Remote HLS VOD. The origin returns an HLS playlist and uses expiring signed URLs plus request headers. The private signed URL is omitted; the engine itself confirmed the response body as HLS by throwing
hlsPlaylistOnVODPathduring the second open.Error codes / log lines
Anything else
Relevant current flow:
AetherEngine.loadchecksRemoteHLSMediaSelection.shouldReroute(probeFailure:...)only after the first probe.loadNative(... preopenedDemuxer: nil ...)soHLSVideoEnginereopens the source.HLSVideoEngine.start()catches every reopen error and converts it toopenFailed(reason: "\(error)"), losing the typedhlsPlaylistOnVODPathcause before the load orchestrator can apply AE#154.Suggested acceptance criteria:
httpHeaders, source identity, andstartPosition.1/1, and final cause.No SyncnextHybrid workaround is requested: route selection for this default
AetherEngine.loadflow belongs to AetherEngine and is already part of the AE#154 behavior.