Desktop application for DJI drone footage: drag a folder in and get
interactive maps of your flights and photos (including a 360-degree
panorama viewer), or embed the flight telemetry from DJI .SRT logs into
the video files so photo apps can sort and place them.
This installer bundles everything the app needs (FFmpeg, ExifTool, the
dji-embed command-line engine) and installs per-user with no admin
rights required.
Note: this package also puts the dji-embed command on PATH. Install
either this desktop package or the portable CLI package
(CallMarcus.DJIMetadataEmbedder) - not both.
README
DJI Drone Metadata Embedder
A free tool that puts your DJI drone footage on the map — literally. It reads
the telemetry DJI records with every flight (GPS position, altitude, camera
settings) and turns it into interactive maps, standard GPS files, and location
metadata embedded in the videos themselves.
See every flight on one map — point dji-embed flightmap at a folder of
footage and get a single interactive map with each flight as its own
coloured track (experimental).
See where every photo was taken — dji-embed photomap pins a whole
folder of stills on one clustered map, thumbnails included; 360° panoramas
get their own marker color and toggle, and open in an interactive viewer.
Make videos searchable by location — dji-embed embed writes the GPS
data into the video files so Windows Photos, Google Photos, and similar apps
can find them by place. No re-encoding, no quality loss, and the full
telemetry is preserved as a subtitle track for overlays in video editors.
Export flight tracks — GPX for Google Earth, CSV for spreadsheets,
GeoJSON/KML for GIS tools, CoT for ATAK/TAK
(see docs/geospatial.md and
docs/fmv-interop.md).
Verify footage — see what metadata files already carry
(dji-embed check), and cross-check shadows in the footage against the
astronomical sun (dji-embed verify-sun).
Keep locations private when sharing — --redact drops or coarsens GPS
data to ~100 m in any output.
Works on Windows, macOS, and Linux; whole folders are processed in one go,
with a progress bar. .DAT flight logs can be merged in where available.
Get started
You'll need Python 3.10+ and FFmpeg (ExifTool is optional, but unlocks the
photo map and a few other features). On Windows the bootstrap script below
installs all of it for you.
Windows
Download the installer (dji-metadata-embedder-setup-.exe) from the
latest release
and run it — you get the DJI Metadata Embedder desktop app in the Start
menu plus the full dji-embed command line in any terminal, with FFmpeg and
ExifTool bundled. No admin rights needed.
The app gives you all of it without a terminal — drop a folder, pick a
mode, and the finished map opens right in the app:
Direct download: grab the ready-to-run dji-embed.exe from the
GitHub Releases page.
Windows Package Manager:
winget install CallMarcus.DJIMetadataEmbedder
Already installed? Upgrade with winget upgrade CallMarcus.DJIMetadataEmbedder.
winget installs the portable dji-embed.exe only — install FFmpeg and
ExifTool separately (winget install Gyan.FFmpeg OliverBetz.ExifTool), or
use the bootstrap script above, which bundles them.
Python package:
pip install dji-drone-metadata-embedder
If the command python is not recognized on Windows, use py instead.
Verify your download (optional)
Windows binaries and the installer are Authenticode code-signed from v1.23.0
onwards — right-click the file → Properties → Digital Signatures and you
should see "Open Source Developer, Marcus Westermark" (issued by Certum).
Every release also ships SHA256SUMS files alongside the binaries, and
release builds (v1.23.0 onwards) carry build provenance attestations
— a Sigstore-signed proof that the file you downloaded was produced by this
repository's public build workflow. With the GitHub CLI:
The same works for dji-embed.exe. PyPI wheels are attested separately by
PyPI's Trusted Publishing — see the
"provenance" details on each file at
pypi.org.
docker run --rm -v "$PWD":/data callmarcus/dji-embed -i *.MP4
Build from source with uv sync --extra dev (or pip install -e .)
Use the provided Dockerfile for custom images
Review CI scripts under .github/workflows
Your first map
dji-embed doctor # 1. confirm everything is installed
dji-embed flightmap /path/to/footage # 2. all flights in a folder -> flightmap.html
dji-embed photomap /path/to/photos # 3. all photos in a folder -> photomap.html
Open the resulting .html file in any browser — done. The maps embed your
data but load the background map tiles from the internet, so they need a
connection to render.
No terminal at all? On Windows, drag your footage folder onto
dji-embed.exe — it maps every flight log (and geotagged photos) in the
folder, including subfolders, and opens the result in your browser. The same
works in a terminal by passing just a folder: dji-embed /path/to/footage.
Prefer clicking over typing? On Windows, install the desktop app
(see above) — folder in, map or telemetry out, no terminal. For viewing
maps there's also dji-embed photomap --serve.
> Privacy note: maps and exports reveal where you fly. Share them
> deliberately, or add --redact fuzz to coarsen every position to ~100 m.
Don't see your model? We're happy to add support in exchange for sample SRT files we can parse the telemetry format from. Open an issue and attach one or two raw .SRT files from your drone (a short clip is plenty), and we'll add a parser for it. See the Contributing Guide for details.
Sidecar-less models (Air 3S, Mini 5 Pro, …): telemetry is read straight from
the MP4's embedded djmd/dbgi track via ExifTool — no .SRT needed —
for dji-embed convert FILE.MP4 and dji-embed verify-sun FILE.MP4.
Requires a recent ExifTool (Air 3S ≥ 13.39, Mini 5 Pro ≥ 13.52); see
docs/MP4_TIMED_METADATA.md.
Intended use & scope
This is a tool for transparency and accountability. Drone telemetry is
dual-use, and this project deliberately focuses on the open side of that:
verifying and documenting footage, georeferencing it for mapping, and making it
interoperable with open GIS workflows. The uses we build for include
open-source verification and journalism, human-rights and conflict
documentation, search-and-rescue and disaster response, humanitarian damage
assessment, environmental monitoring, agriculture, and infrastructure
inspection.
Feature and documentation decisions favor provenance, verification,
georeferencing, and standards interoperability. We do not build targeting or
other offensive capabilities.
Command reference
Everything below is also available at the terminal via
dji-embed COMMAND --help. Scripting or building a frontend? photomap,
flightmap, embed, and check accept --progress jsonl — one JSON
event per line on stdout, documented in
docs/PROGRESS_JSONL.md.
dji-embed [OPTIONS] COMMAND [ARGS]...
Commands:
embed Embed telemetry from SRT files into MP4 videos
validate Validate SRT/MP4 pairs and report drift
convert Convert SRT telemetry to GPX, CSV, GeoJSON, KML, HTML, or CoT
flightmap Map every flight in a folder of SRT logs on one combined map
photomap Map GPS-tagged still photos to an HTML/KML/GeoJSON map
check Check media files for embedded metadata
doctor Show system information and verify dependencies
ui Launch the legacy local web UI (deprecated)
verify-sun Summarise the sun's position over a clip for shadow cross-checking
Global Options:
--version Show the version and exit
-h, --help Show this message and exit
dji-embed embed - Process Videos
Embed telemetry from SRT files into the matching MP4 videos. Processing shows
a progress bar for each file; results go to a processed subdirectory (see
Output).
dji-embed embed [OPTIONS] DIRECTORY
Arguments:
DIRECTORY Directory containing MP4 and SRT files
Options:
-o, --output DIRECTORY Output directory (ignored if --overwrite)
--overwrite Overwrite original video files in place
(destination = input folder)
--exiftool Also use ExifTool for GPS metadata
--dat PATH DAT flight log to merge
--dat-auto Auto-detect DAT logs matching videos
--audio-sidecar Auto-detect a same-basename .m4a audio sidecar
(e.g. DJI Neo 2) and mux it in (no re-encode)
--redact [none|drop|fuzz] Redact GPS coordinates (default: none)
--container [mp4|mkv] Output container; 'mkv' preserves DJI djmd/dbgi
data streams (default: mp4)
--extract-home Extract the drone's HOME / launch point into the
JSON sidecar. **The HOME point is the operator's
launch location** — it is off by default, never
written to the MP4, and always honours `--redact`
(`drop` removes it, `fuzz` coarsens to ~100 m).
--progress [jsonl] Emit machine-readable progress events on stdout,
one JSON object per line (docs/PROGRESS_JSONL.md)
-v, --verbose Verbose output
-q, --quiet Suppress progress output
dji-embed validate - Check SRT/MP4 Sync
Validate SRT/MP4 pairs and generate a drift analysis report — useful when
subtitles seem out of step with the video.
dji-embed validate /path/to/footage
All options
dji-embed validate [OPTIONS] DIRECTORY
Options:
--drift-threshold FLOAT Drift threshold in seconds for warnings
--format [text|json] Output format for drift report
-v, --verbose Verbose output
-q, --quiet Suppress info output
dji-embed convert - Export Telemetry
Convert SRT telemetry to GPX, CSV, GeoJSON, KML, CoT, or a standalone HTML
map. On sidecar-less models (Air 3S, Mini 5 Pro, …) the input can be the MP4
itself.
dji-embed convert gpx DJI_0001.SRT # GPS track for Google Earth
dji-embed convert csv DJI_0001.SRT -o telemetry.csv
dji-embed convert gpx /path/to/srt/files --batch # whole directory in one go
dji-embed convert html DJI_0001.SRT # single-flight map -> DJI_0001.html
dji-embed convert html DJI_0042.MP4 # sidecar-less models: telemetry from the MP4
GPX/CoT timestamps are written in UTC. DJI SRT times are local with no
timezone, so the offset is auto-detected from the file's modification time.
Override it when the mtime is unreliable (e.g. copied files):
dji-embed convert [OPTIONS] {gpx|csv|geojson|kml|html|cot} INPUT
Arguments:
{gpx|csv|geojson|kml|html|cot} Output format
INPUT SRT file or directory to convert
Options:
-o, --output PATH Output file path
-b, --batch Batch process directory
--tz-offset OFFSET UTC offset for GPX/CoT timestamps, e.g. '+05:30' or
'-8' ('auto' detects from file mtime; default: auto)
--redact [none|drop|fuzz] GPS redaction (default: none), applied to the track
in every format: drop removes the track (or blanks
the GPS+sun columns in csv, rows kept); fuzz
coarsens to ~100 m. HOME marker redacted the same way
--interval FLOAT cot only: seconds between sampled points (default: 1.0)
--cot-type CODE cot only: CoT type/affiliation code (default: a-n-A)
--footprint geojson/kml only: add camera footprint polygons
(requires --redact none)
--footprint-interval FLOAT geojson/kml only: seconds between footprints (default: 2.0)
--model NAME footprint FOV-table entry, e.g. air3, mini4pro
--extract-home Opt-in: extract the HOME / launch point (operator
location) into gpx/csv/geojson output. Off by
default; subject to --redact
-v, --verbose Verbose output
-q, --quiet Suppress info output
Camera footprints — overlay per-interval ground-coverage polygons in
GeoJSON/KML for down-looking footage (suppressed under --redact; see
docs/geospatial.md):
Map every flight in a folder of DJI .SRT logs on one combined map. Reads
only the .SRT telemetry sidecars — the videos are never opened and no
external tool is needed — so scanning a large archive is fast. Each flight
becomes its own coloured track with a popup (start time, duration, altitude
range, GPS points) and a layer toggle; the KML imports into Google Earth and
Google My Maps as one line per flight. SRT files without GPS telemetry
(e.g. ordinary subtitles) are skipped and counted in a summary; -v lists them.
> Experimental:flightmap is new and its size-split joining heuristics
> may still be tuned based on real-world feedback. If it joins flights it
> shouldn't (or misses ones it should), please
> open an issue
> with the SRT file names and timestamps.
Prefer one map per clip? dji-embed convert html /path/to/footage --batch
writes a separate .html next to each video.
All options
dji-embed flightmap [OPTIONS] DIRECTORY
Arguments:
DIRECTORY Directory containing .SRT flight logs
Options:
-o, --output FILE Output file; used as the base name when
--format all
-f, --format [html|kml|geojson|all]
Map output format (default: html)
-r, --recursive Scan subdirectories too
--title TEXT Map title (default: directory name)
--redact [none|fuzz] GPS redaction: fuzz coarsens every flight to
~100 m before writing (default: none)
--join-gap SECONDS Chain size-split recordings (DJI starts a new
file at the 4 GB limit) into one flight when
the next file's telemetry starts within
SECONDS and resumes where the previous file
ended. 0 disables joining (default: 15.0)
--tz-offset OFFSET UTC offset of the SRT timestamps, e.g.
'+05:30' or '-8'. 'auto' detects it from each
file's mtime; pass it explicitly when the
files were copied through zip/cloud transfers
that rewrote the mtimes (default: auto)
--progress [jsonl] Emit machine-readable progress events on
stdout, one JSON object per line
(docs/PROGRESS_JSONL.md)
-v, --verbose Verbose output
-q, --quiet Suppress info output
Notes:
With -r, flights are labelled by their path relative to DIRECTORY
(session1/DJI_0001), so per-session folders that reuse DJI's restarting
file numbering stay distinct.
Long recordings that DJI split at the 4 GB file limit are stitched back
into one flight when the next file's telemetry continues (in time and
position) where the previous one ended — measured on the SRT's own
timestamps, so it survives copied files with rewritten mtimes. The popup
lists the joined files; tune or disable with --join-gap.
Tracks are thinned to ~1 GPS point per second for the map (DJI logs ~30
per second) — visually identical but far smaller files; use
dji-embed convert on a single flight when you need every sample.
Popup start times are converted to UTC using each file's mtime. On archives
whose mtimes were rewritten (zip/cloud transfers) the tool warns once and
falls back to the mtime; pass --tz-offset with your recording timezone to
get correct absolute times. Joining itself is unaffected either way.
Sidecar-less models whose telemetry lives inside the MP4 (Air 3S,
Mini 5 Pro, …) are not scanned; map those per clip with
dji-embed convert html VIDEO.MP4.
Leaflet and the OpenStreetMap basemap tiles load from the internet; the
flight data itself is embedded, so the HTML file is portable but needs a
connection to render. A flight map publishes where you fly — share it
deliberately, or use --redact fuzz.
--3d renders the flights over real 3D terrain (MapLibre + Mapterhorn/
Copernicus, keyless) as a separate flightmap-3d.html, instead of the
flat basemap.
dji-embed photomap - Map Still Photos
Map GPS-tagged still photos (JPG/JPEG/DNG) as an HTML, KML, or GeoJSON map.
Requires ExifTool (dji-embed doctor checks it). Photos without GPS data are
skipped and counted in a summary; -v lists the skipped filenames. On the
HTML map, regular photos are blue markers and 360° panoramas orange ones,
with a checkbox to show/hide each type when a folder mixes both.
dji-embed photomap /path/to/photos # -> photos/photomap.html
dji-embed photomap /path/to/photos -f all # -> photos/photomap.{html,kml,geojson}
dji-embed photomap /path/to/photos -r --title "Churches of Finland" # recurse + custom title
dji-embed photomap /path/to/photos --link-originals # popups open the original photos
dji-embed photomap /path/to/photos --link-originals --link-base ../DCIM # originals live elsewhere
dji-embed photomap /path/to/photos --redact fuzz # ~100 m coarsened pins
dji-embed photomap /path/to/photos --popup-fields none # popups show thumbnails only
dji-embed photomap /path/to/photos --serve # serve + open browser (360° viewer works)
All options
dji-embed photomap [OPTIONS] DIRECTORY
Arguments:
DIRECTORY Directory containing JPG/JPEG/DNG photos
Options:
-o, --output FILE Output file; used as the base name when
--format all
-f, --format [html|kml|geojson|all]
Map output format (default: html)
-r, --recursive Scan subdirectories too
--title TEXT Map title (default: directory name)
--link-originals HTML popups link the thumbnail/filename to
the original photo file
--link-base PREFIX Folder or URL prefix for --link-originals
hrefs, for when the originals do not sit
beside the HTML
--popup-fields LIST Limit what HTML popups show: 'none' or a
comma list of name, timestamp, camera,
altitude (default: all of them)
--redact [none|fuzz] Coarsen every photo location to ~100 m
before writing (default: none)
--serve Serve the map on 127.0.0.1 and open the
browser (implies --link-originals; not
combinable with --progress jsonl)
--progress [jsonl] Emit machine-readable progress events on
stdout, one JSON object per line
(docs/PROGRESS_JSONL.md)
-v, --verbose Verbose output
-q, --quiet Suppress info output
Notes:
With -r, pins are labelled by their path relative to DIRECTORY, so
per-session folders that reuse DJI's restarting DJI_0001.JPG names stay
distinct on the map.
Popup previews use the small EXIF thumbnail, falling back to a size-capped
embedded preview for DNGs that carry no thumbnail.
A Hover previews toggle in the map's top-right corner (off by default,
mouse devices only, remembered by the browser) shows each pin's thumbnail
and filename in a tooltip, so a map can be skimmed without clicking every
pin (HTML output).
Photos with no EXIF altitude are clamped to the ground in KML (Google Earth)
instead of being buried at 0 m below terrain.
--link-originals makes each popup's thumbnail and filename a click-through
to the full-resolution original (HTML output only). The links are plain
relative hrefs, so they resolve while the HTML sits next to the photos and
break if the map is moved or emailed without them — the popup always names
the file regardless. Use --link-base when the originals live elsewhere
(a relative folder like ../DCIM, or an absolute URL). Browsers download
rather than display DNG files; JPGs open in a new tab.
360° panoramas (DJI, Insta360, Google Camera, …) are detected during the
same scan; clicking one opens an embedded interactive viewer instead of a
flat, distorted JPEG when the map is served with --serve (or opened over
HTTP with --link-originals) — browsers block the viewer on maps opened
straight from disk.
--redact fuzz coarsens every pin to ~100 m before the map is written —
use it for maps you plan to share. Combined with --link-originals, the
linked originals still carry exact GPS in their EXIF, so share those
deliberately too.
Leaflet and the OpenStreetMap basemap tiles load from the internet; the
photo thumbnails themselves are embedded, so the HTML file is portable but
needs a connection to render. A photo map publishes your shooting
locations — share it deliberately.
dji-embed check - Check Existing Metadata
Check whether videos or photos already contain GPS or altitude information.
check uses ffprobe for QuickTime tags and exiftool for EXIF data
when available. Pass --verbose for debug output or --quiet to only
show warnings and errors. --progress jsonl switches stdout to
machine-readable events (see docs/PROGRESS_JSONL.md).
dji-embed verify-sun - Sun-Position Cross-Check
Summarise the sun's azimuth/elevation over a clip so analysts can compare
shadow direction and length in the footage against the astronomical sun.
Accepts an .SRT sidecar or, on sidecar-less models, the MP4 itself.
CSV export (dji-embed convert csv) gains matching datetime_utc /
sun_azimuth / sun_elevation columns.
dji-embed verify-sun [OPTIONS] SRT
Options:
--tz-offset OFFSET UTC offset for the SRT timestamps, e.g. '+05:30' or
'-8'. 'auto' detects it from the SRT file mtime
(default: auto)
--format [text|json] Output format (default: text)
-v, --verbose Verbose output
-q, --quiet Suppress info output
dji-embed doctor - System Diagnostics
Show system information and verify all dependencies.
dji-embed doctor
dji-embed doctor --install exiftool downloads a pinned, checksum-verified
ExifTool into your user directory — useful where system packages are too old
for MP4 timed metadata (most Linux distros).
Opt-in update check. Normal commands never touch the network. doctor
is the only command that may go online for version info, and it asks first:
the first interactive run prompts once (Check online for newer versions (PyPI + ExifTool)? [y/N], default No) and remembers your answer. When
enabled, doctor compares your dji-embed against PyPI and your ExifTool
against the bundled pin, and prints an upgrade command matched to how you
installed (winget/EXE, pipx, or the exact python -m pip for your
interpreter). Network failures degrade silently to the offline report.
dji-embed doctor --online # enable for this run and remember it
dji-embed doctor --offline # disable for this run and remember it
Set DJIEMBED_NO_UPDATE_CHECK=1 to hard-disable the check regardless of the
remembered choice. Non-interactive runs (no terminal, or CI set) never
prompt and never check. The ExifTool-vs-pin comparison needs no network and
is always shown when your ExifTool is older than the pinned release.
Output
The embed command creates a processed subdirectory containing:
*_metadata.MP4 - Video files with embedded metadata and telemetry subtitles
*_telemetry.json - Flight summary with GPS data, altitude, and camera settings
SRT Parsing: Extracts telemetry data from DJI SRT subtitle files
Metadata Embedding: Uses FFmpeg to:
Add SRT as subtitle track (preserves all telemetry)
Embed GPS coordinates in video metadata
Add altitude and other metadata tags
No Re-encoding: Uses stream copy for fast processing without quality loss
Summary Generation: Creates JSON files with flight statistics
The tool recognises multiple DJI SRT dialects, from the bracketed
[latitude: 59.302335] [longitude: 18.203059] style of recent models to the
legacy GPS(59.302335,18.203059,132.860) form — the full catalogue lives in
docs/SRT_FORMATS.md.
Using Claude, ChatGPT, Copilot, or another AI tool? Upload
HELP.md to it first — it primes the assistant with accurate,
current facts about this tool so you can ask questions in your own words
("how do I map my photos?") and get answers that match how it actually works.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
Working with an AI coding agent? Point it at AGENTS.md — the
conventions, test gates, and invariants contributions must follow.
If your DJI model uses a different SRT format, we're happy to add support in exchange for sample SRT files we can parse the telemetry format from:
Open an issue and attach one or two raw .SRT files from your drone (a short clip is plenty) — we'll add a parser for it
Or submit a PR with regex patterns for the new format
Release
See docs/RELEASE.md for instructions on publishing a new version.
There are also plans to grow this CLI tool into a Windows application with a
graphical interface — see the Development Roadmap.
License
MIT License - see LICENSE file for details
Acknowledgments
Thanks to the DJI drone community for format documentation
FFmpeg and ExifTool teams for their excellent tools