Skip to content
Peter edited this page Apr 26, 2026 · 6 revisions

Ubiquiti mux standard audio/video streams along with their metadata into proprietary ubv files. This format is a series of records designed to be easily written sequentially. It allows recovery even if power is lost during recording while still providing affordances for forward/backward seeking and efficient jumping to point-in-time.

The format has evolved over time, with header bits used to signal the presence of extra optional fields - however the simple fundamental structure holds of a record format that lightly wraps standard Audio+Video essence streams.

Big endian used consistently throughout.

History and Motivation

It is important for users to be able to access my Unifi Protect recordings as standard MP4 files. Personally, I found Ubiquiti's UI/API for this to be flaky (especially given the limited and highly contended IO environment on their NVRs). I also wanted to ensure that I can keep synchronous backups of my .ubv files to hardened storage that will survive even if the on-premises NVRs are destroyed or stolen - and in this case I need to be able to read the .ubv files from standard environments.

In the past Ubiquiti shipped a ubnt_ubvinfo tool to parse and display .ubv file structure. In 2020 I wrote a program that would parse the output of that tool, extracting the Audio and Video frame data and constructing an MP4. This had limitations, since Ubiquiti don't supply Linux x86 (or macOS Apple Silicon) releases, meaning that users needed to copy that tool from their NVR and then use qemu to run it under Linux x86.

Despite these limitations, my remux tool has helped several users (that I know of, and I assume others who have not reached out) recover footage from damaged/corrupted Protect systems that would otherwise have been irrecoverable due to the proprietary nature of the UBV format.

My previous technique relied on Ubiquiti continuing to freely provide ubnt_ubvinfo. Unfortunately, recent Protect releases appear to have stopped including this tool, which meant that in order to ensure continued compatibility I had to fully reverse engineer the file format (as opposed to my previous, more limited attempts to simply understand the payload encodings).

Presumed good-will

Note, that while Ubiquiti have stopped actively providing this helpful tooling, they haven't shown any signs of adding active techniques to frustrate the ability for owners to freely access/read the video files their systems record.

Folder and Filename structure

UBV files are written to disk in a simple video/yyyy/mm/dd/[camera mac]_[type1]_[type2]_[start utc millis].ubv structure. They are created with size of 1.0GB and filled out over time.

Observed type1_type2 values:

  • 0_rotating: full-quality camera recordings
  • 2_rotating: 1fps low-quality (640x480) recording (not yet mapped, unsupported by remux tool)
  • 0_timelapse: generated timelapse file (not yet mapped, unsupported)
  • 2_timelapse: generated timelapse file (not yet mapped, unsupported)

Record Structure - High Level

Physically, UBV files are a simple list of Records with the following structure:

+────────+────────────+──────+────────────+──────+───────────+
│ HEADER │ HEADER EXT | SIZE │ PAYLOAD    | PAD  │ BACK_SIZE │
│ 8B     │ variable   | 4B   │ SIZE bytes | 0-3B │ 4B        │
+────────+────────────+──────+────────────+──────+───────────+

Where:

  • Header must be parsed to identify Header Ext length, see Record Header - Detail
  • Size is the number of bytes of payload
  • Payload is some record-specific payload data (Size bytes long)
  • Pad bytes are inserted after Payload to re-align to 32 bit boundaries
  • Back Size is the size of HEADER+HEADER EXT+SIZE+PAYLOAD+PAD, and is presumably present to allow efficient reverse seeking through footage

Partition

Logically, a file is a sequence of Partition (which appears to represent a recording session), where each Partition contains one or more of the following packets:

  • Clock Sync
  • Video frame (I or P frame, as expected B frames are not used)
  • Audio frame
  • Skip packet (for deleted content)

Packet

AV Packets

Audio and Video packets are most useful for our purposes. So far the following types have been observed:

  1. H.264 (Track 7) - length-prefixed NALs
  2. H.265 (HEVC) (Track 1003) - length-prefixed NALs
  3. AV1 (Track 1004) - AV1 low-overhead bitstream OBUs
  4. AAC audio (Track 1000) - containing a single ADTS frame

Clock Sync Packets (0xDA7E)

Partitions start with a Clock Sync packet; further clock sync packets are distributed throughout the partition. My working assumption is that they are inserted periodically, and at the point of any timecode discontinuity.

