---
myst:
html_meta:
description: "Convert a video for playback, upload it as a channel on a dataset and review it in a run."
---
# Video ingest
{.lead}
Upload a video file as a channel on a dataset with {py:meth}`~nominal.core.Dataset.add_video`.
With the right start time, it plays in sync with the telemetry in the same run.
```{include} /guides/_snippets/connect-the-client.md
```
## Convert the video
`add_video` takes H.264 or H.265 video. For smooth playback, it also needs YUV420p pixels and regular keyframes.
{py:func}`~nominal.experimental.video_processing.normalize_video` converts a file to that format with [FFmpeg](https://www.ffmpeg.org/download.html), which must be installed with `ffprobe`:
```python
from nominal.experimental.video_processing import normalize_video
normalize_video("engine_fire.mov", "engine_fire.mp4", force=False) # force=False keeps an existing output
```
It writes H.264 with a keyframe about every 2 seconds, converting any audio to AAC.
`resolution` scales the video down, keeping its aspect ratio.
Normalizing can change the number of frames, and logs a warning when it does.
## Upload with a start time
`start` is the absolute time of the first frame. Use the real capture time, or the video won't line up with its telemetry.
```python
from datetime import datetime, timezone
dataset = client.create_dataset("Engine fire tests")
file = dataset.add_video(
"engine_fire.mp4",
channel="engine_cam",
start=datetime(2026, 9, 10, 14, 38, 19, tzinfo=timezone.utc),
)
file.poll_until_ingestion_completed()
```
If you know each frame's capture time, pass them as epoch nanoseconds in `frame_timestamps` instead of `start`.
More files on the same channel add to it.
`overwrite_overlapping=True` deletes earlier segments that overlap the new file.
For MCAP files, see [MCAP video ingest](/guides/video/mcap-video-ingest.md).
The standalone `Video` APIs, such as `client.create_video()`, are deprecated in favor of video channels.
## Review it in a run
Create a run over the file's time range and attach the dataset:
```python
assert file.bounds is not None # set once ingestion succeeds
run = client.create_run("Engine fire", start=file.bounds.start, end=file.bounds.end)
run.add_dataset("engine_video", dataset)
```
In a workbook on the run, add a video panel and select `engine_cam`. See [Runs](/guides/runs.md) for other ways to organize data.
## Inspect and remove files
{py:meth}`~nominal.core.Dataset.list_video_files` lists a dataset's video files and their metadata:
```python
for video in dataset.list_video_files():
print(video.name, video.num_frames, video.media_frame_rate, video.bounds)
```
To delete one file, call {py:meth}`~nominal.core.DatasetFile.delete` on it.
`dataset.archive()` archives the whole dataset, including its telemetry.
To process frames before uploading, see [Object identification](/guides/video/object-identification.md).