fix(sample): stop a malformed sidx from hanging the download

pymp4 loops forever on a sidx that counts more references than it holds. Manifest errors in download threads now raise instead of sys.exit.
This commit is contained in:
imSp4rky
2026-10-24 12:46:19 -06:00
parent 1036528fa3
commit bcef9ff87a
4 changed files with 18 additions and 11 deletions
+6
View File
@@ -345,6 +345,12 @@ def test_parse_sidx_refuses_what_it_cannot_cut() -> None:
assert parse_sidx(sidx_box(count=3)[:-4], 0) == (0, [])
def test_parse_sidx_refuses_a_box_that_counts_more_references_than_it_holds() -> None:
box = bytearray(sidx_box(count=3))
box[30:32] = (5).to_bytes(2, "big")
assert parse_sidx(bytes(box) + bytes(100), 0) == (0, [])
def test_a_single_file_dash_track_is_cut_by_its_segment_index(monkeypatch: pytest.MonkeyPatch) -> None:
box = sidx_box()
requested: list[str] = []
+4 -5
View File
@@ -6,7 +6,6 @@ import logging
import math
import re
import shutil
import sys
from concurrent.futures import Future
from contextlib import ExitStack, closing
from copy import deepcopy
@@ -17,6 +16,7 @@ from urllib.parse import urljoin, urlparse
from uuid import UUID
from zlib import crc32
import click
import requests
from langcodes import Language, tag_is_valid
from lxml.etree import Element, ElementTree, XMLSyntaxError
@@ -497,14 +497,13 @@ class DASH:
if config.sample and segments:
break
# raise, never sys.exit: this runs in a download thread, where SystemExit escapes the failure handler
if not segments:
log.error("Could not find a way to get segments from this MPD manifest.")
log.debug(track.url)
sys.exit(1)
raise click.ClickException("Could not find a way to get segments from this MPD manifest.")
if config.sample and not segment_durations and not isinstance(track, Subtitle):
log.error("--sample cannot cut this track: the manifest gives it as one file with no segment index.")
sys.exit(1)
raise click.ClickException("--sample cannot cut this track: the server sent no segment index for it.")
# TODO: Should we floor/ceil/round, or is int() ok?
track.data["dash"]["timescale"] = int(segment_timescale)
+3 -5
View File
@@ -8,7 +8,6 @@ import os
import re
import shutil
import subprocess
import sys
import time
from contextlib import ExitStack, closing
from itertools import accumulate
@@ -18,6 +17,7 @@ from urllib.parse import urljoin, urlparse
from uuid import UUID
from zlib import crc32
import click
import m3u8
import requests
from langcodes import Language, tag_is_valid
@@ -644,14 +644,12 @@ class HLS:
if not isinstance(response, (requests.Response, RnetResponse)):
raise TypeError(f"Expected response to be a requests.Response or rnet.Response, not {type(response)}")
if not response.ok:
log.error(f"Failed to request the invariant M3U8 playlist: {response.status_code}")
sys.exit(1)
raise click.ClickException(f"Failed to request the invariant M3U8 playlist: {response.status_code}")
master = m3u8.loads(utf8_text(response), uri=track.url)
if not master.segments:
log.error("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.")
sys.exit(1)
raise click.ClickException("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.")
unwanted_segments = [
segment for segment in master.segments if callable(track.OnSegmentFilter) and track.OnSegmentFilter(segment)
+5 -1
View File
@@ -360,11 +360,15 @@ def parse_sidx(data: bytes, offset: int) -> tuple[int, list[tuple[int, int, int]
Returns the timescale and one (start time, first byte, last byte) for each subsegment. The list
is empty when the box is not a ``sidx`` or when it points at more indexes and not at media.
"""
size, header = int.from_bytes(data[:4], "big"), 32 if data[8:9] == b"\x00" else 40
# pymp4 loops forever on a box that counts more references than its size holds
if data[4:8] != b"sidx" or size < header + 12 * int.from_bytes(data[header - 2 : header], "big"):
return 0, []
try:
box = Box.parse(data)
except ConstructError:
return 0, []
if box.type != b"sidx" or any(reference.reference_type != "MEDIA" for reference in box.references):
if any(reference.reference_type != "MEDIA" for reference in box.references):
return 0, []
start, byte = box.earliest_presentation_time, offset + box.end + box.first_offset
subsegments: list[tuple[int, int, int]] = []