# hls.js PR #8055 playback reproduction

This page reproduces the skipped audio part through normal hls.js playback.
JavaScript generates live playlist updates through the supported `pLoader` configuration.
The default loader reads real fMP4 audio through HTTP or embedded Blob URLs.
A wrapper records each media request.

## Public demo and playlist

The [public demo](https://arc-euc1-development.crowdcats.dev/?delivery=http) supports both hls.js builds and native Safari.
It uses the HTTP replay by default through that link.

The [fresh playlist URL](https://arc-euc1-development.crowdcats.dev/start.m3u8?tail=1&boundaries=30) starts a new replay for each player.
The server redirects each initial request to a unique media playlist URL.
Playlist reloads use that unique URL and keep the same replay clock.
The server uses no cookies and sends `Cache-Control: no-store`.
The final update includes `EXT-X-ENDLIST`.

The URL accepts these parameters:

| Parameter | Values | Default |
| --- | --- | --- |
| `tail` | `1`, `4`, `11`, or `12` AAC frames | `1` |
| `boundaries` | `1` through `60` | `30` |

The [85.333 ms playlist](https://arc-euc1-development.crowdcats.dev/start.m3u8?tail=4&boundaries=30) amplifies the missing requests in this fixture.
Use the starter URL when you share a replay.
An active session URL keeps its original clock and expires after ten minutes.

## Open one HTML file

1. Open [standalone.html](standalone.html) directly in Chrome.
2. Select **Upstream baseline** and **Run playback**.
3. Repeat with **PR #8055 only**.

The single file contains both player builds and all audio for the four tail sizes.
It needs no server or internet connection.
You can copy that HTML file alone to another folder or computer.
All playback lengths are available, including approximately two minutes.
Native Safari comparison uses the HTTP version described below.

## Run the HTTP version

1. Open a terminal in this folder.
2. Start the static server:

   ```sh
   python3 tools/serve.py --port 8775
   ```

3. Open <http://127.0.0.1:8775/> in Chrome.
4. Select **Upstream baseline**.
5. Select the playback length.
6. Select **Run playback**.
7. Repeat the procedure with **PR #8055 only**.

The page includes both player builds and all media files.
The JavaScript mode needs no CDN or live streaming server.
The included server also supports the HTTP replay for native Safari.
The default length is approximately 60 seconds and covers 30 boundaries.
The other lengths cover one boundary, 15 boundaries (approximately 30 seconds), or 60 boundaries (approximately two minutes).
Audio is muted by default.
The **Stop** button ends the current run.

## Expected requests

| Build | Media request order after initialization | Result |
| --- | --- | --- |
| Upstream | `1.0 → 1.1 → 1.2 → 1.3 → 2.0 → 2.1 → …` | Skips tail `1.4` |
| PR #8055 | `1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 2.0 → 2.1 → …` | Loads tail `1.4` |

In the HTTP version, the browser network panel shows the actual `.m4s` requests.
The single file uses Blob URLs containing the same media bytes.
The page log also shows those requests and the hls.js debug output.
The page counters show examined boundaries, bypassed tails, later recovery, and the duration of audio still unrequested.
Thirty skipped tails total 640 ms of audio.
This total measures missing requests, not audio/video sync drift.

## Tail size

Two thresholds affect this example.
The default `maxFragLookUpTolerance` is 250 ms.
It permits fragment lookup to advance beyond a missing tail.
The default `maxBufferHole` is 100 ms.
The buffer helper treats holes smaller than 100 ms as continuous buffer.
The controller can therefore continue loading without returning for a small missing tail.

A larger hole keeps the forward buffer end before the missing tail.
After loading the next segment, the controller returns for that tail.
An initial bypass therefore does not guarantee lasting loss.

At 48 kHz, each 1024-sample AAC frame lasts 21.333 ms.
Chrome tests with default settings produced these results:

| Tail | Upstream result | PR result |
| --- | --- | --- |
| 1 frame: 21.333 ms | Remains unrequested | Loads in order |
| 4 frames: 85.333 ms | Remains unrequested | Loads in order |
| 5 frames: 106.667 ms | Bypassed, then recovered | Loads in order |
| 11 frames: 234.667 ms | Bypassed, then recovered | Loads in order |
| 12 frames: 256 ms | Loads in order | Loads in order |

Four frames are the largest whole-frame tail below the 100 ms threshold at this sample rate.
The page includes the 1-, 4-, 11-, and 12-frame options.
The 234.667 ms option demonstrates an initial ordering error, followed by recovery.
It does not reproduce the lasting omission seen with the smaller tails.
The result label reports this distinction.
These findings apply to this fixture and the tested browser; they do not define a universal limit for every stream.

In the 30-boundary run, upstream bypassed 15 of the 234.667 ms tails and later loaded all 15.
The PR bypassed none.

## Native Safari comparison

1. Open the [public demo](https://arc-euc1-development.crowdcats.dev/?delivery=http) in Safari.
2. Select the tail size and playback length.
3. Select **HTTP replay (Safari comparison)**.
4. Run the selected hls.js build.
5. Select **Native Safari**.
6. Run the native player.

Both engines receive the same playlist snapshots, media bytes, and publication schedule through real HTTP requests.
Each run creates a separate replay session that starts at the same point.
The server holds blocking playlist requests until the requested parts appear.
The HTTP schedule publishes the first update after 1.2 seconds, then follows the selected segment duration.

The **Current live playlist URL** link shows the source for the active session.
The **Fresh playlist URL** link starts a separate replay for another player.
Safari uses its built-in HLS player, without hls.js.
Native requests can include complete segments instead of individual parts.
The native counters show fetched tail files and full segments, because Safari does not expose hls.js fragment state.
The native tail counter excludes historical segment 0.

JavaScript playlist loaders run only inside hls.js.
GitHub Pages can host the JavaScript mode, but native Safari needs an HTTP server for the changing playlist.
For local playback, start the included server and open <http://127.0.0.1:8775/> in Safari.

## Why the update matters

This section describes the original one-frame tail.
Each original segment contains 93 AAC frames at 48 kHz.
The five parts contain 23 + 23 + 23 + 23 + 1 frames.
Their durations are 490.667 ms, 490.667 ms, 490.667 ms, 490.667 ms, and 21.333 ms.

The first playlist contains completed segment 0 and the first four parts of segment 1.
Playback starts at segment 1 through `startPosition`.
The playlist loader holds the next request until part `1.3` loads.
After a 350 ms delay, the next playlist adds part `1.4`, completes segment 1, and adds four parts of segment 2.

That delay lets `doFragPartsLoad` finish its initial chain before the new tail appears.
The default `maxFragLookUpTolerance` then permits fragment selection to advance to segment 2.
The controller clamps the target to 3.968 seconds, which equals the start of part `2.0`.
Upstream selects `2.0` and skips `1.4`.
The PR selects `1.4` first.

The same sequence repeats for each segment.
Later updates occur at intervals of 1.984 seconds, which equals the segment duration.
The 4-, 11-, and 12-frame options use intervals of 2.048 seconds, 2.197333 seconds, and 2.218667 seconds.
Each update waits at least 350 ms after the preceding part 3 finishes.
The page stops loading after it examines the selected number of boundaries.

The loader generates playlist responses and holds pending requests.
It does not call internal controller methods or assign loaded, buffered, or fragment state.
The request wrapper passes HTTP or Blob media requests directly to the default loader.

The saved [playlist A](playlist-a.m3u8) and [playlist B](playlist-b.m3u8) show the first two updates.
Opening either snapshot alone does not recreate the update timing.

## GitHub Pages

GitHub Pages can host this folder unchanged.
The browser generates the live updates, so Pages needs no server code.
The existing site is <https://darfink.github.io/hls.js/>.
A suitable destination is `repros/pr8055/` on its `gh-pages` branch.
The proposed public URL is <https://darfink.github.io/hls.js/repros/pr8055/>.
This package does not publish that URL.

## Build and media sources

The baseline commit is `c721313f028431b107e4a77edf40bb908f8d782c`.
The PR commit is `40b4aba0f32d5ffbf188e764ec7fc2d8062850fe`.
Both builds use the same dependency installation and the original full ESM build configuration.
The PR build includes only the changes in PR #8055.
File hashes are in [provenance.json](provenance.json).

To regenerate the audio, run:

```sh
python3 tools/generate-media.py
```

The script requires FFmpeg.
It encodes a synthetic 440 Hz tone and splits the result at AAC frame boundaries.
Each part retains its original decode timestamps.
The package contains 62 segments for each of the four tail sizes.
Their total durations are 123.008 seconds, 126.976 seconds, 136.235 seconds, and 137.557 seconds.
Part `1.4` contains one AAC packet at 3.946667 seconds, with a duration of 0.021333 seconds.
The HTTP server reads fixtures that the same JavaScript playlist generator creates.
To regenerate those fixtures after a playlist change, run:

```sh
node tools/write-playlists.mjs
```

To rebuild the player bundles, provide an hls.js checkout with both commits and installed dependencies:

```sh
node tools/build.cjs /path/to/hls.js
```

To rebuild the single HTML file after changing the source or media, run:

```sh
python3 tools/build-standalone.py
```

## Validation and scope

The public HTTPS demo reproduced the missing one-frame tail with upstream and loaded it first with the PR.
Chrome also played the starter URL directly with the one- and four-frame tails.
Upstream omitted `1.4`, and the PR requested `1.4` before `2.0`.
Both builds kept the canonical replay URL for playlist reloads.
Public runs reported a nonfatal startup `bufferSeekOverHole` as playback moved to the configured start position.
They reported no fatal errors.
Native Safari also played the public starter URL beyond the first tail without a media error.
The public API checks confirmed independent sessions, no cookies, media delivery, and `EXT-X-ENDLIST`.

Chrome opened the single HTML file from an isolated folder with no other assets.
Both builds completed the original 30-boundary run with browser networking disabled.
Upstream left all 30 tails unrequested, totaling 640 ms; the PR loaded all 30.
Both runs reported no hls.js errors.
Quick checks also confirmed the same outcomes for all four tail options in the single file.

Chrome completed the default 30-boundary run with both builds.
Upstream skipped all 30 tails, which totals 640 ms of unrequested audio.
The PR build loaded all 30 tails.
Both runs continued playback for approximately 58 seconds and reported no hls.js errors.
The [upstream log](logs/upstream.txt) and [PR log](logs/pr8055.txt) contain the observed request sequences.
The complete results are in [verification.json](verification.json).
The additional tail-size checks cover 21.333 ms, 85.333 ms, 106.667 ms, 192 ms, 213.333 ms, 234.667 ms, and the 256 ms control.
The 106.667 ms, 192 ms, and 213.333 ms checks also recovered their bypassed tail.
Read-only controller traces show how `maxBufferHole` affects recovery in this fixture.
The 85.333 ms trace contains two browser buffer ranges separated by the missing tail.
The buffer helper merges them for loading because their gap is smaller than 100 ms.
The 106.667 ms trace keeps them separate and then selects the missing tail.
The 85.333 ms option also completed a 30-boundary run.
Upstream left all 30 tails unrequested, totaling 2.56 seconds.
It reported 30 nonfatal buffer stalls and 30 seeks across the gaps.
The PR loaded all 30 tails and reported no errors.
The [85.333 ms upstream log](logs/tail-4-upstream.txt) and [PR log](logs/tail-4-pr8055.txt) show those results.
The [85.333 ms trace](logs/trace-tail-4.json) and [106.667 ms trace](logs/trace-tail-5.json) record the recovery threshold.
The HTTP replay reproduced the initial 234.667 ms bypass with upstream and loaded the tail first with the PR.
Native Safari played that HTTP replay without an error and requested `1.4` before `2.0`.
Native Safari also played the new 85.333 ms replay without an error and requested its tail before the next segment.

The archived 234.667 ms long-run log uses the earlier label that called any initial bypass a reproduced bug.
All its bypassed tails were recovered.
The current page distinguishes recovered bypasses from tails still unrequested.

This example demonstrates the missing media request with synthetic audio.
It does not reproduce cumulative audio/video drift.

The reviewer's separate concern about an independent part at an exact boundary also reproduces.
For `[P0 independent, P1, P2 independent, P3]` at target 22, upstream selects P2 and the PR selects P0.
The playback reproduction establishes the tail problem, but that boundary regression still needs a narrower fix.

The player source retains its Apache-2.0 license in [LICENSE](LICENSE).
