Twitch.tv may stall at ad transition during live streaming
Component: WebCore Media Source Extensions | 93e7a14
MSE Coded Frame Processing is the normatively specified algorithm determining how incoming encoded frames interact with already-buffered content; steps 1.14/1.15 are its overlap-removal phase, erasing buffered content that a new frame's presentation range overlaps. B-frames (bidirectional predictive) have PTS > DTS because they are decoded before display, and at the tail of an fMP4 segment the last-in-decode-order sample's trun.sample_duration is a decode-grid placeholder rather than a real presentation duration.
LayoutTests/media/media-source/media-source-append-b-frame-tail-overshoot.html
Because that tail duration is a placeholder, frameEndTime = pts + duration can slightly exceed the next buffered sample's PTS without representing a true editorial overlap — and the spec-literal removal then erases legitimate content, producing a buffered-range gap that stalls playback at Twitch ad splice points. The fix adds a per-track "B-frame tail + within-timeFudgeFactor" heuristic that redirects to a forward-shift path instead, introducing MediaSample::createCopyWithAdjustedStartTime, TrackBuffer::adjustSampleStartTime, and SampleMap::replaceSample to shift the overlapped sample forward by the overshoot. Three coupled structures mutate together: the SampleMap's presentation-order submap, its decode-order submap, and TrackBuffer::m_decodeQueue.
fMP4 tail B-frame: dts=50 pts=80 dur=30 => frameEnd=110
Next buffered content sync: pts=100
BEFORE (spec-literal 1.14):
Erase range [50, 110) from buffered
content sync @ pts=100 ∈ [50,110) → REMOVED
step 1.15 cascades dependents
buffered: [0, 0.110) ← gap, playback stalls
AFTER (new heuristic path):
overshoot = frameEnd(110) − nextSample.pts(100) = 10 ms
10ms < timeFudgeFactor() AND B-frame tail?
YES → adjustSampleStartTime: shift sync pts 100→110
SampleMap::replaceSample (erase+hint-insert, both submaps)
m_decodeQueue entry updated to match
1.14 erase range [50,110) no longer contains the sync
buffered: [0, 0.200) ← continuous, no stall
Significance
The fix adds timing-mutation code paths that atomically rewrite three coupled MSE bookkeeping structures — both SampleMap submaps and the decode queue — on a path driven directly by attacker-supplied fMP4 bytes. Live-stream stalls at ad splice points are resolved, at the cost of new state and lifetime surface in tightly coupled sample bookkeeping.
Audit directions
Narrow: SampleMap::replaceSample performs erase+hint-insert in both the presentation-order and decode-order submaps without validating that the adjusted sample's key is strictly ordered relative to its neighbors. A zero-duration sample clamping the offset to zero makes the "new" key identical to the old, so the erase+insert is a no-op while m_buffered is still mutated — the two structures desynchronize. The review tell is an erase-then-insert pair where the new key is derived arithmetically from the old without an ordering assertion.
Wider: range bookkeeping symmetry. TrackBuffer::adjustSampleStartTime subtracts the original sample's [pts, presentationEndTime) from m_buffered and re-adds the adjusted range. If the original range is absent at call time — the sample not yet fully committed, or a concurrent step-1.15 cascade already having removed it — the subtraction is a no-op and the adjusted range is inserted without a matching removal, producing a spurious buffered range that shifts the media element's seekable range. Audit every other m_buffered mutation in TrackBuffer for the same subtract-then-add shape and check whether each verifies the subtraction actually removed something. Adjacent: the per-track isPresentationTail flag is computed in processPendingMediaSamples before the append loop, so multi-track segments with interleaved audio/video samples could flag the wrong sample and let the shift path fire on a non-tail video sample the spec mandates be removed.
Widest: platform/engine split-brain. MediaSampleAVFObjC::createCopyWithAdjustedStartTime uses CMSampleBufferCreateCopyWithNewTiming and applies the offset to each sub-sample of a multi-sample CMSampleBuffer. If any adjusted start time goes negative or a sub-sample duration underflows, AVFoundation may accept the buffer while WebCore's TimeRanges bookkeeping computes a different range — the platform decoder and the MSE buffered attribute then disagree. This generalizes to every place WebCore mirrors platform-media timing state; wherever both sides compute a range independently from the same mutation, the divergence case needs a test. Fuzzing crafted fMP4 segments that hit the B-frame tail path across changeType, appendWindowStart, and timestampOffset boundaries is the most productive entry point.