fix(hls): enforce supported HLS subset with explicit errors

The merge pipeline concatenates stored bytes, so features it cannot
reproduce faithfully are now detected and rejected with a clear error
instead of silently succeeding with a broken output:

- EXT-X-GAP (a hole in the merged file) and EXT-X-DISCONTINUITY
  (a broken timeline)
- separate audio renditions referenced by the selected variant: the
  master EXT-X-MEDIA groups are parsed and an AUDIO group with its own
  URI fails the resolve; unreferenced groups are ignored
- changing init sections: a repeated EXT-X-MAP is compared by all of
  its attributes (URI and BYTERANGE), not just the URI
- encrypted EXT-X-MAP without an explicit IV: the spec does not define
  a sequence-derived IV for init sections
- non-identity AES-128 KEYFORMAT values

The supported matrix is documented in the package doc.

Add regression tests for every rejection plus real media validation:
ffmpeg-generated TS segments and fMP4 init/fragments are committed as
fixtures, downloaded through the fetcher (including an AES-128 key
rotation variant), and the merged outputs are validated structurally -
TS on the 188 byte grid with continuity counter rules, PAT/PMT and PES
presence, fMP4 by walking the ISO-BMFF box hierarchy.
This commit is contained in:
sun committed 2026-09-15 11:42:06 +08:00
1 parent 675028c64a
commit f1f17948c6
12 files changed
+611 -32

No files matched your search

+7 -4
View File
@@ -227,11 +227,14 @@ func (f *Fetcher) resolvePlanLocked(ctx context.Context, keepName string) (*fetc
mediaURL := finalURL
if IsMasterPlaylist(content) {
variants, err := ParseMaster(content, baseURL)
master, err := ParseMasterPlaylist(content, baseURL)
if err != nil {
return nil, "", err
}
best := PickBestVariant(variants)
best := PickBestVariant(master.Variants)
if err := checkAudioRendition(master, best); err != nil {
return nil, "", err
}
variantContent, variantURL, err := f.fetchText(ctx, http.MethodGet, best.URI)
if err != nil {
return nil, "", fmt.Errorf("fetch variant playlist failed: %w", err)
@@ -248,8 +251,8 @@ func (f *Fetcher) resolvePlanLocked(ctx context.Context, keepName string) (*fetc
if err != nil {
return nil, "", err
}
if media.Live {
return nil, "", errors.New("live streams are not supported yet")
if err := validateSupported(media); err != nil {
return nil, "", err
}
outputName := keepName