HLS Encoding Incident

Empty CMAF Audio Fragment Causes Safari HLS Black Screen

This report focuses on a specific HLS encoding/packaging output defect: the playlist declares an audio media segment, but the corresponding CMAF chunk contains only an empty media-fragment shell with no AAC samples.

Analysis date: 2026-07-21 Affected platform: Safari / iOS MediaSource Player: ViewMaster + hls.js

Conclusion

The direct input that triggers the black screen is an invalid CMAF audio fragment generated by the upstream HLS encoding/packaging pipeline. The fragment contains moof and mdat, so it structurally presents itself as a media fragment. However, trun.sample_count = 0 and mdat has no payload, leaving no audio data to decode.

hls.js reports FRAG_PARSING_ERROR: Found no media. When Safari processes an audio chunk that has media-fragment structure but no actual samples, MediaSource/SourceBuffer can enter an invalid state and the video can black-screen.

Affected Asset

Master playlist
35bc4254b1c8229207a8-br636702.m3u8
Audio playlist
35bc4254b1c8229207a8_a.m3u8
Invalid chunk
35bc4254b1c8229207a8_a_000000007.cmfa
Chunk position
Final audio fragment, segment 7; declared duration: 1 second
Chunk size
120 bytes
Runtime error
Found no media in fragment 7 of track 0

Direct Evidence in the Playlist

The audio playlist ends as shown below. In HLS, #EXTINF declares the duration of the media segment URI that immediately follows it. Therefore, #EXTINF:1 tells the player to expect approximately one second of audio from chunk 7.

#EXTINF:2,
35bc4254b1c8229207a8_a_000000006.cmfa
#EXTINF:1,
35bc4254b1c8229207a8_a_000000007.cmfa
#EXT-X-ENDLIST

This establishes a clear playlist contract: URI 7 is a one-second audio media segment. However, the downloaded file has trun.sample_count = 0 and an mdat payload of 0 bytes.

Playlist declaration: one second of audio exists.
Actual CMAF chunk: zero audio samples exist.
This is a direct conflict between the timeline declaration and the media output.

Binary Structure of the Invalid Chunk

styp 24 bytes, segment type marker
moof 88 bytes, media fragment metadata
  └─ traf
    └─ trun sample_count = 0
mdat 8 bytes, box header only, payload = 0 bytes
00000000: 0000 0018 7374 7970 ...  ....styp
00000018: 0000 0058 6d6f 6f66 ...  ...Xmoof
...
00000060: 0000 0010 7472 756e 0000 0000 0000 0000
                                      ^^^^^^^^
                                      sample_count = 0
00000070: 0000 0008 6d64 6174  ....mdat
          ^^^^^^^^^
          box size = 8, meaning there is no payload

Roles of moof, trun.sample_count, and mdat

CMAF uses the fragmented MP4 structure. The player must combine sample metadata with encoded media bytes to determine when to decode samples, how many samples to decode, and where each sample is stored. These three structures provide that mapping.

moof
The Movie Fragment Box is the metadata entry point for the current media fragment. Its nested traf, tfhd, tfdt, and trun boxes describe the track, decode start time, sample count, sample durations, and sample sizes. The player uses this information to place the fragment on the correct playback timeline.
trun.sample_count
The sample count declared by the Track Run Box. For AAC audio, one sample typically represents one decodable AAC access unit. The player uses this count to read each sample's duration, size, and flags and to split the bytes in mdat. sample_count = 0 explicitly states that the fragment contains no audio samples.
mdat
The Media Data Box stores the actual compressed media payload, which should be AAC data in this case. The 8-byte mdat box header is not media content, so a total box size of 8 means the payload is 0 bytes.

How They Work Together During Normal Playback

moov in the init segment
  └─ Provides codec, track ID, timescale, and other global metadata

moof in the media segment
  └─ trun.sample_count = N
     ├─ Describes the timing and size of N samples
     └─ Maps to the corresponding N AAC payloads in mdat

mdat
  └─ Provides the actual AAC bytes

Player
  └─ Parse timeline from moof → read samples from mdat → append to SourceBuffer → decode

