blob: ffce9417f6ad8635b38c28e0af42268998e4c7b5 [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
from pypaimon.common.delta_varint_compressor import DeltaVarintCompressor
from pypaimon.schema.data_types import is_blob_type
from pypaimon.table.row.blob import (
Blob,
BlobRef,
VideoFrameDescriptor,
)
from pypaimon.write.blob_format_writer import BlobFormatWriter
class VideoFormatWriter(BlobFormatWriter):
"""Pack complete encoded videos and an embedded logical frame-run index."""
VERSION = 1
FOOTER_MAGIC_NUMBER = 0x4F454449
FOOTER_SIZE = 21
NULL_REFERENCE = -1
PLACE_HOLDER_REFERENCE = -2
def __init__(
self,
output_stream,
file_path=None,
copy_buffer_size=BlobFormatWriter.BUFFER_SIZE):
super().__init__(
output_stream,
blob_consumer=None,
file_path=file_path,
copy_buffer_size=copy_buffer_size,
)
self._physical_lengths = []
self._physical_videos = {}
self._run_lengths = []
self._run_references = []
self._run_first_frames = []
self._current_run_length = 0
self._current_run_reference = None
self._current_run_first_frame = 0
self._current_run_last_frame = 0
self._closed = False
def add_element(self, row) -> None:
if not hasattr(row, 'values') or len(row.values) != 1:
raise ValueError("VideoFormatWriter only supports one field")
if not is_blob_type(row.fields[0].type):
raise ValueError(
"VideoFormatWriter only supports one scalar BLOB field"
)
value = row.values[0]
if value is None:
self._append(self.NULL_REFERENCE, 0)
return
if value is Blob.PLACE_HOLDER:
self._append(self.PLACE_HOLDER_REFERENCE, 0)
return
if type(value) is not BlobRef:
raise ValueError(
"Video fields require an exact BlobRef containing a "
"VideoFrameDescriptor."
)
frame = value.to_descriptor()
if not isinstance(frame, VideoFrameDescriptor):
raise ValueError(
"Video fields require an exact BlobRef containing a "
"VideoFrameDescriptor."
)
payload = frame.payload_descriptor
ordinal = self._physical_videos.get(payload)
if ordinal is None:
length = self._write_video_payload(value)
ordinal = len(self._physical_lengths)
self._physical_lengths.append(length)
self._physical_videos[payload] = ordinal
self._append(ordinal, frame.frame_index)
@property
def physical_video_count(self) -> int:
return len(self._physical_lengths)
@property
def run_count(self) -> int:
return len(self._run_lengths) + (1 if self._current_run_length else 0)
def close(self) -> None:
if self._closed:
return
self._flush_run()
physical_index = DeltaVarintCompressor.compress(self._physical_lengths)
run_length_index = DeltaVarintCompressor.compress(self._run_lengths)
run_reference_index = DeltaVarintCompressor.compress(
self._run_references
)
first_frame_index = DeltaVarintCompressor.compress(
self._run_first_frames
)
for index in (
physical_index,
run_length_index,
run_reference_index,
first_frame_index,
):
self.output_stream.write(index)
self.output_stream.write(struct.pack(
'<IIIIIB',
len(physical_index),
len(run_length_index),
len(run_reference_index),
len(first_frame_index),
self.FOOTER_MAGIC_NUMBER,
self.VERSION,
))
if hasattr(self.output_stream, 'flush'):
self.output_stream.flush()
if hasattr(self.output_stream, 'close'):
self.output_stream.close()
self._closed = True
def _write_video_payload(self, blob: BlobRef) -> int:
start = self.position
stream = blob.new_input_stream()
try:
while True:
chunk = stream.read(self.copy_buffer_size)
if not chunk:
break
self.output_stream.write(chunk)
self.position += len(chunk)
finally:
stream.close()
length = self.position - start
if length <= 0:
raise ValueError("Encoded video payload must not be empty.")
return length
def _append(self, reference: int, frame_index: int) -> None:
if self._can_extend(reference, frame_index):
self._current_run_length += 1
self._current_run_last_frame = frame_index
return
self._flush_run()
self._current_run_reference = reference
self._current_run_first_frame = frame_index
self._current_run_last_frame = frame_index
self._current_run_length = 1
def _can_extend(self, reference: int, frame_index: int) -> bool:
if (
self._current_run_length == 0
or self._current_run_reference != reference
):
return False
return (
reference < 0
or frame_index == self._current_run_last_frame + 1
)
def _flush_run(self) -> None:
if self._current_run_length == 0:
return
self._run_lengths.append(self._current_run_length)
self._run_references.append(self._current_run_reference)
self._run_first_frames.append(self._current_run_first_frame)
self._current_run_length = 0