Files
2026-07-13 13:05:14 +08:00

176 lines
5.5 KiB
Python

"""Query video streams efficiently using keyframe information."""
from __future__ import annotations
import atexit
import pathlib
import shutil
import tempfile
TMP_DIR = pathlib.Path(tempfile.mkdtemp())
atexit.register(lambda: shutil.rmtree(TMP_DIR) if TMP_DIR.exists() else None)
# region: setup
from fractions import Fraction
from io import BytesIO
from pathlib import Path
import av
import numpy as np
import pyarrow as pa
from datafusion import col
from datafusion import functions as F
import rerun as rr
sample_video_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample"
)
server = rr.server.Server(datasets={"video_dataset": sample_video_path})
client = server.client()
dataset = client.get_dataset(name="video_dataset")
df = dataset.filter_contents(["/video_stream/**"]).reader(index="log_time")
times = pa.table(df.select("log_time"))["log_time"].to_numpy()
video_column = "/video_stream:VideoStream:sample"
# endregion: setup
# region: add_keyframe_column
# Preprocessing step: Add keyframe information to existing video data as a layer
# This is typically done once to make subsequent queries faster
# Query all video samples from the existing recording
video_samples_df = df.select("log_time", video_column)
video_table = pa.table(video_samples_df)
sample_times = video_table["log_time"].to_numpy()
samples = video_table[video_column].to_numpy()
# Concatenate all samples to analyze keyframes
sample_bytes = b""
for sample in samples:
sample_bytes += sample[0].tobytes()
# Decode the video to detect keyframes
data_buffer = BytesIO(sample_bytes)
container = av.open(data_buffer, format="h264", mode="r")
video_stream = container.streams.video[0]
# Identify which samples are keyframes
keyframe_times = []
for packet, ts in zip(container.demux(video_stream), sample_times):
if packet.is_keyframe:
keyframe_times.append(ts)
container.close()
keyframe_values = [True] * len(keyframe_times)
print(f"Found {len(keyframe_times)} keyframes")
# Save keyframe data as a separate layer
# Get the segment ID to align with the original recording
segment_ids = dataset.segment_ids()
first_segment_id = segment_ids[0]
# Create time column and content using the columnar API
# Make sure the timeline matches the original video stream
timeline = "log_time"
time_column = rr.TimeColumn(timeline=timeline, timestamp=keyframe_times)
content = rr.DynamicArchetype.columns(
archetype="KeyframeData", components={"is_keyframe": keyframe_values}
)
# Write to a new file as a layer
layer_path = TMP_DIR / "keyframe_layer.rrd"
with rr.RecordingStream(
application_id="keyframes",
recording_id=first_segment_id, # Match original recording_id
) as rec:
rec.save(layer_path)
rec.send_columns("/video_stream", indexes=[time_column], columns=[*content])
# Register the layer with the dataset
dataset.register([layer_path.as_uri()], layer_name="keyframes")
print(f"Registered keyframe layer at {layer_path}")
# endregion: add_keyframe_column
# region: query_with_keyframes
# Query using keyframe information for efficient random access
# Assume we've already added keyframe information via the preprocessing step
# above
target_frame_index = 42
target_time = times[target_frame_index]
# Create a reader that includes the keyframe layer data
# The column name follows the pattern: /{entity_path}:{component_name}
keyframe_column = "/video_stream:is_keyframe"
full_df = dataset.filter_contents(["/video_stream/**"]).reader(index="log_time")
# Query to find the most recent keyframe at or before the target time.
# Since we only log when is_keyframe=True, any row with this column present
# is a keyframe
keyframe_slice = full_df.filter(
(col("log_time") <= target_time) & col(keyframe_column).is_not_null()
)
closest_keyframe_df = keyframe_slice.aggregate(
[],
[
F.last_value(col("log_time"), order_by=[col("log_time")]).alias(
"latest_keyframe"
)
],
)
keyframe_result = pa.table(closest_keyframe_df)
# Start decoding from the most recent keyframe
start_time = keyframe_result["latest_keyframe"].to_numpy()[0]
start_frame_idx = np.searchsorted(times, start_time)
frames_saved = target_frame_index - start_frame_idx
print(
f"Found keyframe at frame {start_frame_idx}, "
f"saved decoding {frames_saved} frames"
)
# Query only the video samples from keyframe to target (much more efficient!)
efficient_video_df = df.filter(
col("log_time").between(start_time, target_time)
).select("log_time", video_column)
efficient_table = pa.table(efficient_video_df)
frames_to_decode = len(efficient_table)
print(
f"Decoding {frames_to_decode} frames "
f"(vs {target_frame_index + 1} without keyframe info)"
)
# Now decode just this smaller range
samples = efficient_table[video_column].to_numpy()
sample_times = efficient_table["log_time"].to_numpy()
sample_bytes = b""
for sample in samples:
sample_bytes += sample[0].tobytes()
data_buffer = BytesIO(sample_bytes)
container = av.open(data_buffer, format="h264", mode="r")
video_stream = container.streams.video[0]
# Decode to the target frame
frame = None
for packet, time in zip(
container.demux(video_stream), sample_times, strict=False
):
packet.time_base = Fraction(1, 1_000_000_000)
packet.pts = int(time - sample_times[0])
packet.dts = packet.pts
for decoded_frame in packet.decode():
frame = decoded_frame
if isinstance(frame, av.VideoFrame):
image = np.asarray(frame.to_image())
print(
f"Efficiently decoded frame {target_frame_index} "
f"with shape: {image.shape}"
)
# endregion: query_with_keyframes