Think of moof as the index and playback instructions, mdat as the actual media content, and trun.sample_count as the number of playable entries in the index. The browser can build a continuous audio timeline only when the index and content agree.

The invalid chunk is contradictory: it provides a moof/mdat shell, presenting itself as a media fragment, while the index declares sample_count = 0 and the data area contains no payload. At the same time, the HLS playlist declares one second of audio with #EXTINF:1. The player therefore receives conflicting timeline and media information.

Expected vs. Actual Output

Valid CMAF audio fragment

  • trun.sample_count > 0
  • mdat contains real AAC sample payload
  • The segment time range matches the playlist's #EXTINF
  • The player can parse and append it to the audio SourceBuffer

Current invalid output

  • The playlist declares a one-second audio segment
  • A moof/mdat shell exists
  • sample_count = 0
  • No AAC payload exists to satisfy the playlist declaration

Why This Is an HLS Encoding/Packaging Issue

The chunk downloaded from the CDN is already the empty 120-byte media fragment described above. The defect exists before the data enters ViewMaster; the player did not remove AAC data during transport or parsing.

The final output proves that the HLS pipeline emitted a regular media segment with no samples. The output alone cannot determine whether the AAC encoder failed to emit the final frames or whether the muxer/packager created an empty segment while aligning durations, flushing the encoder, or cutting the final segment.

This can be described externally as an HLS encoding issue. For the pipeline owner, the more precise investigation area is audio tail encoding + CMAF muxing/packaging.

Likely Generation Path

  1. The video timeline is approximately one second longer than the available audio samples.
  2. The packager creates a final one-second segment to align the audio playlist with the video timeline.
  3. No AAC frame exists in that range, or encoder flush does not pass the final frames to the muxer.
  4. The muxer still writes moof and an empty mdat, then adds the URI to the playlist.
  5. The player receives a chunk that is structurally fragment-like but contains no media.

This is the most likely inference from the output. Encoding pipeline logs and source-media durations are required to identify the exact layer where the defect occurs.

Required HLS Encoding/Packaging Fixes

  1. Do not emit regular media segments with zero samples: for every non-gap audio chunk, require trun.sample_count > 0 and a non-empty mdat payload.
  2. Verify tail flush behavior: ensure the AAC encoder passes its final frames to the muxer and correctly handles encoder delay and padding.
  3. Verify audio/video duration alignment: do not create a final audio fragment with no audio samples solely to match the video timeline.
  4. Represent missing media correctly: if no audio exists in the time range, omit the segment, insert silent AAC samples, or use a valid #EXT-X-GAP instead of emitting an empty moof/mdat.
  5. Add output validation: scan every CMAF audio chunk before CDN upload and reject outputs where the playlist declares media but the chunk contains no samples.

Verification Commands

Inspect CMAF boxes

curl -sL \
  "https://cdn.liftoff-creatives.io/customers/9dbbcce438/video/hls-2-sec-QVBR/125_35bc4254b1c8229207a8/35bc4254b1c8229207a8_a_000000007.cmfa" \
  | xxd

Confirm that no audio packets exist

BASE="https://cdn.liftoff-creatives.io/customers/9dbbcce438/video/hls-2-sec-QVBR/125_35bc4254b1c8229207a8/35bc4254b1c8229207a8"

curl -sL "${BASE}_ainit.cmfa" -o /tmp/audio-init.cmfa
curl -sL "${BASE}_a_000000007.cmfa" -o /tmp/audio-chunk.cmfa
cat /tmp/audio-init.cmfa /tmp/audio-chunk.cmfa > /tmp/audio.mp4

ffprobe -v error \
  -select_streams a \
  -show_entries packet=pts_time,duration_time,size \
  -of json \
  /tmp/audio.mp4

The result is "packets": [], confirming that the chunk contains no parseable AAC samples.

Player-Side Status

ViewMaster drops the invalid audio track when it identifies this audio parsing error to prevent Safari from black-screening. This is client-side fault tolerance, not a root-cause fix. The root solution is for the encoding/packaging pipeline to stop generating empty media-fragment shells.