✳ OMNIVIEWER PVT metadata PVT viewer ← back

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.

On this page The extension The three members The manifest The METADATA tab Bytes: EXIF GPS Bytes: QuickTime No server, ever Related reading

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
Why nothing opens it. A double-click hands the package to Photos or nothing at all, and every web-based viewer refuses it, because a browser file input takes one file at a time — drop a folder on an ordinary upload box and you get a zero-byte entry. That is the specific gap OmniViewer’s /pvt route exists to close.

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
The first box is 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.
Same box grammar, different brand: 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>.
One key, 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:

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.

And the headline it opens with. A Live Photo writes the capture coordinate twice — once per member, in two structures that share no code. The tab leads with a banner naming every copy it found, by member and by structure, because stripping one is not stripping both. The next two sections are those two structures, in hex. The full argument is in how a Live Photo stores your location twice.

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...............
Tag 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...
Latitude: 0x2e/1 = 46°, 0x1f/1 = 31′, 0x12c0/0x64 = 4800/100 = 48″  →  46.5300
Longitude: 8/1 = , 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
Seven keys in this table; the first is 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/...
Entry index 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.
This is why an EXIF stripper is not enough. The tools that promise to “remove EXIF” understand the first structure. They have no idea the second exists. Clean the photo, hand over the package, and the movie beside it still names the spot.

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.

Including the map that is not there. The METADATA tab prints the coordinate but never plots it against a tile service, and never geocodes it. Fetching a map tile would mean sending a third party the exact position the tab exists to help you find and delete. The page’s content-security policy has no reachable tile host, so it could not happen by accident either.

Related reading

Both members are ordinary formats with toolkits and write-ups of their own:

Open a package yourself

Open the PVT viewer → Drop a .pvt package or a zipped one, press and hold to play it, and read both halves. Nothing is uploaded. Go straight to METADATA → Both members’ fields side by side, with every copy of the capture location named. See the bytes → The hex view above, on your own file — byte-exact, at any size.