You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(player): hold the last frame through a software-path seek (#90)
SoftwarePlaybackHost.seek() flushed the renderer, and
SampleBufferRenderer.flush() unconditionally removed the displayed image
(modern: flush(removingDisplayedImage: true); legacy: flushAndRemoveImage()),
so the visible frame was cleared before the post-seek keyframe decoded, a
black flash on slow sources like MPEG-2. The native/AVPlayer path holds the
frame through a seek.
flush() now takes a removingDisplayedImage flag (default true for
stop/teardown); seek() passes false so the previous frame stays on screen
until the post-seek frame is enqueued. DisplayFlushOp is the pure decision
(modern rendererFlush vs legacy flushAndRemoveImage vs legacy hold) split out
so the contract is unit-testable without a live AVSampleBufferDisplayLayer; 4
tests cover the full truth table.
Confirmed on-device by the reporter: MPEG-2 episodes no longer flash black on
seek.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A seek holds the last frame on screen rather than blanking it. `SampleBufferRenderer.flush()` takes a `removingDisplayedImage` flag (`DisplayFlushOp` is the pure decision split out for testing): stop/teardown clears the visible frame (the default), but `SoftwarePlaybackHost.seek()` passes `false`, so the previous frame stays up until the post-seek keyframe decodes instead of flashing black on slow sources like MPEG-2. This matches the native/AVPlayer path, which holds the frame through a seek (#90).
42
+
41
43
`AudioDecoder` stamps each `CMSampleBuffer` from a running sample count anchored to the first frame (`AudioClockAnchor`), not from the container-quantized per-packet PTS. Container timebases are coarse (1 ms in MKV), so when a frame's duration is not an integer number of ticks (a 1536-sample AC-3 frame is 34.83 ms at 44.1 kHz but exactly 32 ms at 48 kHz) the quantized PTS leave a sub-millisecond gap or overlap at every buffer boundary, and `AVSampleBufferAudioRenderer` reconciles a discontinuity at each one (~29 clicks/sec, a continuous crackle). Anchoring to the sample clock makes consecutive buffers abut exactly; a real source discontinuity (> 100 ms off the predicted clock, i.e. a seek or edit) re-anchors so genuine gaps are not papered over, and `flush()` drops the anchor. The clock advances only on a successfully emitted buffer, so a dropped buffer injects no phantom samples.
42
44
43
45
AV1+DV (Profile 10.0 / 10.1 / 10.4) routes through the native path on hardware-AV1 hosts via the `dav1` / `av01` track type plus the source's `dvvC` box. AV1+Atmos is genuinely rare in the wild (mastering still runs in HEVC overwhelmingly), so the SW pipeline's lack of Atmos passthrough is a theoretical limitation rather than a real one. The dispatch happens once at load time; hosts see a unified `@Published` state surface either way.
@@ -179,7 +181,7 @@ Sources/AetherEngine/
179
181
├── Network/
180
182
│ └── HLSLocalServer.swift Native path: local HTTP server (127.0.0.1) serving playlist + segments
0 commit comments