Repository navigation
feat(airplay): convert on the iPhone when the TV can't play a stream - #329
Merged
Merged
Conversation
…libraries hls-remux opens a source with libavformat, copies its video, and copies or converts one audio track to stereo AAC, letting AC-3, E-AC-3 and DTS decoders mix down with the stream's own levels. It cuts MPEG-TS segments at video keyframes, each starting with the tables a player needs. The C is built against the real headers, since libav's struct layouts change between FFmpeg versions. Linux builds it against the system FFmpeg for the tests. iOS will build it in the app against MPVKit's, whose FFmpeg has no HLS muxer. mvp-core's convert::hls writes the playlists, names the codecs, and paces a converter so a fast remux stays only a little ahead of playback.
When the receiver can't play a stream as it is, airplay_load_converted starts hls-remux on the source and serves its segments from the iPhone's Wi-Fi address through the shared LAN server, to that subnet only and under a random path. It returns once three segments are up, and AVPlayer plays the master playlist. A refused login fails before the TV hears anything. The converter is paced by the receiver's state, so a paused movie stops converting and memory stays to the sliding window. The plugin counts a converted stream's position from where it started and takes its length from the source. Xcode builds the C against MPVKit's FFmpeg. Linux builds it only for the tests, so CI's mutation jobs install FFmpeg's dev packages.
The destination converts when the route says to, seeks a converted movie by converting again from there, can't pause converted live, and ends the conversion when it stops. Before a conversion starts, the AirPlay button asks, saying the iPhone has to stay on Wi-Fi and charged but can lock. A load that fails takes the panel away and says why.
The playback destination contract says a capabilities change always comes with a new state. The converted flag was set after the opening state went out, so a subscriber could read the old capabilities.
Brings in the #323 review fixes and the fixed #306. The converter keeps its confirm step and sender profile on top of the reworked send, which now checks the item before the picker. A failure after local playback stopped words the backend's reason and says nothing is playing. The probe's audio tracks now carry a layout, which the converter leaves out.
6 of 8 tasks
The converted load names its URL by the formats list's generation too, and a seek's restart passes the same generation.
pick_audio takes whether the receiver plays 5.1 AAC. AirPlay's converter makes stereo only, so it passes false.
…hanges Findings 5, 6, 7, 10, 12, 13 and 15.1-15.3 of the #329 review, in the converter and its playlists: - A converter is made before it opens, so a cancel stops an open that hangs, and the start timeout hangs up on the source. - Live HTTP reconnects after the provider drops it. - Timestamps that jump back or forward are moved to carry on, and a packet whose DTS would go back is dropped. - A segment is cut at 8 s or 32 MB without a keyframe. The target duration is a fixed 10 s, and the window keeps at most 15 segments, 50 s and 200 MB. - The resampler is set up again when the audio's layout, rate or format changes, and the sample count follows the timestamps after a gap. - A resume seeks from the source's first timestamp, and back until the keyframe is at or before the start, and the position counts from that keyframe. - A movie's playlist starts at its first segment, and its pacer waits for the receiver to play. A live pacer never stops. - H.264 whose profile can't be read is named avc1.640028. The tests make H.264 and MP2 in MPEG-TS with the system's FFmpeg and libx264, through test-media/test_media.c, which the app never builds.
Brings dev to 9cd03bb. Cargo.lock is dev's with hls-remux added.
Findings 2, 3, 4, 8, 11, 14 (in part), 15.4, 15.5 and 15.9 of the #329 review: - airplay/conversion.rs runs the converted loads, apart from iOS so the tests reach it: one at a time, a later load or a stop makes an older one give up, a new one opens its source only after the old one has hung up, and a conversion is kept only once the TV was told to play it. A conversion that ends on its own, or a live one that ends at all, shows as the item's error. ios.rs only joins it to hls_out. - The frontend runs one converted seek at a time and only the last one asked meanwhile, and the player says when a seek on the TV fails. - The conversion ends when the TV connection is lost. - Only playing runs a movie's pacer, and the lock screen and remotes can't pause a converted live channel or seek a converted stream. - The convert question comes before the system picker, Escape answers no, and so does the button going away while it asks.
The privacy policy says the TV gets the stream's address with its login, or, while the iPhone converts, only an address on the iPhone. The Linux setup lists the FFmpeg dev packages crates/hls-remux builds its tests against. The convert question listens for Escape only while it asks.
5 of 8 tasks
pick_audio takes the codecs this device decodes, so the converter now reports, for each audio track, whether its FFmpeg has a decoder.
Cast's route refuses to transcode HDR, and AirPlay reaches that once the iPhone transcodes video. The player said it couldn't check the stream.
They write their media with test_media, which only Linux test builds have, so iOS Clippy's --all-targets build couldn't compile them.
cargo-mutants only skips a module gated by a plain #[cfg(test)], so the Linux gate is a second attribute.
…irplay-converter # Conflicts: # Cargo.lock
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Step 3 of AirPlay. When an AirPlay receiver can't play a stream as it is, the iPhone or iPad converts it and serves it as HLS on the Wi-Fi network, and the TV plays that. This covers most M3U live channels (raw MPEG-TS), MKV VOD, and MP2, AC-3, E-AC-3 or DTS audio.
This builds on cast's converter branch (
feat/cast-converter, with cast'sLanServer). It hasn't merged, so the diff also shows its changes. #323 and #320 are indev.crates/hls-remux(new): a small C file and its Rust wrapper.downmix=stereo).main.swiftin MPVKit 1.1.0-n9.0.2 turns off every muxer and enables only flac, dash, hevc, m4v, matroska, mov, mp4, mpegts and webm.project.ymlsource../../../../../crates/hls-remux/native, and a group plus a build file added toproject.pbxprojby hand, since there's no XcodeGen here.build.rsbuilds the C against the system FFmpeg, only for tests. The app takes the crate as a Linux dev-dependency, so the .deb, .rpm and AppImage never link FFmpeg.aac_at: MPVKit's build turns off every encoder except ffmpeg's ownaacand a few others.mvp_core::convert::hls(new): the media and master playlists, theCODECSnames (avc1.*from avcC or Annex B,mp4a.40.2,mp4a.40.34), and aPacer.src-tauri/src/hls_out.rs(new): one conversion.LanServer::start_for(Peers::Subnet), at the iPhone's Wi-Fi address (en0, fromgetifaddrsinwifi.rs). It answers only that subnet, and only under a random path.airplay://state.pick_audiowith AirPlay's codec list.airplay_load_converted,airplay_end_conversionandairplay_converter_support(convert on iOS, no hardware encoder yet). The plugin counts a converted stream's position from where it started, and takes its length from the source.AirPlayDestinationtakes a sender profile and loads converted routes. A converted live channel can't pause, and a converted stream keeps one audio track. The capabilities go out with the state that brings them, as the fixed feat(player): send the player's controls through a playback destination #306 asks.mutants.ymlandchanged-code.ymlinstall FFmpeg's dev packages forhls-remux's C, andinstall-linux-depsaddslibswresample-dev.hls-remuxhas a place in the weekly mutants matrix.changed-code.pyreads the workspace members, so the pull request shards pick the crate up by themselves. The iOS-onlyairplay/ios.rsandwifi.rs'sinterfacesare left out of cargo-mutants.crates/core/src/convert/mod.rs(pub mod hls;). The test helper incrates/core/src/convert/tests.rsgets the newlayoutfield, the same way cast's newer branch has it.Not in this step: a video transcode with
h264_videotoolbox(HEVC, MPEG-2 and interlaced still say they can't play), a WebVTT subtitle rendition, and choosing the audio language.How it was tested
On Linux, with no display and no device. iOS can't be built here, so CI's iOS build is the first compile of the C against MPVKit and of the iOS Rust.
hls_outtests that serve on loopback and fetch as a receiver would. They cover the master and media playlists, the segments, a held pacer, the sliding window, stopping, a refused login, video it can't copy, the subnet limit, and a source too short to cut. The app's tests also coverwifi, the commands and the argument table.Drop(frees memory, which no test can see) and two iOS-only app functions have skip comments.Needs the iPhone and the Samsung TV
On each one, check that the sound plays on the TV, not only AVPlayer's state.
Review fixes (reviews/329.md)
Fixed in this PR:
airplay/conversion.rsruns the converted loads. It's apart from iOS, so the tests and mutants reach it.superseded.reconnect_streamed,reconnect_on_network_errorandreconnect_on_http_error=5xx. A conversion that ends on its own with an error, or a live one that ends at all, shows as the item's error (refusedorfailed) through the plugin's newfail_converted.EXT-X-START:TIME-OFFSET=0,PRECISE=YES, and its pacer waits for the TV to play.avc1.640028(High 4.0).Convertedsits aboveload's docs.playingruns a movie's pacer.docs/development.mdlists the FFmpeg dev packages.Follow-ups, not in this PR:
lan_server.rs. Retrying afterECONNABORTEDorEMFILEand rebinding is cast's to change, and I've passed it on. Restarting a converted movie after the app was suspended comes after that.en0, such as a Personal Hotspot or Ethernet.How it was tested (review fixes)
On Linux, with no display and no device. CI's iOS job is the first compile of
remux.cagainst MPVKit, and ofairplay/ios.rs, the Swift and the iOS-only Rust.test-media/test_media.cwrites H.264 (libx264) and MP2 in MPEG-TS for them, with a chosen GOP, channel count and timestamp offset. The app never builds it. The tests cover:EXT-X-START, readiness by time, and a pacer that waits while paused.hls_outtests: an H.264 source served and read back, a resume at the keyframe before, a live clock that never pauses, a movie that waits for the TV, a start timeout that hangs up on the source, and the segment byte cap.airplay::conversiontests: keeping, stopping, a stop while starting, failed starts and plays, quick loads collapsing into the last, waiting for the old source to close, a source that never closes, and failures reported only for the conversion that plays.failandfail_converted.superseded, a lost TV ending the conversion, the confirm order, Escape and unmount, and VideoPlayer's seek failure.??or a finally, so the AirPlay hook and button catch on the promise.Needs the iPhone and the Samsung TV
On each, check that the sound plays on the TV, not only AVPlayer's state.
Checklist
devpnpm lint,pnpm testandcargo clippy --workspace --all-targets -- -D warningspassUnreleasedsection ofCHANGELOG.md