What happened?
VC-1 Advanced Profile content played through AetherEngine's software decode path judders, reproducibly, on Apple TV 4K (via Sodalite/Jellyfin or Moonfin/Jellyfin) and does not judder in mpv or other FFmpeg-based players on the same files on AppleTV. The interlace/deinterlace subsystem, container transport, and file bitrate have all been ruled out. The most likely cause is that AetherEngine's software decoder reads AVFrame.pts directly rather than FFmpeg's best_effort_timestamp, and AVFrame.pts is unset (AV_NOPTS_VALUE) on every I and P frame in affected files — only B-frames carry a valid raw pts. AVSampleBufferDisplayLayer, which the software path uses for presentation, has no independent pacing/cadence model, so it presents strictly on whatever CMTime it's given — any inaccurate fallback timestamp for the 60% of frames lacking raw PTS becomes visible, periodic judder.
Steps to reproduce
Play a VC-1 file in Sodalite
It will exhibit significant (unwatchable) video judder
AetherEngine version or commit SHA
as embedded in Sodalite 1.0.0 (20)
Host app
Sodalite
Platform
tvOS
OS version
tvos 26
Device / chip
Apple TV 4k 3g
Playback path
Software (dav1d) host
Source media (for playback bugs)
vc1 (Advanced) (WVC1 / 0x31435657), yuv420p(progressive), 1920x1080, 23.98 fps, 23.98 tbr, 1k tbn, Matroska container, TimecodeScale default (ms granularity)
ffprobe.txt
Error codes / log lines
Anything else
From Claude based on extensive analysis of video files, logs, and testing across devices and media types. I also have separate analysis of why this is an issue with AetherEngine and not mpv and other ffmpeg based media players.
Environment
- Repo:
github.com/superuser404notfound/AetherEngine, HEAD examined 4219e140 (2026-08-22), release tag 6.34.1
- Clients: Sodalite and AetherPlayer both pin
f4bebc0fd3c66f656ef18d2c7c0b3243df3ac86d = tag 6.34.1 (current release, not stale)
- Playback target: Apple TV 4K
- Reproduces via: Jellyfin/Sodalite over 1 Gbps, and direct SMB mount over 10 Gbps — identical judder both ways
- Sample file: 1920x1080 VC-1 (Advanced), progressive, 23.98 fps, ~19.7 Mb/s, Matroska container, B-frames present
What's been ruled out
- Profile-conditional interlace assumption. No code path branches on VC-1 profile;
VideoRoutingPolicy.requiresSoftwarePath routes all VC-1 to software unconditionally regardless of field order (VideoRoutingPolicy.swift:29-47), and routesSoftwareForDeclaredInterlace is hard-gated to H.264 only (VideoRoutingPolicy.swift:58).
- Deinterlace filter engaging on progressive content.
DeinterlaceFilter only activates on the decoded frame's actual AV_FRAME_FLAG_INTERLACED flag, checked per-frame (SoftwareVideoDecoder.swift:235-269). Confirmed via direct ffprobe -show_frames against the reproducing file: repeat_pict=0 on every frame in a 37.5s sample — no pulldown/interlace signaling present in the bitstream at all.
- Historical PTS-doubling bug (
eedaa5b5). Fixed since 3.11.2, and in any case only fires when DeinterlaceFilter is active — moot here since the deinterlacer never engages for progressive content.
- Transport/bitrate. Identical judder over both a 1 Gbps Jellyfin stream and a 10 Gbps local SMB mount rules out network throughput and IO buffering as a cause.
- Container/demuxer-level timestamp irregularity.
ffprobe -show_entries frame=pts_time,best_effort_timestamp_time,pkt_dts_time,pict_type,repeat_pict against the actual reproducing file shows best_effort_timestamp_time populated on 100% of frames, evenly spaced at the expected ~41.7/41.6/41.7ms cadence for 23.976fps, with no gaps or discontinuities across the full sample. The source data and FFmpeg's own reconstruction of it are both clean.
The finding
In the same ffprobe output, raw pts_time (i.e., AVFrame.pts as the VC-1 decoder set it, before any FFmpeg-side reconstruction) is N/A on every I-frame and every P-frame in the sample — only B-frames carry a valid raw PTS. This split is total and consistent across the entire 37.5s capture (GOP pattern I B P B P; 3 of every 5 frames have no raw PTS). best_effort_timestamp, which FFmpeg computes specifically to paper over exactly this kind of gap using DTS/reorder-depth reconstruction, is complete and correct for the same frames.
This means: whatever timestamp AetherEngine's software decoder hands to the renderer for 60% of frames in this file depends entirely on which field it reads. If it reads raw pts (matching SoftwareVideoDecoder.swift's apparent per-frame handling in drainDecodedFrames, SoftwareVideoDecoder.swift:235-269) rather than best_effort_timestamp, most frames arrive with no usable timestamp and whatever fallback is used — extrapolation, last-known-PTS reuse, arrival-time estimation — is a strong candidate for the judder's actual source.
This is also why the symptom is AetherEngine-specific despite shared FFmpeg internals: mpv and other FFmpeg-based players never consume raw frame->pts for exactly this reason, always using best_effort_timestamp instead. And it's why the software-decode presentation path is unusually sensitive to any timestamp error in the first place — AVSampleBufferDisplayLayer presents each sample strictly at the CMTime it's given, with no vsync-aware cadence model or interpolation to absorb an inaccurate value the way mpv's own pacing loop would.
(Why VC-1's decoder produces this split — B-frames tagged, I/P frames losing their association during internal reference-picture buffering — is a plausible mechanism based on the observed pattern, not something confirmable without libavcodec source access. It doesn't change the recommended fix.)
Recommended fix
- In
SoftwareVideoDecoder.swift's frame emission path, switch from reading the decoded frame's raw pts to its best_effort_timestamp (bridged via av_frame_get_best_effort_timestamp() if not already exposed as a Swift-accessible property). This is confirmed complete and correctly-paced for the reproducing file and should need no additional reconstruction logic.
- Audit whatever fallback currently runs when
pts == AV_NOPTS_VALUE — that code path is exercised on the majority of frames in files like this one and is the immediate suspect for the visible judder.
- Once switched, set an explicit
duration on each CMSampleTimingInfo (not just presentationTimeStamp), so CMSampleBufferMake/AVSampleBufferDisplayLayer has the intended per-frame duration rather than inferring it from enqueue timing.
Supporting data
ffprobe -show_frames output against the reproducing file (pts_time, pkt_dts_time, best_effort_timestamp_time, pict_type, repeat_pict), ~900-frame / 37.5s sample — available on request, confirms the 100%-consistent I/P pts_time=N/A pattern and fully clean best_effort_timestamp_time sequence.
- Stream metadata:
vc1 (Advanced) (WVC1 / 0x31435657), yuv420p(progressive), 1920x1080, 23.98 fps, 23.98 tbr, 1k tbn, Matroska container, TimecodeScale default (ms granularity).
What happened?
VC-1 Advanced Profile content played through AetherEngine's software decode path judders, reproducibly, on Apple TV 4K (via Sodalite/Jellyfin or Moonfin/Jellyfin) and does not judder in mpv or other FFmpeg-based players on the same files on AppleTV. The interlace/deinterlace subsystem, container transport, and file bitrate have all been ruled out. The most likely cause is that AetherEngine's software decoder reads AVFrame.pts directly rather than FFmpeg's best_effort_timestamp, and AVFrame.pts is unset (AV_NOPTS_VALUE) on every I and P frame in affected files — only B-frames carry a valid raw pts. AVSampleBufferDisplayLayer, which the software path uses for presentation, has no independent pacing/cadence model, so it presents strictly on whatever CMTime it's given — any inaccurate fallback timestamp for the 60% of frames lacking raw PTS becomes visible, periodic judder.
Steps to reproduce
Play a VC-1 file in Sodalite
It will exhibit significant (unwatchable) video judder
AetherEngine version or commit SHA
as embedded in Sodalite 1.0.0 (20)
Host app
Sodalite
Platform
tvOS
OS version
tvos 26
Device / chip
Apple TV 4k 3g
Playback path
Software (dav1d) host
Source media (for playback bugs)
vc1 (Advanced) (WVC1 / 0x31435657), yuv420p(progressive), 1920x1080, 23.98 fps, 23.98 tbr, 1k tbn, Matroska container, TimecodeScale default (ms granularity)
ffprobe.txt
Error codes / log lines
Anything else
From Claude based on extensive analysis of video files, logs, and testing across devices and media types. I also have separate analysis of why this is an issue with AetherEngine and not mpv and other ffmpeg based media players.
Environment
github.com/superuser404notfound/AetherEngine, HEAD examined4219e140(2026-08-22), release tag6.34.1f4bebc0fd3c66f656ef18d2c7c0b3243df3ac86d= tag6.34.1(current release, not stale)What's been ruled out
VideoRoutingPolicy.requiresSoftwarePathroutes all VC-1 to software unconditionally regardless of field order (VideoRoutingPolicy.swift:29-47), androutesSoftwareForDeclaredInterlaceis hard-gated to H.264 only (VideoRoutingPolicy.swift:58).DeinterlaceFilteronly activates on the decoded frame's actualAV_FRAME_FLAG_INTERLACEDflag, checked per-frame (SoftwareVideoDecoder.swift:235-269). Confirmed via directffprobe -show_framesagainst the reproducing file:repeat_pict=0on every frame in a 37.5s sample — no pulldown/interlace signaling present in the bitstream at all.eedaa5b5). Fixed since 3.11.2, and in any case only fires whenDeinterlaceFilteris active — moot here since the deinterlacer never engages for progressive content.ffprobe -show_entries frame=pts_time,best_effort_timestamp_time,pkt_dts_time,pict_type,repeat_pictagainst the actual reproducing file showsbest_effort_timestamp_timepopulated on 100% of frames, evenly spaced at the expected ~41.7/41.6/41.7ms cadence for 23.976fps, with no gaps or discontinuities across the full sample. The source data and FFmpeg's own reconstruction of it are both clean.The finding
In the same ffprobe output, raw
pts_time(i.e.,AVFrame.ptsas the VC-1 decoder set it, before any FFmpeg-side reconstruction) isN/Aon every I-frame and every P-frame in the sample — only B-frames carry a valid raw PTS. This split is total and consistent across the entire 37.5s capture (GOP patternI B P B P; 3 of every 5 frames have no raw PTS).best_effort_timestamp, which FFmpeg computes specifically to paper over exactly this kind of gap using DTS/reorder-depth reconstruction, is complete and correct for the same frames.This means: whatever timestamp AetherEngine's software decoder hands to the renderer for 60% of frames in this file depends entirely on which field it reads. If it reads raw
pts(matchingSoftwareVideoDecoder.swift's apparent per-frame handling indrainDecodedFrames,SoftwareVideoDecoder.swift:235-269) rather thanbest_effort_timestamp, most frames arrive with no usable timestamp and whatever fallback is used — extrapolation, last-known-PTS reuse, arrival-time estimation — is a strong candidate for the judder's actual source.This is also why the symptom is AetherEngine-specific despite shared FFmpeg internals: mpv and other FFmpeg-based players never consume raw
frame->ptsfor exactly this reason, always usingbest_effort_timestampinstead. And it's why the software-decode presentation path is unusually sensitive to any timestamp error in the first place —AVSampleBufferDisplayLayerpresents each sample strictly at theCMTimeit's given, with no vsync-aware cadence model or interpolation to absorb an inaccurate value the way mpv's own pacing loop would.(Why VC-1's decoder produces this split — B-frames tagged, I/P frames losing their association during internal reference-picture buffering — is a plausible mechanism based on the observed pattern, not something confirmable without libavcodec source access. It doesn't change the recommended fix.)
Recommended fix
SoftwareVideoDecoder.swift's frame emission path, switch from reading the decoded frame's rawptsto itsbest_effort_timestamp(bridged viaav_frame_get_best_effort_timestamp()if not already exposed as a Swift-accessible property). This is confirmed complete and correctly-paced for the reproducing file and should need no additional reconstruction logic.pts == AV_NOPTS_VALUE— that code path is exercised on the majority of frames in files like this one and is the immediate suspect for the visible judder.durationon eachCMSampleTimingInfo(not justpresentationTimeStamp), soCMSampleBufferMake/AVSampleBufferDisplayLayerhas the intended per-frame duration rather than inferring it from enqueue timing.Supporting data
ffprobe -show_framesoutput against the reproducing file (pts_time,pkt_dts_time,best_effort_timestamp_time,pict_type,repeat_pict), ~900-frame / 37.5s sample — available on request, confirms the 100%-consistent I/Ppts_time=N/Apattern and fully cleanbest_effort_timestamp_timesequence.vc1 (Advanced) (WVC1 / 0x31435657), yuv420p(progressive), 1920x1080, 23.98 fps, 23.98 tbr, 1k tbn, Matroska container,TimecodeScaledefault (ms granularity).