# Video ingest

Upload a video file as a channel on a dataset with [`add_video()`](https://dev.nominal.io/python/ref/generated/nominal.core.Dataset.md#nominal.core.Dataset.add_video "nominal.core.Dataset.add_video").
With the right start time, it plays in sync with the telemetry in the same run.

## Connect the client

This guide assumes you have [installed](https://dev.nominal.io/python/guides/install.md) the client and setup your [API keys](https://dev.nominal.io/python/guides/api-keys-setup.md).

```python
from nominal.core import NominalClient

client = NominalClient.from_profile("default")
```

## Convert the video

`add_video` takes H.264 or H.265 video. For smooth playback, it also needs YUV420p pixels and regular keyframes.
[`normalize_video()`](https://dev.nominal.io/python/ref/generated/nominal.experimental.video_processing.normalize_video.md#nominal.experimental.video_processing.normalize_video "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

asset = client.get_or_create_asset_by_properties({"serial": "<serial number>"}, name="Test engine")
dataset = asset.get_or_create_dataset("engine_video")
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](https://dev.nominal.io/python/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 on the asset over the file’s time range. The run sees the asset’s datasets, so you don’t attach the dataset again:

```python
assert file.bounds is not None  # set once ingestion succeeds
run = asset.create_run("Engine fire", start=file.bounds.start, end=file.bounds.end)
```

In a workbook on the run, add a video panel and select `engine_cam`. See [Runs](https://dev.nominal.io/python/guides/runs.md) for other ways to organize data.

## Inspect and remove files

[`list_video_files()`](https://dev.nominal.io/python/ref/generated/nominal.core.Dataset.md#nominal.core.Dataset.list_video_files "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 [`delete()`](https://dev.nominal.io/python/ref/generated/nominal.core.DatasetFile.md#nominal.core.DatasetFile.delete "nominal.core.DatasetFile.delete") on it.
`dataset.archive()` archives the whole dataset, including its telemetry.

To process frames before uploading, see [Object identification](https://dev.nominal.io/python/guides/video/object-identification.md).
