Skip to content

AetherEngine 5.23.5

Choose a tag to compare

@superuser404notfound superuser404notfound released this 26 Jul 12:07

Patch release. One fix: an E-AC-3 source whose first segment holds no audio packet now plays, with its passthrough intact.

Fixed

An E-AC-3 source whose first segment carries no audio packet plays instead of wedging the muxer

A 4K HEVC MKV with E-AC-3 5.1 never produced a frame. seg-0.m4s failed to cut, the pump ended, the VOD revive rebuilt the identical configuration twice more, and the session was abandoned as "not muxable", so a host fell back to a server transcode of a file it should have played untouched:

[mp4] [error] Cannot write moov atom before EAC3 packets parsed.
[HLSSegmentProducer] seg-0.m4s cut FAILED; muxer is wedged, ending pump
[HLSSegmentProducer] pump finished: reason=muxerFailed packetsRead=98 packetsWritten=96
... x3 ...
[HLSVideoEngine] #99 VOD muxerFailed revive cap reached; giving up (source not muxable in this session)

The trigger is file layout, not codec support. movenc builds the ec-3 sample entry's dec3 box in handle_eac3, that is only from a PARSED bitstream frame, never from codecpar and never from extradata (a valid dec3 supplied in extradata still returns -22). Under +delay_moov the moov is written at the first fragment flush, so a source whose audio blocks sit behind seconds of video in file order (common in WEB-DL remuxes) reaches its first segment boundary with zero audio packets muxed, and that flush cannot succeed.

flushPendingFragment already refused such a flush. cutFragmentForNextSegment did not, and its only "not now" signal was a nil return, which the producer correctly reads as a fatal wedge, which is why applying the existing guard verbatim to the cut wedges by a shorter route.

A cut now has a third outcome, distinct from both success and failure: nothing is written and the muxer stays intact. The pump treats it as recoverable, scans forward for one real audio frame (bounded at 128 MiB or 30 s, discarding everything else), and exits with that frame. The session keeps it, and every muxer built from then on, including the ones a seek or an audio switch rebuilds, muxes it at init. That writes moov with a genuine dec3, after which the primed fragment's bytes are discarded, so the delivered segment is exactly what the plan called for: no out-of-place audio sample, no disjoint track ranges, no shifted boundaries. The E-AC-3 stream-copy is preserved, Atmos included.

Bridging would not have been a fix. The E-AC-3 bridge wedges identically, because its encoder output is also E-AC-3, and routing every affected source to the FLAC bridge would trade a black screen for a silent Atmos downgrade. A source whose audio never arrives inside the scan bounds still falls through to the existing recovery.

One detail worth recording for anyone touching the muxer's packet order: movenc consumes the +frag_discont flag on the first packet written, and afterwards rewrites each track's next fragment-opening sample to start_dts + track_duration, with tfdt being cluster[0].dts - start_dts. A prime packet consumes that single opportunity, so without re-arming the flag the first real audio fragment lands at tfdt 0 no matter where its samples belong. Measured on the repro fixture, a 12 s audio start rendered 12 s early. Re-armed, fragment timestamps match an unprimed session exactly.

A teardown on a muxer that never received an audio packet stops logging failed moov writes

finalize() gained the same precondition as the cut, so a muxer that cannot write moov closes quietly instead of emitting two more -22s for a moov it was never able to produce.

Verification

6 new tests in Issue222EAC3MoovPrimeTests: deferral instead of failure, AAC unaffected (its sample entry is codecpar-derived), dec3 present in the primed init, the primed fragment not served, a video-only first segment cutting cleanly, and the tfdt regression, which is proven to fail without the re-arm. Full suite 1087 tests in 175 suites green, swift build -Xswiftc -strict-concurrency=complete clean, CI green on macOS, tvOS Simulator and iOS Simulator.

End to end against a synthetic MKV that reproduces the reported layout (HEVC Main10 plus E-AC-3 5.1, audio starting behind 12 s of video): audioTrack codec='ec-3', first frame at 0.15 s, 20 s of playback with no stall, and produced segments carrying seg0 video-only at 0.000s, seg3 video 11.792s with audio 11.995s, seg4 continuous. A 40-seek burst settles with 0.00s error and no second deferral. A control fixture with healthy interleave primes nothing and keeps its fragment timestamps unchanged.

Acknowledgements

Thanks to @thatcube for the report and for the root cause: the missing guard in cutFragmentForNextSegment, the packet-ordering trigger behind audioWritten=0, and for mechanically ruling out both dec3-in-extradata and dropping +delay_moov before proposing anything.