Skip to content

feat(software): a live join starts near the newest media once the connect burst has landed - #797

Merged
superuser404notfound merged 2 commits into
superuser404notfound:mainfrom
jpsts:feat/software-live-join-placement
Oct 16, 2026
Merged

superuser404notfound merged 2 commits into
superuser404notfound:mainfrom
jpsts:feat/software-live-join-placement

Conversation

@jpsts

@jpsts jpsts commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A continuous live stream over HTTP (an IPTV panel's MPEG-TS form of a channel) opens with a burst of what the origin holds of the channel, at link speed, and real time after that. SoftwarePlaybackHost's live feeder arms its clock on the first packet of that burst, and nothing on the software path ever catches up, so the picture runs the whole burst behind the broadcast for as long as the channel plays. Measured on one panel: 17 s behind, on a channel whose native-path session (AVPlayer placed a holdback behind the newest segment) ran 6 s behind. This places a software-path live join near the newest media once the burst has landed.

What changed

  • SoftwareLiveJoinPolicy (new, pure): fed the ring's first and newest PTS from the feeder thread, it says when the burst has landed and where to start. The burst is landed when the ring has grown no faster than 1.5× real time over a 0.25 s window, judged from 0.3 s after the first packet, or after 1.5 s at most on a link still catching up. Then: a ring holding at least leadSeconds + minimumSkipSeconds (4 + 2 s) ahead of its first packet starts leadSeconds (4 s) behind the newest media; anything shallower, and a source that delivers in real time, starts from the first packet as before. 4 s is AudioLookaheadPolicy.targetLeadSeconds, and a panel that hands over about a second of media at a time leaves the lead swinging within it.
  • runLiveFeederLoop asks the policy before it feeds anything (two new closures, liveJoinPlacement and placeCursor, defaulting to the old behaviour), waits 50 ms between asks, and on .place moves the feed cursor to the keyframe at or before the target (PacketRingBuffer.seq(forKeyframeAtOrBefore:)), so the clock arms there on the first decoded audio as it always did. A seek that lands before the decision retires it; a source that ends first plays what there is. The media behind the start stays in the rewind ring, and sessionStartPts is unchanged, so the session's axis and behindLiveSeconds read as before.
  • Only sessions with a DVR ring take the live feeder, so only those are placed; a live session without one keeps the demux loop's first-packet start.
  • docs/formats.md's paragraph on raw live MPEG-TS described it as always routed software, which has not been so since live sources were exempted from the forward-only rule; it now says how it is routed and describes the placement. DocumentedConstantsTests pins the numbers the paragraph quotes.

Evidence

The origin: an IPTV panel's live MPEG-TS channel, H.264 (2 s GOPs) + AAC, 6 to 12 Mbit/s, read over a wired connection. Probed with a plain socket read, the first half second brought 8.5 MB and the stream then ran at the media rate in chunks about a second apart; the video PTS span over the first 14 s of wall clock was 24 s, so about 10 s of media arrived ahead of real time on that connect (a later connect brought 17 s, the burst varies with the content's bitrate).

