# Custom ffmpeg pipelines

Some things Frigate and go2rtc can't do natively — burning a timestamp
into the picture, rotating a ceiling-mounted camera on the GPU, splitting a
dual-fisheye sensor into two views — you can do by defining a go2rtc **`exec:`
stream**: a custom `ffmpeg` command that produces an RTSP feed Frigate then
consumes like any other camera. Fregata ships a full-featured `ffmpeg` with
`drawtext`, the VideoToolbox filters (`transpose_vt`, `scale_vt`), and the
VideoToolbox encoders, so these pipelines run hardware-accelerated on Apple
Silicon.

## Before you start

`exec:` sources run arbitrary local commands, so **Fregata disables them by
default**. You have to opt in — see
[Custom go2rtc stream sources](/guides/network-and-remote-access/#custom-go2rtc-stream-sources)
for the full security rationale. In short:

1. Set
   [`GO2RTC_ALLOW_ARBITRARY_EXEC=true`](/reference/environment-variables/#go2rtc_allow_arbitrary_exec)
   in **Settings → Environment Variables** (menu-bar tray).
2. Restart Fregata — environment-variable changes take effect only when
   Frigate starts.

Without this, the streams below are dropped from the generated go2rtc config
and a line naming them is written to the Frigate log.

**One rule governs every recipe here:** stream values are expanded with
Python's `str.format()` before go2rtc sees them (so you can drop in
`{FRIGATE_*}` variables), which means **every literal `{` or `}` must be
doubled**. That is why the RTSP output placeholder is written `{{output}}`
(it collapses to the `{output}` token go2rtc fills in) and why an ffmpeg
expansion such as `drawtext`'s `%{localtime}` is written `%{{localtime}}`.
See
[The `{{output}}` token and brace-doubling](/guides/network-and-remote-access/#the-output-token-and-brace-doubling).

The general shape of every recipe:

```yaml
go2rtc:
  streams:
    my_stream:
      - "exec:ffmpeg <input flags> -i <source> <filters/encode> -rtsp_transport tcp -f rtsp {{output}}"

cameras:
  my_camera:
    ffmpeg:
      inputs:
        # Point Frigate at the restream, not the raw camera
        - path: rtsp://127.0.0.1:8554/my_stream
          roles: [detect, record]
```

Store camera passwords as `FRIGATE_*` environment variables (e.g.
`{FRIGATE_CAM_PW}` below) rather than in plaintext — see
[Environment variables](/reference/environment-variables/#other-secrets).

## Timestamp and text overlays (drawtext)

Burn a live clock into the top-left corner. `%{localtime}` must be written
`%{{localtime}}` so it survives the `str.format` pass and reaches ffmpeg
intact:

```yaml
go2rtc:
  streams:
    front_osd:
      - "exec:ffmpeg -rtsp_transport tcp -i rtsp://user:{FRIGATE_CAM_PW}@10.0.0.5/main -vf drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='%{{localtime}}':x=12:y=12:fontsize=36:fontcolor=white:box=1:boxcolor=black@0.5 -c:v h264_videotoolbox -b:v 4000k -rtsp_transport tcp -f rtsp {{output}}"
```

Adjust the `fontfile=` path to any `.ttf` on your Mac. Anything after
`drawtext=` is a normal ffmpeg filter — see the
[ffmpeg drawtext docs](https://ffmpeg.org/ffmpeg-filters.html#drawtext-1)
for `%{{localtime}}` format strings, positioning, and multi-line text.

## GPU-accelerated rotation (VideoToolbox)

go2rtc's built-in `#rotate` is CPU-only, so a rotated stream burns a core
per camera. VideoToolbox's `transpose_vt` filter does the rotation on the
GPU with a zero-copy path — decode, rotate, and encode all stay on the
hardware:

```yaml
go2rtc:
  streams:
    garage_rotated:
      - "exec:ffmpeg -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -rtsp_transport tcp -i rtsp://user:{FRIGATE_CAM_PW}@10.0.0.6/main -vf transpose_vt=dir=clock -c:v h264_videotoolbox -b:v 4000k -rtsp_transport tcp -f rtsp {{output}}"
```

- `-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld` keeps the
  decoded frames on the GPU so `transpose_vt` and the encoder operate on them
  without a round-trip to system memory.
- `transpose_vt=dir=clock` rotates 90° clockwise. Use `dir=cclock` for
  counter-clockwise; chain two for 180°
  (`transpose_vt=dir=clock,transpose_vt=dir=clock`).
- `-b:v 4000k` is not optional — the VideoToolbox encoders need an explicit
  bitrate. Size it to the camera's resolution.
- Don't add low-latency encoder flags here; they aren't needed for a restream
  and can hurt compatibility.

Then set the Frigate camera's input to
`rtsp://127.0.0.1:8554/garage_rotated`, as in the shape above.

## Splitting a dual-fisheye camera

A 180°/360° camera that publishes two fisheye circles in one frame can be
split into two independent Frigate cameras by cropping each half in its own
`exec:` stream:

```yaml
go2rtc:
  streams:
    porch_left:
      - "exec:ffmpeg -hwaccel videotoolbox -rtsp_transport tcp -i rtsp://user:{FRIGATE_CAM_PW}@10.0.0.7/main -vf crop=iw/2:ih:0:0 -c:v h264_videotoolbox -b:v 3000k -rtsp_transport tcp -f rtsp {{output}}"
    porch_right:
      - "exec:ffmpeg -hwaccel videotoolbox -rtsp_transport tcp -i rtsp://user:{FRIGATE_CAM_PW}@10.0.0.7/main -vf crop=iw/2:ih:iw/2:0 -c:v h264_videotoolbox -b:v 3000k -rtsp_transport tcp -f rtsp {{output}}"
```

`crop=iw/2:ih:0:0` takes the left half; `crop=iw/2:ih:iw/2:0` takes the
right. Each restream becomes its own camera in `config.yml`.

## Verifying and debugging

- **The stream never appears.** Check the Frigate log (tray → **Settings →
  Open Frigate Logs**). A restricted-source or brace-doubling problem is
  logged with the stream name and the reason — a dropped stream is never
  silent. The most common cause is an un-doubled brace (`{output}` instead of
  `{{output}}`, or `%{localtime}` instead of `%{{localtime}}`).
- **Confirm the raw pipeline first.** Before wiring it into `config.yml`, run
  the un-escaped `ffmpeg` command directly in a terminal against your camera
  to confirm the filter chain and encoder work, then add the `str.format`
  brace-doubling.
- **Re-encoding has a cost.** Every recipe here decodes and re-encodes, which
  uses a VideoToolbox session and some GPU time per camera. That's fine for a
  handful of streams; if you're rotating or overlaying many cameras, watch the
  numbers on the [Performance](/guides/performance/) page.

For the underlying restream configuration keys (names, sub-streams,
`#backchannel`), Fregata follows upstream Frigate — see the
[Frigate restream reference](https://docs.frigate.video/configuration/restream/).
