chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<!--[metadata]
|
||||
title = "Compressed camera video stream"
|
||||
tags = ["2D", "Image encoding", "Video", "Streaming"]
|
||||
thumbnail = "https://static.rerun.io/camera_video_stream/b2f8f61eb62424aa942bdb5183e49246cf417e60/480w.png"
|
||||
thumbnail_dimensions = [480, 300]
|
||||
-->
|
||||
|
||||
This example uses [pyAV](https://pypi.org/project/av/) to fetch and encode a video stream to H.264 video and streams it live
|
||||
to the Viewer using the [`VideoStream`](https://www.rerun.io/docs/reference/types/archetypes/video_stream) archetype
|
||||
|
||||
<img src="https://static.rerun.io/camera_video_stream/b2f8f61eb62424aa942bdb5183e49246cf417e60/480w.png">
|
||||
|
||||
|
||||
## Run the code
|
||||
|
||||
```bash
|
||||
pip install -e examples/python/camera_video_stream
|
||||
python -m camera_video_stream
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
To learn more about video support in general check [the docs page on the topic](https://rerun.io/docs/concepts/logging-and-ingestion/video).
|
||||
|
||||
The example first sets up a video stream on an entity called `"video_stream"` with the H264 codec.
|
||||
```py
|
||||
rr.log("video_stream", rr.VideoStream(codec=rr.VideoCodec.H264), static=True)
|
||||
```
|
||||
H264 is a very well established codec and well suited for streaming.
|
||||
Note that in `setup_output_stream` we specicifially configure `pyAV` to use low latency encoding.
|
||||
|
||||
⚠️ Latency is expected to be well below a second, but as of writing Rerun is not well
|
||||
optimized for low latency video transmission as used for teleoperations.
|
||||
If you need lower latency you should consider tweaking the encoding settings further and
|
||||
configure Rerun's [Micro Batching](https://rerun.io/docs/reference/sdk/micro-batching).
|
||||
|
||||
For each frame we have to set a new timestamp.
|
||||
```py
|
||||
rr.set_time("time", duration=float(packet.pts * packet.time_base))
|
||||
```
|
||||
The time set here is the [_presentation timestamp_](https://en.wikipedia.org/wiki/Presentation_timestamp) (PTS) of the frame.
|
||||
Note that unlike with unlike with [`VideoAsset`](https://www.rerun.io/docs/reference/types/archetypes/asset_video),
|
||||
there's no need to log [`VideoFrameReference`](https://www.rerun.io/docs/reference/types/archetypes/video_frame_reference),
|
||||
to map the video's PTS to the Rerun timeline, since the time at which video samples
|
||||
are logged directly represents the PTS.
|
||||
TODO(#10090): In the presence of H.264/H.265 b-frames, separate _decode timestamps_ (DTS) are needed. This is not yet supported.
|
||||
|
||||
The frame data, known as a frame-`sample` since this may contain data relevant for an arbitrary number of frames in the future,
|
||||
is then logged with:
|
||||
```py
|
||||
rr.log("video_stream", rr.VideoStream.from_fields(sample=bytes(packet)))
|
||||
```
|
||||
|
||||
It's best practice to log the codec only once as it never changes, but there is little harm in
|
||||
doing so for every frame:
|
||||
```py
|
||||
rr.log("video_stream", rr.VideoStream(codec=rr.VideoCodec.H264, sample=bytes(packet)))
|
||||
```
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
import av
|
||||
|
||||
import rerun as rr
|
||||
|
||||
|
||||
def setup_camera_input(video_device: str | None = None) -> av.container.InputContainer:
|
||||
"""
|
||||
Setup the camera input container.
|
||||
|
||||
Uses first available video device. Exact behavior is platform dependent.
|
||||
"""
|
||||
if platform.system() == "Darwin":
|
||||
if video_device is None:
|
||||
video_device = "0"
|
||||
|
||||
return av.open(
|
||||
video_device,
|
||||
format="avfoundation",
|
||||
container_options={"framerate": "30"}, # `avfoundation` fails if the frame rate is not set.
|
||||
)
|
||||
elif platform.system() == "Windows":
|
||||
if video_device is None:
|
||||
# On windows we *have* to know the device name. Query ffmpeg for it.
|
||||
list_devices_cmd = ["ffmpeg", "-f", "dshow", "-list_devices", "true", "-i", "dummy", "-hide_banner"]
|
||||
devices_output = subprocess.check_output(list_devices_cmd, stderr=subprocess.STDOUT).decode()
|
||||
|
||||
# Extract video device names
|
||||
video_devices = []
|
||||
for line in devices_output.split("\n"):
|
||||
# FFmpeg will return both `(audio)` and `(video)` devices
|
||||
if "(video)" in line and "Alternative name" not in line:
|
||||
device_name = line.split('"')[1]
|
||||
if device_name not in video_devices:
|
||||
video_devices.append(device_name)
|
||||
if not video_devices:
|
||||
raise RuntimeError("No video devices found")
|
||||
|
||||
# Use first available video device
|
||||
video_device = video_devices[0]
|
||||
print(f"Using video device: {video_device}")
|
||||
|
||||
return av.open(f"video={video_device}", format="dshow")
|
||||
else:
|
||||
if video_device is None:
|
||||
video_device = "/dev/video0"
|
||||
|
||||
return av.open(video_device, format="v4l2")
|
||||
|
||||
|
||||
def setup_output_stream(width: int, height: int, codec: str = "h264") -> av.video.VideoStream:
|
||||
"""Setup the output stream which encodes the video stream to the specified codec."""
|
||||
|
||||
if codec == "h264":
|
||||
output_container = av.open("/dev/null", "w", format="h264") # Use AnnexB H.264 stream.
|
||||
output_stream = output_container.add_stream("libx264") # type: ignore[assignment]
|
||||
elif codec == "av1":
|
||||
output_container = av.open("/dev/null", "w", format="ivf") # Use IVF container for AV1 stream.
|
||||
output_stream = output_container.add_stream("libaom-av1") # type: ignore[assignment]
|
||||
else:
|
||||
raise ValueError(f"Unsupported codec: {codec}")
|
||||
|
||||
# Type narrowing
|
||||
assert isinstance(output_stream, av.video.stream.VideoStream)
|
||||
output_stream.width = width
|
||||
output_stream.height = height
|
||||
|
||||
# Configure for low latency.
|
||||
if codec == "h264":
|
||||
output_stream.codec_context.options = {
|
||||
"tune": "zerolatency",
|
||||
"preset": "veryfast",
|
||||
}
|
||||
elif codec == "av1":
|
||||
output_stream.codec_context.options = {
|
||||
"cpu-used": "8",
|
||||
"usage": "realtime", # Optimize for realtime encoding
|
||||
}
|
||||
|
||||
output_stream.max_b_frames = 0 # Avoid b-frames for lower latency.
|
||||
|
||||
return output_stream
|
||||
|
||||
|
||||
def stream_video_to_rerun(
|
||||
input: av.container.InputContainer, output: av.video.VideoStream, codec: str = "h264"
|
||||
) -> None:
|
||||
"""Streams the video continuously to Rerun."""
|
||||
|
||||
# Log codec only once as static data (it naturally never changes). This isn't strictly necessary, but good practice.
|
||||
video_codec = rr.VideoCodec.H264 if codec == "h264" else rr.VideoCodec.AV1
|
||||
rr.log("video_stream", rr.VideoStream(codec=video_codec), static=True)
|
||||
|
||||
while True:
|
||||
try:
|
||||
for frame in input.decode(video=0):
|
||||
# By default all the frames that come from the camera are marked as I-frames.
|
||||
# If we pass this just on as-is, then we get an encoded video stream that
|
||||
# just consists entirely of I-frames, thus having very poor compression!
|
||||
# Instead, we want the encoder to decide when to use P & I frames.
|
||||
frame.pict_type = av.video.frame.PictureType.NONE
|
||||
|
||||
for packet in output.encode(frame):
|
||||
if packet.pts is None:
|
||||
continue
|
||||
rr.set_time("time", duration=float(packet.pts * packet.time_base))
|
||||
rr.log("video_stream", rr.VideoStream.from_fields(sample=bytes(packet)))
|
||||
except av.BlockingIOError:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Streams compressed video from camera to Rerun.")
|
||||
parser.add_argument(
|
||||
"--video-device",
|
||||
type=str,
|
||||
help="Video device to use. If not provided, the first available video device will be used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--codec",
|
||||
type=str,
|
||||
choices=["h264", "av1"],
|
||||
default="h264",
|
||||
help="Video codec to use for encoding (default: h264).",
|
||||
)
|
||||
|
||||
rr.script_add_args(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
rr.script_setup(args, "rerun_example_video_stream_camera")
|
||||
|
||||
av.logging.set_level(av.logging.VERBOSE)
|
||||
|
||||
input_container = setup_camera_input(args.video_device)
|
||||
output_stream = setup_output_stream(
|
||||
input_container.streams.video[0].width, input_container.streams.video[0].height, args.codec
|
||||
)
|
||||
|
||||
try:
|
||||
stream_video_to_rerun(input_container, output_stream, args.codec)
|
||||
except KeyboardInterrupt:
|
||||
print("Recording stopped by user")
|
||||
|
||||
rr.script_teardown(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "camera_video_stream"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
dependencies = ["rerun-sdk", "av>=14.2.0"]
|
||||
|
||||
[project.scripts]
|
||||
camera_video_stream = "camera_video_stream:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
Reference in New Issue
Block a user