Clock-sync records map between a DTS value and a wall-clock time, expressed as 4 bytes UNIX timestamp, and 4 bytes nanosecond part. wall_clock_utc_millis = seconds_field * 1000 + (nanoseconds_field + 500000) / 1000000

Smart Event packets

Smart Event packets may be included throughout; these are assumed to be to do with AI detections. I've not mapped the payload structure yet, and this is not a high-priority since presumably anything contained within can be re-derived from analysing the AV stream.

Seek Table

When finalised, partitions end with a sorted map of Wall-Clock time -> I-frame offset (for fast seeking)

Record Header - Detail

Record Headers start at 32-bit boundaries and are of variable length, starting with an 8 byte static header and then optional additional fields. The Format Code (bytes 4-5) define which additional fields are present.

Static Header

Bytes 0-3: Track

+─────────────+──────────+──────────+
│ Magic Byte  │ Track ID │ Checksum |
│ Const 0xA0  │ 2B       │ 1B       │
+─────────────+──────────+──────────+

Checksum is an XOR of the Magic Byte and Track ID bytes (0xA0 ^ TrackID[0] ^ TrackID[1])

Examples

Bytes 0-3 (hex) Track ID Checksum Record type
a0 00 09 a9 0x0009 a9 Partition header
a0 00 07 a7 0x0007 a7 H.264 video
a0 03 e8 4b 0x03E8 4b AAC audio
a0 03 eb 48 0x03EB 48 HEVC video
a0 da 7e 04 0xDA7E 04 Clock sync
a0 00 06 a6 0x0006 a6 Skip packet

Bytes 4-5: Format Code

Big-endian uint16. Controls header layout, frame metadata, and clock rate.

Byte 4 (high byte) - Flag Bits

Bit Mask Name Meaning when set
7 0x80 Extended header Extended header with per-field parsing. Always observed as set
6 0x40 Unknown Always observed as set
5 0x20 Keyframe Always set, except on Video P Frames
4 0x10 Has CTS CTS field present (only adds bytes when bit 3 is not set - see extended header fields).
3 0x08 Set Sample Rate If set, set the track sample rate from this packet for this and subsequent packets
2 0x04 64-bit DTS Signals 64-bit DTS; otherwise 32-bit DTS
1 0x02 Unknown Never observed to be set
0 0x01 Unknown Always observed as set
Observed Flag Combinations
Byte 4 Binary Key 64-bit DTS Clock Rate Usage
0xED 11101101 Yes Yes Yes Video keyframe
0xCD 11001101 No Yes Yes Video non-keyframe
0xFD 11111101 Yes Yes Yes Audio frame, partition header (new format)
0xF9 11111001 Yes No Yes Clock sync (old format)
0xF1 11110001 Yes No No Skip/padding packet, or: timelapse track 6 clock ref

Byte 5 (low byte) - Sample Rate Index

Low nibble (bits 3-0) indexes into a fixed clock rate table. Upper nibble always observed as zero.

Index Clock Rate (Hz) Usage
0 Unknown Unknown
1 (special) Read uint32 from stream instead of table lookup
2 1,000 Milliseconds (Clock sync, timelapse)
3 Unobserved
4 Unobserved
5 Unobserved
6 16,000 Audio (observed)
7 Unobserved
8 Unobserved
9 Unobserved
10 44,100 Audio (observed)
11 48,000 Audio
12 90,000 Video (observed)
13 Unobserved
14 Unobserved
15 Unobserved

Bytes 6-7: Sequence Counter

Per-track unsigned sequence counter (wraps at 0xFFFF). Increments by 1 each packet, with discontinuities indicating dropped frames.

Additional Fields

When the Extended header flag is set (which is the case for all observed packets), the remaining header is built by appending each of the following fields in order, skipping any whose condition is not met:

  1. Literal Sample Rate (4 bytes, uint32): if Sample Rate Index == 1 ("special") - Holds a literal custom track sample rate value
  2. 64-bit DTS (8 bytes, uint64): if 64-bit DTS flag is set
  3. 32-bit DTS (4 bytes, uint32): unless 64-bit DTS flag is set
  4. CTS (4 bytes, uint32): if Has CTS flag is set and Clock rate present flag is not set - Only appears in timelapse files (with Format Code 0xF100), and is the difference between DTS and PTS (it would allow for the use of B-Frames, although so far the only observed CTS is zero)