An event that closes on a keyframe ends in a fragment holding that frame
alone. The muxer gives the file's last sample the packet's duration, which
camera packets leave at 0, so the last sidx reference would last 0 ticks
and build_sidx_region refused the whole index ("Could not index ...; it
will play, but a player must read it all before the first frame").
Fold a zero-length last reference into the one before it. That reference
still points at a real moof, so seeking is unchanged. A lone zero-length
fragment, or one mid-file, is still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5174 reports an event's m3u8 as invalid and proposes moving every fragment's
byte range 8 bytes further into the file, from @17203 to @17211.
That reading comes from ffprobe's trace, whose second column is the offset of
the box *body* -- the start plus the 8 byte box header -- not the start. In
the manifest quoted there the first fragment is 1434876@17203, and the trace
has the moof body at 17211 and the mdat ending at 1452079, so the range spans
exactly moof(264) + mdat(1434612) = 1434876 from the start of the moof. The
proposed change would cut the moof header off every segment.
The manifest is not contiguous, which is what draws the eye: the init range
is ftyp+moov and the fragments start 16KB later, because reserve_region puts
the leading sidx in between and the sidx has to END where the fragments
BEGIN. Nothing fetches those bytes and HLS does not require byte ranges to
abut.
So assert it rather than argue it. Against sidx-moof.mp4, using the box
walker the sidx tests already keep for the purpose, check that a fragment
starts at its moof box rather than 8 bytes in, that fragments run back to
back, that the init range is the header boxes and nothing else, and that the
gap between them is exactly the reserved index.
This says nothing about the browser error that prompted the report, which is
not quoted in the issue; it only rules the byte ranges in or out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
finalize() located the mfra by reading the trailing mfro with fopen and
fseeko and decoding it by hand, the same job zm_mp4::media_end() does for
the sidx scan. Use media_end() for both, so the last HLS fragment and the
index agree on where the media ends.
media_end() is also stricter: it requires an mfra box of the stated size
at the offset the mfro points to, where the old code accepted any size up
to the file length. A trailer that does not check out now leaves the final
fragment running to EOF, as a missing trailer already did.
A new test covers media_end() against the fixture's real mfra and three
damaged trailers: an mfro size off by four, one larger than the file, and
no mfro at all.
refs #5144
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_sidx_region() wrote starts_with_SAP=1, SAP type 1 into every
reference. That holds for frag_keyframe recordings, where every fragment
opens on a keyframe, but a monitor whose encoder options add frag_duration
or frag_size gets fragments cut mid-GOP, and the index then claimed a
decodable start where there was none.
scan_fragments() now reads the sync flag of each fragment's first video
sample: trun first_sample_flags, else the first entry's sample_flags, else
the tfhd default_sample_flags, else the trex default (read_video_track()
now keeps it). A reference with no SAP gets 0 for starts_with_SAP, type and
delta; a merged reference takes the flag of its first fragment. parse_traf()
also skips tfhd default_sample_size, which it never needed before.
Tests: remuxing the fixture with frag_keyframe and a 250 ms frag_duration
gives about a dozen references, of which exactly the three that begin at a
keyframe are SAPs; the frag_keyframe fixture scans as all SAPs, and the
byte-for-byte comparison with the reference tool is unchanged; merging takes
the first fragment's flag. The remux sequence the muxer test used is now a
helper shared by both.
refs #5144
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every fragmented event reserved 64 KiB for its leading sidx, whatever its
length. On a short motion clip of 1-2 MB that is 3-6% of the file.
zm_mp4::reserve_size() sizes the region for twice the fragments expected
in one section: section length divided by the GOP, which is the encoder's
gop_size when encoding and the packet queue's longest keyframe interval
otherwise, over the capture fps. It rounds up to whole 4 KiB blocks and
stays between 4 KiB (337 references) and the previous 64 KiB, which is
still taken when the section length, GOP or fps is unknown. The default
600 s section at a 1 s GOP now reserves 16 KiB.
A low guess does not lose the index: an event with more fragments than
the region holds has neighbouring fragments merged into one reference,
as before, which only coarsens seeking. VideoStore keeps the size it
reserved so finalize() fills that exact region, and Monitor gains a
GetSectionLength() accessor.
refs #5144
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VideoStore::open() reserves a 64 KiB free box between moov and the first
fragment, but only when the MOV muxer wrote the moov in the header and
moof+mdat fragments follow it. finalize() scans the fragments and fills the
region with free padding followed by a sidx that ends at the first moof, so
FFmpeg-based players (Chromium, Android WebView, Electron) treat the index as
complete and start playback without visiting every fragment. On any parse or
write failure the region stays a free box.
fixes#5144