aetherctl play --live --fast-zap --dvr-window 600 --max-concurrent-requests 1 --sw, before, 40 s (behind is the engine's behindLiveSeconds, the distance from the newest demuxed media):

t=01 cur=0.85  edge=18.16 behind=17.31
t=10 cur=9.85  edge=27.12 behind=17.27
t=20 cur=19.86 edge=36.96 behind=17.10
t=30 cur=29.86 edge=47.12 behind=17.26
t=40 cur=40.11 edge=57.60 behind=17.49

First picture 0.45 s after the open, and 17 s behind for the whole run.

The same command with this change, 100 s:

[SWHost] live join placed 4.0s behind the newest media: skipped 8.0s of the connect burst (cursor seq=0 -> 763, keyframe pts=7258.06)
t=01 cur=8.22   edge=13.98  behind=5.76
t=10 cur=17.22  edge=23.12  behind=5.90
t=30 cur=37.22  edge=41.98  behind=4.76
t=50 cur=57.46  edge=62.64  behind=5.18
t=70 cur=77.46  edge=83.12  behind=5.66
t=100 cur=107.72 edge=113.12 behind=5.40

First picture 1.06 s after the open (the settle: the burst had calmed by then), 4.6 to 6 s behind for the whole run, no underrun or rebuffer (aLead held at 4.0 s in the [SWDiag] line throughout). The same channel's native-path session in the same session of measurements ran 5.3 to 7.3 s behind (TARGETDURATION sealed at 3 s, holdback 9 s), so the two routes now agree to within a GOP.

Test plan

  • Device / OS: a Mac, macOS 26, swift test, and aetherctl play on the same Mac.
  • Source media: an IPTV panel's live MPEG-TS channel, H.264 (2 s GOPs) + AAC, SDR, 6 to 12 Mbit/s, as above.
  • Result:
    • SoftwareLiveJoinPlacementTests, new: a 12 s burst over the first half second is landed and placed 4 s behind the newest media between 0.3 and 1 s in; a real-time source starts from its first packet by 0.7 s; a burst still landing at 1.5 s is placed on what arrived; a 5 s burst and backwards timestamps start from the first packet.
    • DocumentedConstantsTests (new pin), PublicAPIDocumentationTests, the software-host suites, and the full suite (4,432 Swift Testing tests and the XCTests) pass. Scripts/check-doc-links.py passes.
    • The measurements above.

Note: the CHANGELOG.md bullet sits under [Unreleased] › Changed; #796 adds one under Fixed at the same spot, so whichever merges second may need a trivial rebase there.

Checklist

  • CHANGELOG.md updated
  • Commit messages follow Conventional Commits (feat(...), fix(...), chore(...))
  • The fix lives in the engine, not in a host-side workaround
  • Public API changes are intentional and documented (none; SoftwareLiveJoinPolicy is internal)

jpsts added 2 commits October 16, 2026 01:16
…nect burst has landed

A continuous live stream over HTTP opens with a burst of what the origin
holds of the channel (8 to 18 s on one IPTV panel, at link speed), and
the software host armed its clock on that burst's first packet. Nothing
on the software path catches up, so the picture ran the whole burst
behind the broadcast for as long as the channel played: measured 17 s
behind on a channel whose native-path session ran 6 s behind.

The live feeder now waits for the burst to land (the ring growing no
faster than real time over a quarter second, from 0.3 s on, or 1.5 s at
most) and starts 4 s behind the newest media, on a keyframe. A source
that delivers in real time, or a ring too shallow to be worth a later
keyframe, still starts from its first packet. The media behind the start
stays in the rewind ring. Measured on the same channel: first picture
1.1 s after the open, 4 to 6 s behind the newest media.

SoftwareLiveJoinPolicy, SoftwareLiveJoinPlacementTests.
@superuser404notfound
superuser404notfound merged commit 54f1443 into superuser404notfound:main Oct 16, 2026
3 of 4 checks passed
@superuser404notfound

Copy link
Copy Markdown
Owner

Merged as 54f14436, thanks for this one. The measurement against the native path's holdback made the 4 s lead easy to check, and keeping the burst in the rewind ring with sessionStartPts untouched means nothing downstream of the session axis moves.

Two things went in after the merge:

  • bd5d5fa4 (merged 280f3854): the placement now writes the feed cursor compare-and-set on the seek generation the feeder iteration read. Before, a seek landing between the policy's decision and placeCursor had its target overwritten by the join. The log line only fires when the placement actually took.
  • The red CI run on this branch was not this change: Issue782RestartReanchorTests missed its 4 s detour-fetch budget on a loaded runner and fell through to the rescue reconnect at the landing. Fixed in d3b40beb together with two other suites that asserted on a wall clock or on purgeable memory.

One observation for later, not a request: every software live join with a DVR ring now waits at least minimumSettleSeconds before the first feed, including a tuner that delivers in real time from the first packet (interlaced DVB lands on the software path). 0.3 s seems a fair price for the burst case; noting it in case a zap-time report ever points there.

@superuser404notfound

Copy link
Copy Markdown
Owner

Released in 7.38.0. Thanks again for this.

jpsts pushed a commit to jpsts/AetherEngine that referenced this pull request Oct 16, 2026
… first

The feeder decided the placement and then wrote the cursor unconditionally,
so a seek between the two had its target overwritten by the join. The write
is now compare-and-set on the seek generation the feeder iteration read, and
the placement is logged only when it took. Follow-up to superuser404notfound#797.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants