Skip to content

AirPlay carries audio but no picture on the software path, and the engine announces the opposite #550

Description

@superuser404notfound

Field report (Discord): an MKV played to an AirPlay receiver gave sound and no picture. Log from AetherPlayer 0.18.1 (69), iPhone 17 Pro, iOS 27.0, engine 7.5.0.

The session that produced it:

[VTProbe] canHardwareDecode codec=27 1280x720 -> true (status=0)
[AetherEngine] source is forward-only, forcing software path
[AetherEngine] dispatch: codec=27 -> software
[AetherEngine] #315: an external screen holds the picture, so no local first frame is coming;
               latching hasFirstFrameReadyForDisplay at readiness
[AetherEngine] #321: effective video route = software

Audio then played to the receiver for 16 s (clock advancing, enq and disp tracking) with nothing on either screen.

Mechanism

The AirPlay video path (#86, #227) is an AVPlayer feature end to end: handleExternalPlaybackChange returns early unless playbackBackend == .native, and the receiver is served the engine's own loopback HLS over the LAN IP. The software path has no AVPlayer. Its audio reaches the receiver because AVSampleBufferAudioRenderer follows the audio session route, and its picture goes into a local AVSampleBufferDisplayLayer, which AirPlay does not carry (only screen mirroring would). So audio-and-no-video is the guaranteed outcome of a software-path session on a wireless receiver, not a failure mode.

Two things are worth separating here.

The engine states the opposite of what is true. externalPlaybackHoldsThePicture is read on a session where nothing holds the picture, and #315 latches hasFirstFrameReadyForDisplay on readiness to spare the host a cover that would never lift. On the native path that is exactly right. On the software path it promises a picture that does not exist anywhere, so a host cannot tell the viewer what is going on, and the log reads as if everything is fine. Whatever is decided about routing, this asymmetry should go: the software path should either not take the #315 branch, or the fact that this route carries no picture should be published so a host can say so and point at mirroring.

The routing premise that put this session on the software path does not hold here. The codec was hardware decodable (canHardwareDecode ... -> true); what forced software was the origin answering a suffix range with status=200 and never resolving a size, so the source degraded to forward-only (AetherEngine.swift, the !probe.isSourceSeekable && !options.isLive branch). The exemption next to it already describes this case: options.sequentialOrigin keeps a linear source on the native path because the producer reads from byte 0, the duration is declared, and the segment plan falls back to uniform stride. Here the container declared its duration ([SWHost] session start: ... duration=183.1s), so the #126 premise (unknown length, zero segments) is not what happened. Whether a duration-carrying forward-only URL source should get the same treatment as a declared sequential origin is its own question, and it is the difference between AirPlay working and not working for this class of source.

Scope

Consumers that play arbitrary URLs (AetherPlayer). A Jellyfin library source answers ranges, stays native, and AirPlays normally, so Sodalite is not affected in practice.

The interruption wedge visible in the same log is filed separately.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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