blob: a728ea2ce7b22dd4b3f5e538792e9bb9e35106e7 [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 struct
import tempfile
import unittest
from pathlib import Path
from pypaimon.common.delta_varint_compressor import DeltaVarintCompressor
from pypaimon.common.options import Options
from pypaimon.filesystem.local_file_io import LocalFileIO
from pypaimon.execution.python.read.reader.format_blob_reader import FormatBlobReader
from pypaimon.execution.python.read.reader.video_format_reader import VideoFileMeta
from pypaimon.schema.data_types import AtomicType, DataField
from pypaimon.table.row.blob import (
Blob,
BlobData,
BlobDescriptor,
VideoFrameDescriptor,
)
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.table.row.row_kind import RowKind
from pypaimon.execution.python.write.video_format_writer import VideoFormatWriter
class VideoFormatTest(unittest.TestCase):
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
DESCRIPTOR_FIXTURE = (
REPOSITORY_ROOT
/ "paimon-common/src/test/resources/org/apache/paimon/data/"
"video-frame-descriptor-v1.hex"
)
VIDEO_FIXTURE = (
REPOSITORY_ROOT
/ "paimon-format/src/test/resources/org/apache/paimon/format/blob/"
"video-v1.hex"
)
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.file_io = LocalFileIO(str(self.root), Options({}))
self.field = DataField(0, "video", AtomicType("BLOB"))
def tearDown(self):
self.temp_dir.cleanup()
def test_descriptor_round_trip_preserves_payload_and_frame(self):
descriptor = VideoFrameDescriptor("s3://bucket/a.video", 7, 99, 42)
serialized = descriptor.serialize()
self.assertTrue(
VideoFrameDescriptor.is_video_frame_descriptor(serialized)
)
self.assertFalse(BlobDescriptor.is_blob_descriptor(serialized))
self.assertEqual(descriptor, BlobDescriptor.deserialize(serialized))
self.assertEqual(descriptor, VideoFrameDescriptor.deserialize(serialized))
self.assertEqual(
BlobDescriptor("s3://bucket/a.video", 7, 99),
descriptor.payload_descriptor,
)
restored = Blob.from_bytes(serialized, file_io=self.file_io)
self.assertIsInstance(restored.to_descriptor(), VideoFrameDescriptor)
self.assertEqual(descriptor, restored.to_descriptor())
with self.assertRaisesRegex(ValueError, "trailing bytes"):
VideoFrameDescriptor.deserialize(serialized + b"x")
with self.assertRaisesRegex(ValueError, "non-negative"):
VideoFrameDescriptor("x", 0, 1, -1)
def test_cross_language_descriptor_fixture(self):
fixture = self._fixture_bytes(self.DESCRIPTOR_FIXTURE)
expected = VideoFrameDescriptor("s3://bucket/视频.mp4", 7, 99, 42)
self.assertEqual(fixture, expected.serialize())
self.assertEqual(expected, BlobDescriptor.deserialize(fixture))
def test_pack_raw_videos_and_map_frame_runs(self):
first_bytes = b"first-mp4"
second_bytes = b"second-mp4"
first0 = self._source_frame("first.mp4", first_bytes, 0)
first1 = self._source_frame("first.mp4", first_bytes, 1)
second7 = self._source_frame("second.mp4", second_bytes, 7)
first4 = self._source_frame("first.mp4", first_bytes, 4)
target = (self.root / "data.video").as_uri()
writer = VideoFormatWriter(
self.file_io.new_output_stream(target), file_path=target
)
for value in (first0, first1, second7, first4, None, Blob.PLACE_HOLDER):
writer.add_element(
GenericRow([value], [self.field], RowKind.INSERT)
)
self.assertEqual(2, writer.physical_video_count)
self.assertEqual(5, writer.run_count)
writer.close()
stored = (self.root / "data.video").read_bytes()
self.assertTrue(stored.startswith(first_bytes + second_bytes))
with self.file_io.new_input_stream(target) as stream:
meta = VideoFileMeta(stream, len(stored))
self.assertEqual(6, meta.record_count)
self.assertEqual((0, len(first_bytes), 0), meta.frame(0))
self.assertEqual((0, len(first_bytes), 1), meta.frame(1))
self.assertEqual(
(len(first_bytes), len(second_bytes), 7), meta.frame(2)
)
self.assertEqual((0, len(first_bytes), 4), meta.frame(3))
self.assertIsNone(meta.frame(4))
self.assertIs(Blob.PLACE_HOLDER, meta.frame(5))
values = self._read(target, row_indices=range(5))
frames = [VideoFrameDescriptor.deserialize(value) for value in values[:4]]
self.assertEqual([0, 1, 7, 4], [frame.frame_index for frame in frames])
self.assertEqual(frames[0].payload_descriptor, frames[1].payload_descriptor)
self.assertEqual(frames[0].payload_descriptor, frames[3].payload_descriptor)
self.assertNotEqual(frames[0].payload_descriptor, frames[2].payload_descriptor)
self.assertIsNone(values[4])
def test_cross_language_video_v1_fixture(self):
fixture = self._fixture_bytes(self.VIDEO_FIXTURE)
fixture_path = self.root / "fixture.video"
fixture_path.write_bytes(fixture)
target = fixture_path.as_uri()
with self.file_io.new_input_stream(target) as stream:
meta = VideoFileMeta(stream, len(fixture))
self.assertEqual(7, meta.record_count)
self.assertEqual((0, 3, 2), meta.frame(0))
self.assertEqual((0, 3, 3), meta.frame(1))
self.assertIsNone(meta.frame(2))
self.assertIs(Blob.PLACE_HOLDER, meta.frame(3))
self.assertEqual((3, 4, 7), meta.frame(4))
self.assertEqual((3, 4, 8), meta.frame(5))
self.assertEqual((0, 3, 10), meta.frame(6))
written_target = (self.root / "written.video").as_uri()
writer = VideoFormatWriter(
self.file_io.new_output_stream(written_target),
file_path=written_target,
)
values = (
self._source_frame("a.mp4", b"abc", 2),
self._source_frame("a.mp4", b"abc", 3),
None,
Blob.PLACE_HOLDER,
self._source_frame("b.mp4", b"WXYZ", 7),
self._source_frame("b.mp4", b"WXYZ", 8),
self._source_frame("a.mp4", b"abc", 10),
)
for value in values:
writer.add_element(GenericRow([value], [self.field], RowKind.INSERT))
writer.close()
self.assertEqual(fixture, (self.root / "written.video").read_bytes())
def test_moov_fixture_and_invalid_ranges(self):
import io
fixture = self._fixture_bytes(self.VIDEO_FIXTURE.with_name('video-v1-moov.hex'))
meta = VideoFileMeta(io.BytesIO(fixture), len(fixture))
self.assertEqual((16, 8), meta.moov_range(0))
self.assertEqual((0, 24, 7), meta.frame(0))
self.assertEqual((0, 24, 8), meta.frame(1))
for offset, size in [(-1, 8), (1, 0), (24, 1), (16, 9)]:
with self.assertRaisesRegex(ValueError, 'Moov range'):
VideoFrameDescriptor('x', 0, 24, 0, offset, size)
# Index array counts and bounds are checked before returning any row.
for offsets, sizes in [([], [8]), ([16], []), ([17], [8]), ([1], [0])]:
indexes = [DeltaVarintCompressor.compress(x)
for x in ([24], offsets, sizes, [1], [0], [0])]
raw = fixture[:24] + b''.join(indexes) + struct.pack(
'<IIIIIIIB', *(len(x) for x in indexes), 0x4F454449, 1)
with self.assertRaises(IOError):
VideoFileMeta(io.BytesIO(raw), len(raw))
def test_selection_keeps_logical_frame_positions(self):
target = (self.root / "selection.video").as_uri()
writer = VideoFormatWriter(self.file_io.new_output_stream(target))
source = [
self._source_frame("selection.mp4", b"video", frame)
for frame in range(4)
]
for value in source:
writer.add_element(GenericRow([value], [self.field], RowKind.INSERT))
writer.close()
values = self._read(target, row_indices=[1, 3])
frames = [VideoFrameDescriptor.deserialize(value) for value in values]
self.assertEqual([1, 3], [frame.frame_index for frame in frames])
def test_reader_exposes_record_count_without_blob_indexes(self):
target = (self.root / "record-count.video").as_uri()
writer = VideoFormatWriter(self.file_io.new_output_stream(target))
for frame_index in range(4):
frame = self._source_frame(
"record-count.mp4", b"video", frame_index
)
writer.add_element(
GenericRow([frame], [self.field], RowKind.INSERT)
)
writer.close()
reader = FormatBlobReader(
file_io=self.file_io,
file_path=target,
read_fields=["video"],
full_fields=[self.field],
push_down_predicate=None,
blob_as_descriptor=True,
row_indices=[1, 3],
)
try:
self.assertEqual(2, reader.record_count)
self.assertEqual([], reader.blob_lengths)
self.assertEqual([], reader.blob_offsets)
values = reader.read_values_at([0, 1])
self.assertEqual(
[1, 3],
[value.to_descriptor().frame_index for value in values],
)
finally:
reader.close()
def test_rejects_non_video_frame_input(self):
target = (self.root / "reject.video").as_uri()
writer = VideoFormatWriter(self.file_io.new_output_stream(target))
for value in (
BlobData(b"inline"),
self._source_blob("ordinary.mp4", b"ordinary"),
):
with self.subTest(value=value):
with self.assertRaisesRegex(ValueError, "VideoFrameDescriptor"):
writer.add_element(
GenericRow([value], [self.field], RowKind.INSERT)
)
writer.close()
def test_rejects_out_of_range_run_reference(self):
target_path = self.root / "corrupt.video"
indexes = [
DeltaVarintCompressor.compress([]),
DeltaVarintCompressor.compress([]),
DeltaVarintCompressor.compress([]),
DeltaVarintCompressor.compress([1]),
DeltaVarintCompressor.compress([0]),
DeltaVarintCompressor.compress([0]),
]
target_path.write_bytes(
b"".join(indexes)
+ struct.pack(
'<IIIIIIIB',
*(len(index) for index in indexes),
VideoFormatWriter.FOOTER_MAGIC_NUMBER,
VideoFormatWriter.VERSION,
)
)
with self.assertRaisesRegex(IOError, "physical video count is 0"):
FormatBlobReader(
file_io=self.file_io,
file_path=target_path.as_uri(),
read_fields=["video"],
full_fields=[self.field],
push_down_predicate=None,
blob_as_descriptor=True,
)
def _read(self, target, row_indices=None):
reader = FormatBlobReader(
file_io=self.file_io,
file_path=target,
read_fields=["video"],
full_fields=[self.field],
push_down_predicate=None,
blob_as_descriptor=False,
row_indices=row_indices,
)
try:
return reader.read_arrow_batch().column(0).to_pylist()
finally:
reader.close()
def _source_blob(self, name, data):
source = self.root / name
if not source.exists():
source.write_bytes(data)
return Blob.from_file(self.file_io, source.as_uri(), 0, len(data))
def _source_frame(self, name, data, frame_index):
source = self._source_blob(name, data)
payload = source.to_descriptor()
descriptor = VideoFrameDescriptor(
payload.uri, payload.offset, payload.length, frame_index
)
return Blob.from_descriptor(
self.file_io.uri_reader_factory.create(descriptor.uri), descriptor
)
@staticmethod
def _fixture_bytes(path):
hex_value = "".join(
line for line in path.read_text().splitlines()
if not line.startswith("#")
)
return bytes.fromhex(hex_value)
if __name__ == '__main__':
unittest.main()