Bug 299853
| Summary: | REGRESSION (Safari 26): Live HLS ID3 timed metadata cues mapped with endTime = Infinity cause TextTrack.activeCues to include all past cues indefinitely | ||
|---|---|---|---|
| Product: | WebKit | Reporter: | baehongjun |
| Component: | Media | Assignee: | Eric Carlson <eric.carlson> |
| Status: | RESOLVED FIXED | ||
| Severity: | Normal | CC: | demur_petty.5t, jer.noble, webkit-bug-importer |
| Priority: | P2 | Keywords: | InRadar |
| Version: | Safari 26 | ||
| Hardware: | Mac (Apple Silicon) | ||
| OS: | macOS 26 | ||
baehongjun
**Product/Component**
* Product: WebKit
* Component: Media → TextTracks / Timed Metadata
* Version/Build: Safari 26.0 (20622.1.22), 26.0.1 on macOS 26 / iOS 26
**Summary**
Since Safari 26, ID3 timed metadata (in-band HLS) seems to be mapped to `DataCue` objects whose `endTime` is `Infinity`. As a result, `textTrack.activeCues` no longer represents “cues overlapping the current time” but **keeps all cues inserted so far**.
Our ad logic reads the latest **NMSS timestamp** (`info = "com.timestamp"`) from `activeCues`. On Safari 26, `activeCues[0]` sticks to the very first cue, so the parsed `current-utc` value **never updates**. Earlier Safari versions and other browsers behave as expected.
**Context / Potential cause**
* Safari 26 release notes mention *“Added support for in-band tracks in MSE.”* Our path does not use MSE, but there may have been broader changes in TextTrack/timed-metadata plumbing. ([Apple Developer][1])
* The HTML spec defines **unbounded cues** as those with `endTime = +Infinity`, which **cannot become inactive** during normal playback. Safari 26 appears to map ID3 cues as unbounded now. ([html.spec.whatwg.org][2])
* Past discussions proposed using `Infinity` to represent end-of-media, and the Media Timed Events TF discussed **bounding the previous unbounded cue when a new event arrives** to keep a meaningful active set. Safari 26 seems to keep **all** previous ID3 cues unbounded. ([GitHub][3])
**Actual Results**
* `textTrack.activeCues` permanently includes **all NMSS timestamp cues since insertion** → `activeCues[0]` holds a stale `current-utc`
* Mid-roll ad polling logic stops (no “latest timestamp” detected), so no subsequent ad requests fire
**Expected Results**
* Either keep `activeCues` limited to cues that overlap the current playback time (as before), **or**
* If adopting unbounded cues, **bound the previous cue’s `endTime` when a new event arrives**, so the active set remains meaningful around “now”
**Workaround (production-safe)**
On Safari 26, avoid `activeCues` and **search by `startTime` around the current time** (binary search for performance):
```ts
function isSafari26() {
const ua = navigator.userAgent;
const isSafari = !!ua.match(/Version\/[\d.]+.*Safari/) && navigator.vendor === 'Apple Computer, Inc.';
if (!isSafari) return false;
const m = ua.match(/Version\/(\d+)/);
return m && parseInt(m[1], 10) === 26;
}
function getNmssTimestampCue(player: HTMLVideoElement, tolerance: number): DataCue | null {
for (const track of Array.from(player.textTracks)) {
if (track.kind !== 'metadata') continue;
if (!track.cues) { if (track.mode === 'disabled') track.mode = 'hidden'; continue; }
let hasNmss = false;
for (let i = 0; i < track.cues.length; i++) {
const { value } = track.cues[i] as any;
if (value && value.info === 'com.timestamp') { hasNmss = true; break; }
}
if (!hasNmss) continue;
const active = isSafari26() ? getAdjacentCues(track, player.currentTime, tolerance)
: (track.activeCues ? Array.from(track.activeCues) : []);
if (active.length === 0) continue;
return active[0] as DataCue;
}
return null;
}
function getAdjacentCues(track: TextTrack, currentTime: number, tolerance: number): TextTrackCue[] {
const result: TextTrackCue[] = [];
const cues = track.cues;
if (!cues || cues.length === 0) return result;
let l = 0, r = cues.length - 1, last = -1;
while (l <= r) {
const mid = (l + r) >> 1;
if (cues[mid].startTime <= currentTime) { last = mid; l = mid + 1; }
else { r = mid - 1; }
}
if (last === -1) return result;
for (let i = last; i >= 0; i--) {
const cue = cues[i];
if (currentTime - cue.startTime > tolerance) break;
result.unshift(cue);
}
return result;
}
```
**Regression?**
* Yes. Works on earlier Safari and other engines; breaks on Safari 26.0/26.0.1 (macOS/iOS).
**Questions / Requests**
1. Was mapping ID3 cues to **unbounded** (`endTime = Infinity`) an intentional change in Safari 26?
2. If intentional, could WebKit consider **bounding the previous unbounded cue** when a new event arrives (per MTE TF discussions), or provide guidance on how to fetch the *latest* metadata event without scanning? ([W3C][4])
3. If this is expected by spec, please update release notes/docs to call out the web-compat impact for apps that rely on `activeCues` semantics. Current public notes don’t mention this detail. ([Apple Developer][1])
**References**
* Safari 26 Release Notes (26.0 / 20622.1.22; 26.1 beta list) ([Apple Developer][1])
* WebKit Features in Safari 26.0 (in-band tracks in MSE) ([WebKit][5])
* HTML Standard: unbounded text track cue (`endTime = +Infinity`) & “cannot become inactive” ([html.spec.whatwg.org][2])
* WHATWG: proposal to use `Infinity` for end-of-media ([GitHub][3])
* W3C Media Timed Events TF minutes: bounding previous unbounded cue when a new event arrives ([W3C][4])
* Apple HLS docs (timed metadata overview) ([Apple Developer][6])
---
[1]: https://developer.apple.com/documentation/safari-release-notes
[2]: https://html.spec.whatwg.org/dev/media.html
[3]: https://github.com/whatwg/html/issues/5297
[4]: https://www.w3.org/2021/04/19-me-minutes.html"
[5]: https://webkit.org/blog/17333/webkit-features-in-safari-26-0
[6]: https://developer.apple.com/documentation/http-live-streaming/links-to-additional-specifications-and-videos
[7]: https://docs.aws.amazon.com/medialive/latest/ug/insert-timed-metadata.html
| Attachments | ||
|---|---|---|
| Add attachment proposed patch, testcase, etc. |
baehongjun
# Reproduction Steps
Test on Safari 26.0 or 26.0.1 (macOS 26 / iOS 26)
Open Developer Tools (Inspector) → Console tab
Run the following snippet:
const video = document.querySelector('video');
for (const track of video.textTracks) {
if (track.kind === 'metadata') {
track.mode = 'hidden'; // ensure cues are exposed
console.log('metadata track:', track);
console.log('activeCues:', Array.from(track.activeCues || []));
}
}
- On Safari 26, track.activeCues does not include only the latest cue, but instead retains all past NMSS timestamp cues
- On Safari 25 or earlier, and on Chrome/Firefox, activeCues correctly returns only the cues that overlap the current playback position
Radar WebKit Bug Importer
<rdar://problem/161687917>
demur_petty.5t
issue can be reproduced on https://chzzk.naver.com/
while some streams are region-locked, most can be played.
demur_petty.5t
reproduced on iOS 18 and later, macOS Sequoia, and macOS Tahoe.
Similar to bug 255499.
Eric Carlson
Pull request: https://github.com/WebKit/WebKit/pull/53144
EWS
Committed 302395@main (01952a527b50): <https://commits.webkit.org/302395@main>
Reviewed commits have been landed. Closing PR #53144 and removing active labels.