Loading...
Loading...
Compare original and translation side by side
Mp4Reader.mp4stream().stream()rerun-chunk-processingMp4Reader.mp4stream().stream()rerun-chunk-processingfrom rerun.experimental import Mp4Reader, Mp4TranscodeOptions
reader = Mp4Reader(video_path, entity_path="/camera/front") # mode="stream" by default
stream = reader.stream() # lazy: nothing is decoded yetpathLazyChunkStream.merge(...)from rerun.experimental import Mp4Reader, Mp4TranscodeOptions
reader = Mp4Reader(video_path, entity_path="/camera/front") # 默认 mode="stream"
stream = reader.stream() # 惰性执行:此时尚未解码任何内容pathLazyChunkStream.merge(...) | emits | when |
|---|---|---|
| a codec chunk, per-GOP sample chunks, a keyframe marker | almost always — every frame is time-indexed and queryable |
| the whole file as one blob, plus a frame index | the codec cannot be a |
VideoStreammp4vrerun video.mp4 | 输出内容 | 使用场景 |
|---|---|---|
| 一个编解码器块、每个GOP对应的样本块、一个关键帧标记 | 绝大多数场景——每一帧都带有时间索引,可被查询 |
| 整个文件作为单个 blob,外加一个帧索引 | 编解码器无法被 |
VideoStreammp4vrerun video.mp4tests/assets/video/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4entity_path="/camera/front"[0] entity=/camera/front static=True rows=1 timelines=[] cols=['VideoStream:codec']
[1] entity=/camera/front static=False rows=30 timelines=['video'] cols=['VideoStream:sample']
[2] entity=/camera/front static=False rows=1 timelines=['video'] cols=['VideoStream:is_keyframe']VideoStream:codecif chunk.is_static: continueis_keyframeTruecollect(optimize=…)[True]Truevideoduration[ns]entity_path=Nonefoo/video.mp4/data/data/foo/video.mp4entity_pathchunk_by_gop=Falseis_staticVideoStream:sampleVideoStream:is_keyframeVideoCodectests/assets/video/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4entity_path="/camera/front"[0] entity=/camera/front static=True rows=1 timelines=[] cols=['VideoStream:codec']
[1] entity=/camera/front static=False rows=30 timelines=['video'] cols=['VideoStream:sample']
[2] entity=/camera/front static=False rows=1 timelines=['video'] cols=['VideoStream:is_keyframe']VideoStream:codecif chunk.is_static: continueis_keyframeTruecollect(optimize=…)[True]Truevideoduration[ns]entity_path=None/datafoo/video.mp4/data/foo/video.mp4entity_pathchunk_by_gop=Falseis_staticVideoStream:sampleVideoStream:is_keyframeVideoCodecVideoStream-bf 0ffmpegMp4TranscodeOptions| field | effect |
|---|---|
| re-encode to another |
| force a keyframe every N frames — the knob for seek cost in the viewer |
| best-effort hardware encode, NVENC / VideoToolbox only |
| use this |
output_codecchunk_by_gop=Truegop_size=Ngop_size=10try_gpuMp4Reader(
video_path,
entity_path="/camera/front",
# ~1s GOPs on a 60fps source, so seeking in the viewer stays snappy.
transcode=Mp4TranscodeOptions(gop_size=64),
).stream()VideoStream-bf 0ffmpegMp4TranscodeOptions| 字段 | 效果 |
|---|---|
| 重新编码为另一种 |
| 强制每N帧生成一个关键帧——用于控制查看器中的搜索成本 |
| 尝试硬件编码,仅支持 NVENC / VideoToolbox |
| 使用指定的 |
output_codecchunk_by_gop=Truegop_size=Ngop_size=10try_gpuMp4Reader(
video_path,
entity_path="/camera/front",
# 60fps源视频设置约1秒的GOP,确保查看器中的搜索操作流畅。
transcode=Mp4TranscodeOptions(gop_size=64),
).stream()timeline_name="real_time"timeline_type="timestamp"stream.map(...)icapture_times_ns[i]SAMPLE_COL = "VideoStream:sample"
def _reindex_to_capture_times(stream, capture_times_ns, timeline_name):
cursor = 0
pts_to_time = {}
def _retag(chunk):
nonlocal cursor
if chunk.is_static: # the codec chunk carries no timeline
return chunk
batch = chunk.to_record_batch()
col_index = batch.schema.get_field_index(timeline_name)
old_field = batch.schema.field(col_index)
old_pts = np.asarray(batch.column(col_index).cast(pa.int64()))
if SAMPLE_COL in batch.schema.names:
# Clamp in case the decoder yields a slightly different frame count.
indices = np.clip(np.arange(cursor, cursor + chunk.num_rows), 0, len(capture_times_ns) - 1)
cursor += chunk.num_rows
new_times_ns = capture_times_ns[indices]
pts_to_time.update(zip(old_pts.tolist(), new_times_ns.tolist()))
else:
# The sparse keyframe marker, emitted after every sample chunk.
new_times_ns = np.array([pts_to_time[pts] for pts in old_pts.tolist()], dtype=np.int64)
times = pa.array(new_times_ns.astype("datetime64[ns]"))
# `metadata=` is load-bearing — see gotcha 3.
new_field = pa.field(old_field.name, times.type, nullable=old_field.nullable, metadata=old_field.metadata)
return Chunk.from_record_batch(batch.set_column(col_index, new_field, times))[0]
return stream.map(_retag)mapflat_maptimeline_name="real_time"timeline_type="timestamp"stream.map(...)capture_times_ns[i]SAMPLE_COL = "VideoStream:sample"
def _reindex_to_capture_times(stream, capture_times_ns, timeline_name):
cursor = 0
pts_to_time = {}
def _retag(chunk):
nonlocal cursor
if chunk.is_static: # 编解码器块不包含时间线
return chunk
batch = chunk.to_record_batch()
col_index = batch.schema.get_field_index(timeline_name)
old_field = batch.schema.field(col_index)
old_pts = np.asarray(batch.column(col_index).cast(pa.int64()))
if SAMPLE_COL in batch.schema.names:
# 限制索引范围,避免解码器返回的帧数量略有差异。
indices = np.clip(np.arange(cursor, cursor + chunk.num_rows), 0, len(capture_times_ns) - 1)
cursor += chunk.num_rows
new_times_ns = capture_times_ns[indices]
pts_to_time.update(zip(old_pts.tolist(), new_times_ns.tolist()))
else:
# 稀疏的关键帧标记块,在所有样本块之后输出。
new_times_ns = np.array([pts_to_time[pts] for pts in old_pts.tolist()], dtype=np.int64)
times = pa.array(new_times_ns.astype("datetime64[ns]"))
# `metadata=` 是必需的——参见注意事项3。
new_field = pa.field(old_field.name, times.type, nullable=old_field.nullable, metadata=old_field.metadata)
return Chunk.from_record_batch(batch.set_column(col_index, new_field, times))[0]
return stream.map(_retag)mapflat_mapstreams = [
Mp4Reader(path, entity_path=f"/camera/{name}", timeline_name="real_time", timeline_type="timestamp").stream()
for name, path in cameras.items()
]
(
LazyChunkStream
.merge(*streams)
.collect(optimize=OptimizationProfile.OBJECT_STORE)
.write_rrd(out_path, application_id=app_id, recording_id=segment_id)
)OBJECT_STOREfix_keyframe=Trueskipping GoP rebatching … is_keyframe data is incorrectfix_keyframe=Truestreams = [
Mp4Reader(path, entity_path=f"/camera/{name}", timeline_name="real_time", timeline_type="timestamp").stream()
for name, path in cameras.items()
]
(
LazyChunkStream
.merge(*streams)
.collect(optimize=OptimizationProfile.OBJECT_STORE)
.write_rrd(out_path, application_id=app_id, recording_id=segment_id)
)OBJECT_STOREfix_keyframe=Trueskipping GoP rebatching … is_keyframe data is incorrectfix_keyframe=Truecollect(optimize=…)max_bytesmax_rowsgop_size=10OBJECT_STORELIVEsend_chunkscollect(optimize=…)max_bytesmax_rowsgop_size=10OBJECT_STORELIVEsend_chunks | | delta | |
|---|---|---|---|
| 11.1 ms | 16.3 ms | +5.2 ms (1.5×) |
| 14.2 ms | 48.6 ms | +34.4 ms (3.4×) |
build_sample_indexdetect_gop_startchunk_from_goptaken(0..n)re_arrow_util::take_arraychunk_by_gop=Falsechunk_from_gopconcat_and_sortLIVEOBJECT_STORE | | 差值 | |
|---|---|---|---|
| 11.1 ms | 16.3 ms | +5.2 ms(1.5倍) |
| 14.2 ms | 48.6 ms | +34.4 ms(3.4倍) |
build_sample_indexdetect_gop_startchunk_from_goptaken(0..n)re_arrow_util::take_arraychunk_by_gop=Falsechunk_from_gopconcat_and_sortLIVEOBJECT_STOREstream()to_chunks()send_chunksMp4Reader(…)VideoCodecVideoStreammp4vRuntimeError: MP4 error: MP4 demux: Video track uses unsupported codec "mp4v"VideoFrameReferencemapflat_mappa.field(...)metadata=old_field.metadatarerun:kind: 'index'not chunk.is_staticVideoStream:samplemode="asset"chunk_by_gop=Falsetranscode=ValueErrortimeline_type="timestamp"VideoStreamstream()collect()mapflat_mapstream()to_chunks()send_chunksMp4Reader(…)VideoCodecVideoStreammp4vRuntimeError: MP4 error: MP4 demux: Video track uses unsupported codec "mp4v"VideoFrameReferencemapflat_mapmetadata=old_field.metadatapa.field(...)rerun:kind: 'index'not chunk.is_staticVideoStream:samplemode="asset"chunk_by_gop=Falsetranscode=ValueErrortimeline_type="timestamp"VideoStreamstream()collect()mapflat_maprerun_py/tests/integration/test_mp4_reader.pychunk_by_goptimeline_typecrates/store/re_mp4_reader/stream.rsasset.rscrates/store/re_mp4_reader/tests/stream.rsrerun-chunk-processingrerun-data-modelrerun_py/tests/integration/test_mp4_reader.pychunk_by_goptimeline_typecrates/store/re_mp4_reader/stream.rsasset.rscrates/store/re_mp4_reader/tests/stream.rsrerun-chunk-processingrerun-data-model