blob: 09d5ba4389071a775aaa7407d14e14d2b198807c [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import io
import struct
import weakref
from unittest.mock import patch
import numpy as np
import pytest
from pypaimon.multimodal.mp4 import ContainerFrameIndex
from pypaimon.tests import indexed_torchcodec_test
mp4 = indexed_torchcodec_test.mp4
def test_container_locations_match_decoded_keyframes(mp4):
import av
mapping = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
with av.open(io.BytesIO(mp4)) as source:
frames = list(source.decode(video=0))
expected = [(i, frame.pts) for i, frame in enumerate(frames) if frame.key_frame]
assert mapping.keyframes[:, :2].tolist() == [list(entry) for entry in expected]
np.testing.assert_array_equal(mapping.pts[list(range(len(frames)))], [f.pts for f in frames])
with av.open(io.BytesIO(mp4)) as source:
packets = {packet.pts: packet.pos for packet in source.demux(video=0) if packet.is_keyframe}
assert [packets[int(pts)] for _, pts, _ in mapping.keyframes] == mapping.keyframes[:, 2].tolist()
def test_container_reader_avoids_packet_scan_and_shares_mapping(mp4):
import av
from pypaimon.multimodal.lerobot.dataset import _open_video_decoder
with av.open(io.BytesIO(mp4)) as source:
expected = [frame.to_ndarray(format="rgb24") for frame in source.decode(video=0)]
cache = weakref.WeakValueDictionary()
readers = []
try:
for unused in range(2):
stream = io.BytesIO(mp4)
stream.video_length = len(mp4)
stream.video_metadata_cache = cache
stream.video_metadata_key = 'same-video'
stream.video_read_ranges = lambda ranges: [mp4[o:o + n] for o, n in ranges]
decoder = _open_video_decoder(stream, backend="pyav")
readers.append(decoder)
assert decoder._keyframe_index is not None
decoder._tensor = lambda frame: frame.to_ndarray(format="rgb24")
with patch.object(decoder, '_index_packets', side_effect=AssertionError('packet scan')):
for ordinal in [59, 0, 31, 12, 1, 48]:
np.testing.assert_array_equal(decoder[ordinal], expected[ordinal])
assert readers[0]._keyframe_index is readers[1]._keyframe_index
finally:
for decoder in readers:
decoder.close()
def test_chunk_extent_is_checked_before_decode(mp4):
data = bytearray(mp4)
position = data.index(b'stco')
struct.pack_into('>I', data, position + 12, len(data) + 1)
with pytest.raises(ValueError, match='exceeds video payload'):
ContainerFrameIndex(io.BytesIO(data), len(data))
def test_missing_sample_locations_are_rejected():
from pypaimon.tests.indexed_torchcodec_test import timing_only_mp4
data = timing_only_mp4(100)
with pytest.raises(ValueError, match='chunk offsets'):
ContainerFrameIndex(io.BytesIO(data), len(data))
def test_pyav_timestamp_mismatch_falls_back_to_scan(mp4):
import av
from pypaimon.multimodal.lerobot.dataset import _PyAVVideoDecoder
from pypaimon.multimodal.mp4 import _TimingValues
mapping = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
timestamps = mapping.pts[list(range(len(mapping.pts)))]
timestamps[31] += 1
mapping.pts = _TimingValues(timestamps)
with av.open(io.BytesIO(mp4)) as container:
expected = list(container.decode(video=0))[31].to_ndarray(format="rgb24")
decoder = _PyAVVideoDecoder(io.BytesIO(mp4), mapping)
decoder._tensor = lambda frame: frame.to_ndarray(format="rgb24")
try:
first = decoder[0]
np.testing.assert_array_equal(expected, decoder[31])
assert decoder._keyframe_index is None
np.testing.assert_array_equal(first, decoder[0])
finally:
decoder.close()
def test_stored_moov_skips_top_level_walk(mp4):
from pypaimon.common.mp4_moov import MoovLocator
locator = MoovLocator()
for offset in range(0, len(mp4), 7):
locator.feed(mp4[offset:offset + 7])
hint = locator.finish()
assert hint[1] > 0
expected = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
class Source(io.BytesIO):
def read(self, size=-1):
assert self.tell() >= hint[0]
return super().read(size)
source = Source(mp4)
source.video_moov_range = hint
actual = ContainerFrameIndex(source, len(mp4))
np.testing.assert_array_equal(actual.keyframes, expected.keyframes)
assert actual.metadata_ranges == (hint,)
@pytest.mark.parametrize('hint', [(0, 8), (1, 8), (0, 2**63 - 1)])
def test_bad_moov_hint_falls_back_to_box_walk(mp4, hint):
source = io.BytesIO(mp4)
source.video_moov_range = hint
actual = ContainerFrameIndex(source, len(mp4))
expected = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
np.testing.assert_array_equal(actual.keyframes, expected.keyframes)
def test_writer_retains_moov_on_rewrite(mp4, tmp_path):
from pypaimon.common.options import Options
from pypaimon.execution.python.write.video_format_writer import VideoFormatWriter
from pypaimon.execution.python.read.reader.video_format_reader import VideoFileMeta
from pypaimon.filesystem.local_file_io import LocalFileIO
from pypaimon.table.row.blob import Blob, VideoFrameDescriptor
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.schema.data_types import AtomicType, DataField
file_io = LocalFileIO(str(tmp_path), Options({}))
source = tmp_path / 'source.mp4'
source.write_bytes(mp4)
frame = VideoFrameDescriptor(source.as_uri(), 0, len(mp4), 7)
expected = ContainerFrameIndex(io.BytesIO(mp4), len(mp4)).metadata_ranges[0]
for name in ('first.video', 'rewrite.video'):
target = tmp_path / name
writer = VideoFormatWriter(file_io.new_output_stream(target.as_uri()))
blob = Blob.from_descriptor(file_io.uri_reader_factory.create(frame.uri), frame)
writer.add_element(GenericRow([blob], [DataField(0, 'video', AtomicType('BLOB'))]))
writer.close()
raw = target.read_bytes()
assert raw[:len(mp4)] == mp4
meta = VideoFileMeta(io.BytesIO(raw), len(raw))
assert meta.frame(0) == (0, len(mp4), 7)
assert meta.moov_range(0) == expected
frame = VideoFrameDescriptor(target.as_uri(), *meta.frame(0), *meta.moov_range(0))
assert VideoFrameDescriptor.deserialize(frame.serialize()) == frame
@pytest.mark.parametrize('data', [b'abc', struct.pack('>I4s', 99, b'moov'),
struct.pack('>I4s', 8, b'moov') * 2])
def test_moov_locator_rejects_malformed_or_duplicate_boxes(data):
from pypaimon.common.mp4_moov import MoovLocator
locator = MoovLocator()
locator.feed(data)
assert locator.finish() == (0, 0)
@pytest.mark.parametrize("backend", ["pyav", "torchcodec"])
def test_stored_moov_bootstrap_decodes_without_refetch(mp4, backend):
pytest.importorskip("torch")
import av
indexed = True
if backend == "torchcodec":
from pypaimon.tests.indexed_torchcodec_test import require_torchcodec, supports_file_like
require_torchcodec()
indexed = supports_file_like()
from pypaimon.multimodal.lerobot.dataset import _open_video_decoder
mapping = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
hint = mapping.metadata_ranges[0]
calls = []
source = io.BytesIO(mp4)
source.video_length = len(mp4)
source.video_moov_range = hint
def ranges(regions):
calls.append(regions)
return [mp4[o:o + n] for o, n in regions]
source.video_read_ranges = ranges
with av.open(io.BytesIO(mp4)) as container:
expected = list(container.decode(video=0))[31].to_ndarray(format="rgb24")
decoder = _open_video_decoder(source, backend=backend)
try:
if backend == "pyav":
assert decoder._keyframe_index is not None
else:
assert decoder._indexed is indexed
frame = decoder.get_frames_at(indices=[31]).data[0].permute(1, 2, 0).numpy()
np.testing.assert_array_equal(frame, expected)
assert calls
assert any(o <= hint[0] and o + n >= sum(hint) for o, n in calls[0])
if indexed:
assert not any(o < sum(hint) and o + n > hint[0]
for request in calls[1:] for o, n in request)
finally:
decoder.close()
def test_first_sync_sample_must_cover_first_frame(mp4):
data = bytearray(mp4)
position = data.index(b'stss')
struct.pack_into('>I', data, position + 12, 2)
with pytest.raises(ValueError, match='keyframe order'):
ContainerFrameIndex(io.BytesIO(data), len(data))
def test_transient_io_and_invalid_frame_keep_mapping(mp4):
pytest.importorskip("torch")
from pypaimon.multimodal.lerobot.dataset import _PyAVVideoDecoder
mapping = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
decoder = _PyAVVideoDecoder(io.BytesIO(mp4), mapping)
try:
with patch.object(decoder, '_read_video_ranges', side_effect=OSError('temporary')):
with pytest.raises(OSError, match='temporary'):
decoder.get_frames_at(indices=[31])
assert decoder._keyframe_index is mapping
with patch.object(decoder, '_decode_indexed', side_effect=AssertionError('decode')):
with pytest.raises(IndexError):
decoder.get_frames_at(indices=[len(mapping.pts)])
assert decoder._keyframe_index is mapping
assert decoder.get_frames_at(indices=[31]).data.shape[0] == 1
finally:
decoder.close()
def test_open_runtime_error_falls_back(mp4):
pytest.importorskip("torch")
from pypaimon.multimodal.lerobot.dataset import _open_video_decoder
source = io.BytesIO(mp4)
source.video_length = len(mp4)
with patch('pypaimon.multimodal.lerobot.dataset.ContainerFrameIndex',
side_effect=RuntimeError('initialization')):
decoder = _open_video_decoder(source, backend='pyav')
try:
assert decoder._keyframe_index is None
assert decoder.get_frames_at(indices=[0]).data.shape[0] == 1
finally:
decoder.close()
def test_cached_mapping_prefetches_header_and_moov(mp4):
pytest.importorskip("torch")
from pypaimon.multimodal.lerobot.dataset import _open_video_decoder
mapping = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
calls = []
source = io.BytesIO(mp4)
source.video_length = len(mp4)
source.video_metadata_key = 'video'
source.video_metadata_cache = {('container', 'video'): mapping}
def ranges(regions):
calls.append(regions)
return [mp4[o:o + n] for o, n in regions]
source.video_read_ranges = ranges
with patch('pypaimon.multimodal.lerobot.dataset.ContainerFrameIndex',
side_effect=AssertionError('reparse')):
decoder = _open_video_decoder(source, backend='pyav')
try:
decoder.get_frames_at(indices=[31])
assert calls[0][0][0] == 0
offset, length = mapping.metadata_ranges[0]
assert any(o <= offset and o + n >= offset + length for o, n in calls[0])
finally:
decoder.close()
def test_metadata_cache_retention_and_budget(mp4):
import gc
from pypaimon.multimodal.video import _VideoMetadataCache
mapping = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
size = mapping.nbytes
cache = _VideoMetadataCache(max_bytes=size, max_entries=1)
cache['first'] = mapping
reference = weakref.ref(mapping)
del mapping
gc.collect()
assert cache.get('first') is reference()
assert reference() is not None
cache['second'] = ContainerFrameIndex(io.BytesIO(mp4), len(mp4))
gc.collect()
assert reference() is None
assert cache._bytes <= size
cache.clear()
assert len(cache) == 0
def test_writer_close_failure_is_not_retried():
from pypaimon.execution.python.write.video_format_writer import VideoFormatWriter
class FailingOutput(io.BytesIO):
writes = 0
def write(self, data):
self.writes += 1
raise OSError('footer write')
output = FailingOutput()
writer = VideoFormatWriter(output)
with pytest.raises(OSError, match='footer write'):
writer.close()
assert output.closed
writes = output.writes
writer.close()
assert output.writes == writes
@pytest.mark.parametrize('wide', [False, True])
def test_vectorized_chunk_offsets(wide):
from pypaimon.multimodal.mp4 import _sample_offsets
def box(kind, content):
return struct.pack('>I4s', len(content) + 8, kind) + content
sizes = [3, 5, 7, 11, 13, 17, 19]
offsets = [100, 200, 350]
samples = box(b'stsz', struct.pack('>III7I', 0, 0, 7, *sizes))
samples += box(b'co64' if wide else b'stco', struct.pack(
'>II3' + ('Q' if wide else 'I'), 0, 3, *offsets))
samples += box(b'stsc', struct.pack('>II6I', 0, 2, 1, 2, 1, 3, 3, 1))
np.testing.assert_array_equal(
_sample_offsets(samples, 7, 500), [100, 103, 200, 207, 350, 363, 380])
with pytest.raises(ValueError, match='exceeds video payload'):
_sample_offsets(samples, 7, 398)
def test_chunk_count_is_checked_before_expansion():
from pypaimon.multimodal.mp4 import _sample_offsets
def box(kind, content):
return struct.pack('>I4s', len(content) + 8, kind) + content
samples = box(b'stsz', struct.pack('>III', 0, 1, 1))
samples += box(b'co64', struct.pack('>IIQ', 0, 1, 2**64 - 1))
samples += box(b'stsc', struct.pack('>II3I', 0, 1, 1, 2**32 - 1, 1))
with pytest.raises(ValueError, match='cover samples'):
_sample_offsets(samples, 1, 100)