What is a .pvt file?
Short answer: it is not a file. A .pvt is an Apple Live Photo package — a folder that macOS dresses up as a single item, holding the still photo, the few seconds of motion, and a small manifest. This page opens one up and shows you the actual bytes.
The extension
.pvt stands for the video complement that Apple’s PhotoFoundation framework writes beside a Live Photo. You will run into one when a Live Photo is exported, synced or recovered outside the Photos app — on a Mac, in an iCloud download, or in a backup.
The confusing part is that Finder shows it with a single icon, like a document. That is the macOS bundle bit at work: a directory flagged as a package, the same trick behind .app and .rtfd. Ask any tool that does not honour the flag and the truth comes out:
$ file IMG_1673-preview.pvt
IMG_1673-preview.pvt: directory
$ ls -la IMG_1673-preview.pvt
-rw-r--r-- 2219588 IMG_1673.HEIC ← the still
-rw-r--r-- 2554722 IMG_1673.MOV ← the motion
-rw-r--r-- 256 metadata.plist ← the manifest
The three members, byte by byte
Every member is a perfectly ordinary file in its own right. You can prove that from the first sixteen bytes of each, which is exactly what a hex viewer is for. Here is the still:
00000000: 00 00 00 1c 66 74 79 70 68 65 69 63 00 00 00 00 ....ftypheic....
00000010: 6d 69 66 31 68 65 69 63 68 65 76 63 00 00 01 86 mif1heichevc....
00000020: 6d 65 74 61 00 00 00 00 00 00 00 21 68 64 6c 72 meta.......!hdlr
ftyp, 0x1c = 28 bytes long, major brand heic. Immediately after it comes meta — the box that describes the whole file as a set of items, and where the EXIF lives. It sits at the front, which is why a HEIC of any size opens instantly.And the motion:
00000000: 00 00 00 14 66 74 79 70 71 74 20 20 00 00 02 00 ....ftypqt ....
00000010: 71 74 20 20 00 00 00 08 77 69 64 65 00 02 b1 4c qt ....wide...L
00000020: 6d 64 61 74 00 00 02 71 06 05 ff ff 6d dc 45 e9 mdat...q....m.E.
qt is QuickTime. Then wide, then mdat — the media itself. Note what is not here: no moov. The index is written after the media, at the end of the file. That single fact shapes how the whole toolkit reads it; see no server, ever.The manifest
The third member is the smallest and the most literal. 256 bytes of XML whose entire job is to stamp a version on the pairing:
000000b0: 3e 50 46 56 69 64 65 6f 43 6f 6d 70 6c 65 6d 65 >PFVideoCompleme
000000c0: 6e 74 4d 65 74 61 64 61 74 61 56 65 72 73 69 6f ntMetadataVersio
000000d0: 6e 4b 65 79 3c 2f 6b 65 79 3e 0a 09 3c 73 74 72 nKey</key>..<str
000000e0: 69 6e 67 3e 31 3c 2f 73 74 72 69 6e 67 3e 0a 3c ing>1</string>.<
000000f0: 2f 64 69 63 74 3e 0a 3c 2f 70 6c 69 73 74 3e 0a /dict>.</plist>.
PFVideoComplementMetadataVersionKey, with the value 1. “PF” is PhotoFoundation. It does not name the members or link them — iOS pairs the still and the motion by filename stem (IMG_1673).What OmniViewer shows you: the METADATA tab
Open a package on omniviewer.org/pvt and you get five screens: LIVE (press and hold to play it, the way Photos does), MEMBERS, METADATA, EXPORT and STATS, plus the raw byte and hex views.
METADATA is the one that does something no per-file tool can. It reads both halves and lays them side by side:
- the still’s EXIF — camera make and model, lens, exposure, timestamps, and the GPS block;
- the motion’s QuickTime metadata — the reverse-DNS
mdtakeys an iPhone writes, including its own copy of the location, the content identifier, and on a real device the Live Photo tuning keys (live-photo.vitality-scoreand friends); - the manifest keys above.
Fields are grouped the way every other OmniViewer METADATA tab groups them — Identifying (what the device wrote about you), Recording (how the capture was made), Authoring (what a person typed) — so the question “what does this file say about me?” has the same shape whatever you dropped.
Copy one, in the still: the EXIF GPS IFD
EXIF is a TIFF structure bolted into the HEIC as its own item. It opens with a byte-order mark, and everything after it is a table of 12-byte records:
0: 49 49 2a 00 08 00 00 00 05 00 0f 01 02 00 06 00 II*.............
II = Intel byte order (little-endian), 42 as a magic number, then the offset of the first directory. Every number below is little-endian because of these two bytes.The GPS directory’s records sit at offset 76. Four of them, twelve bytes each — tag, type, count, value-or-offset:
76: 01 00 02 00 02 00 00 00 4e 00 00 00 02 00 05 00 ........N.......
92: 03 00 00 00 c4 00 00 00 03 00 02 00 02 00 00 00 ................
108: 45 00 00 00 04 00 05 00 03 00 00 00 dc 00 00 00 E...............
0x0001 GPSLatitudeRef, ASCII, 2 chars, stored inline: 4e = N.Tag
0x0002 GPSLatitude, type 5 (rational), 3 of them — too big for the slot, so 0xc4 = offset 196.Tag
0x0003 GPSLongitudeRef, inline: 45 = E.Tag
0x0004 GPSLongitude, 3 rationals at 0xdc = offset 220.
Follow those two offsets and you get six 32-bit pairs — numerator, denominator — which are degrees, minutes and seconds:
196: 2e 00 00 00 01 00 00 00 1f 00 00 00 01 00 00 00 ................
212: c0 12 00 00 64 00 00 00 08 00 00 00 01 00 00 00 ....d...........
228: 00 00 00 00 01 00 00 00 10 0e 00 00 64 00 00 00 ............d...
Longitude: 8/1 = 8°, 0/1 = 0′, 0x0e10/0x64 = 3600/100 = 36″ → 8.0100
That is
+46.5300, +8.0100 — the coordinate the METADATA tab prints for the still.
Copy two, in the motion: a QuickTime mdta key
Now the same place, written a completely different way. QuickTime keeps modern metadata as a keys table of reverse-DNS names, with a parallel ilst whose entries point at them by index:
0002b570: 6b 65 79 73 00 00 00 00 00 00 00 07 00 00 00 2c keys...........,
0002b580: 6d 64 74 61 63 6f 6d 2e 61 70 70 6c 65 2e 71 75 mdtacom.apple.qu
0002b590: 69 63 6b 74 69 6d 65 2e 6c 6f 63 61 74 69 6f 6e icktime.location
0002b5a0: 2e 49 53 4f 36 37 30 39 00 00 00 20 6d 64 74 61 .ISO6709... mdta
com.apple.quicktime.location.ISO6709, 0x2c = 44 bytes long including its header and the mdta namespace tag. The keys that follow are ….make, ….model, and so on.The value itself lives in the ilst, as plain text:
0002b670: 65 72 00 00 01 41 69 6c 73 74 00 00 00 33 00 00 er...Ailst...3..
0002b680: 00 01 00 00 00 2b 64 61 74 61 00 00 00 01 00 00 .....+data......
0002b690: 00 00 2b 34 36 2e 35 33 30 30 2b 30 30 38 2e 30 ..+46.5300+008.0
0002b6a0: 31 30 30 2b 31 36 33 33 2e 30 30 30 2f 00 00 00 100+1633.000/...
00 00 00 01 — the first key — then a data atom holding +46.5300+008.0100+1633.000/. That is ISO 6709: latitude, longitude, altitude in metres, terminated by a solidus. The same spot as the EXIF above, to four decimal places, in an entirely unrelated encoding.How this works without a server
Everything above is read in your browser. Nothing is uploaded, and there is no back end to upload it to — omniviewer.org is a static site. Four pieces make that possible:
1. The entries API opens the folder
A dropped package is a directory, and dataTransfer.files gives nothing useful for one. The file-system entries API does — with one sharp edge, which is that the item list is torn down the instant the drop handler returns:
document.addEventListener('drop', (event) => {
// Synchronous, on purpose. A single `await` before this line
// and the item list is already gone.
const entry = firstDirectoryEntry(event.dataTransfer);
if (entry) {
readDirectoryEntry(entry).then((files) =>
onBundle({ name: entry.name, files }));
return;
}
onFile(event.dataTransfer.files[0]);
});
What comes back are ordinary File handles, backed by the bytes on your disk — not copies in memory.
2. Reads are slices, not loads
A File can be sliced, and a slice is only fetched when it is read. So the parsers ask for the few hundred bytes they actually need. For the motion that matters enormously, because — as the hex above showed — QuickTime puts moov after mdat. Instead of scanning, the reader steps along the top-level box headers, sixteen bytes at a time, and jumps:
let off = 0;
while (off + 8 <= file.size) {
const h = await read(file, off, 16); // 16 bytes, not the box
let size = readU32(h, 0);
const type = str(h, 4, 4);
if (size === 1) size = readU64(h, 8); // 64-bit 'largesize'
layout.push({ type, start: off, size });
off += size; // steps straight over mdat
}
const moov = layout.find((b) => b.type === 'moov');
A four-gigabyte mdat costs one addition. That is the whole reason a package with an enormous motion track opens as fast as a three-second one.
3. Parsing runs in a Web Worker
The box walking, the EXIF decode and the QuickTime key table are all done off the main thread, so the page never freezes — and the parsers are hand-written and dependency-free, which is a deliberate constraint across all of OmniViewer.
4. Playback is a local object URL
The LIVE tab hands the motion member straight to a <video> element as an object URL. The browser streams it off your disk with its own decoder; not one media byte passes through our JavaScript, let alone a network.
Related reading
Both members are ordinary formats with toolkits and write-ups of their own:
- How a Live Photo stores your location twice — the companion piece, and the reason
/pvtexists at all. - Inside a HEIC file: boxes, items and HEVC — the still, in depth.
- Two hidden identifiers in iPhone HEIC metadata — UUIDs buried in Apple’s private MakerNote.
- Inside an MP4: moov, mdat and fast start — why the index sits at the end, and what that costs.
- Inside a ZIP file — relevant because a package can also arrive zipped.
- How we auto-detect a file’s format — the content sniff that never trusts an extension.