-
Notifications
You must be signed in to change notification settings - Fork 23
Home
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.
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).
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.
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
remuxtool) - 0_timelapse: generated timelapse file (not yet mapped, unsupported)
- 2_timelapse: generated timelapse file (not yet mapped, unsupported)
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:
-
Headermust be parsed to identifyHeader Extlength, see Record Header - Detail -
Sizeis the number of bytes of payload -
Payloadis some record-specific payload data (Sizebytes long) -
Padbytes are inserted afterPayloadto re-align to 32 bit boundaries -
Back Sizeis the size ofHEADER+HEADER EXT+SIZE+PAYLOAD+PAD, and is presumably present to allow efficient reverse seeking through footage
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)
Audio and Video packets are most useful for our purposes. So far the following types have been observed:
- H.264 (Track 7) - length-prefixed NALs
- H.265 (HEVC) (Track 1003) - length-prefixed NALs
- AV1 (Track 1004) - AV1 low-overhead bitstream OBUs
- AAC audio (Track 1000) - containing a single ADTS frame
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 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.
When finalised, partitions end with a sorted map of Wall-Clock time -> I-frame offset (for fast seeking)
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.
+─────────────+──────────+──────────+
│ 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])
| 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 |
Big-endian uint16. Controls header layout, frame metadata, and clock rate.
| 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 |
| 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 |
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 |
Per-track unsigned sequence counter (wraps at 0xFFFF). Increments by 1 each packet, with discontinuities indicating dropped frames.
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:
- Literal Sample Rate (4 bytes, uint32): if Sample Rate Index == 1 ("special") - Holds a literal custom track sample rate value
- 64-bit DTS (8 bytes, uint64): if 64-bit DTS flag is set
- 32-bit DTS (4 bytes, uint32): unless 64-bit DTS flag is set
- 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)