Skip to content

Remote HLS VOD misroutes after an inconclusive first probe: second-open HLS detection misses AE#154 reroute #246

Description

@qoli

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:

  1. The first probe.open fails inconclusively for a transient network/read reason, so probeOpened == false and probeFailure is not hlsPlaylistOnVODPath.
  2. AetherEngine logs proceeding without criteria and falls through to the native VOD loopback path with a nil preopened demuxer.
  3. HLSVideoEngine.start() reopens the same URL and this second open successfully sees the playlist body, producing AVIOReaderError.hlsPlaylistOnVODPath.
  4. HLSVideoEngine.start() wraps that typed error as HLSVideoEngineError.openFailed(reason: String).
  5. 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

  1. Use a remote HLS VOD origin where the first content probe can fail transiently but a subsequent open can return the playlist body.
  2. Call engine.load(url:options:) with default routing (nativeRemoteHLS == false, isLive == false) and the required HTTP headers.
  3. Allow the first probe.open to fail with a non-HLS transient error.
  4. Allow the reopen inside HLSVideoEngine.start() to read the playlist and produce AVIOReaderError.hlsPlaylistOnVODPath.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions