Skip to content

Audio tap does not survive a session-preserving reload: a track switch ends it, nothing re-arms it #356

Description

@superuser404notfound

Corrected after measuring (the first version of this body claimed the stream dangles open; it does not). An internal reload runs stopInternal, which tears the tap controller down and finishes the stream. A host's for await therefore ends, so the loss is observable. What remains is smaller than first written, and still worth closing.

What happens

installAudioTap() binds the software delivery path to the SoftwarePlaybackHost live at install time. An audio-track switch is a session-preserving reload: it goes through stopInternal(resetDisplayCriteria: false, keepNativeHost:, keepCustomReader: true), which runs audioTapController?.teardown(); audioTapController = nil, and then builds a new host with no sink.

So a transport-level call (selectAudioTrack) ends an opt-in feature the host installed at load, and nothing re-establishes it. Same for a subtitle-track switch, a title switch and reloadAtCurrentPosition, which take the same path.

The documented contract is that load() and stop() finish the stream, opt-in being per load. A host switching an audio track called neither.

The #311 frame-time observer crosses the identical seam and is deliberately carried across it (AetherEngine+Loading.swift, host.setVideoFrameTimeObserver(softwareVideoFrameTimeObserver), "a load builds a new host and a new renderer, so an observer installed once by the host app has to be carried across the seam"). The tap has no counterpart, and the asymmetry is not intentional as far as the history shows.

Measured

40 s MKV, three audio tracks, software path, tap installed at load, selectAudioTrack at +20 ms:

  AUDIOTAP installed (deliverySource=true)
  HOSTCALL selectAudioTrack(index: 3) at +20 ms (was 1)
  AUDIOTAP stream finished (buffers=1)

Re-installing after the switch resumes delivery immediately (abufs 1 → 177), so nothing about the new session is hostile to the tap; it is only that nobody re-arms it.

aetherctl play --sw --seconds 8 --switch-audio 3@20 --audio-stats <multi-audio file>

Suggested shape

Carry the tap across the reload seam the way the frame-time observer is carried: hold the controller through a session-preserving stopInternal and re-arm the sink where the new host is built. A host that opted in at load then keeps its tap across a track switch, which is what the per-load contract already promises.

Failing that, document that a track switch ends the tap, so hosts re-install on stream end rather than discovering it.

Area

Software (VideoToolbox) playback path

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