gpx-from-gopro
v0.9.0
Published
Extract a GoPro video's GPS telemetry as GPX or render-agnostic TrackPoints (points + meta + timezone + UTC anchor).
Readme
gpx-from-gopro
Extract a GoPro video's GPS telemetry (GPMF gpmd track) as merged GPX, or as a render-agnostic
telemetry bundle (points + video metadata + recording UTC anchor + timezone). Node ESM. The MP4 is
streamed through mp4box so multi-GB files never load whole into RAM.
Part of the gpx-stabilizer monorepo; builds on the
core gpx-stabilizer package for GPX writing and
point stabilization.
Install
npm install gpx-from-goproCLI
No install needed:
npx gpx-from-gopro <dir|file.mp4> [...] [--out DIR] [--tz HOURS] [--rate HZ] [--cache-dir DIR | --no-cache]
[--organize DIR] [--yes] [--mode core|ski] [--no-gpx]
[--html] [--png [--width N] [--height N]]Once installed (npm install [-g] gpx-from-gopro), drop the npx prefix and just run
gpx-from-gopro ....
Recurses directories for video files, groups by camera body (serial, falling back to filename
family) + local date, and writes one merged <YYYYMMDD>-<family>.gpx per group — within it, points
split into one <trkseg> per recording session (keyed on the filename file-number, with a time-gap
split for restarts/dropouts). A per-file extraction cache (keyed by size+mtime+rate) lets a killed
run resume without re-extracting. --rate HZ downsamples from the native ~18 Hz; --tz HOURS
overrides the longitude-guessed local date. --mode core|ski runs each session through
gpx-stabilizer's stabilizeTrack before writing
(omit for the raw extraction). --no-gpx skips writing the merged .gpx entirely — extraction,
caching, and (with --organize) reorganizing the source videos still happen normally; --html/
--png are unaffected either way (both render from the raw extracted points, not the .gpx file).
--organize DIR — reorganize the source videos to match the GPX
After every .gpx above has been written, moves each source video into
<DIR>/<group>/<session>/ — the same <date>-<family> group naming as the .gpx file, further
split into one folder per recording session (the filename file-number; no-session/ when a file
has none). Each file's extraction cache moves alongside (never deleted — it's free to keep and
costly to lose); the group's .gpx moves in too, unless --out was explicitly given (then it
stays where you put it). Never overwrites an existing destination file.
Always previews the full plan first, then asks before touching anything — including whether
found .LRV/.THM sidecar files (GoPro's per-chapter low-res preview / thumbnail) should be
deleted (default) or moved alongside. --yes skips both prompts (sidecars default to deleted); a
non-interactive stdin without --yes does nothing rather than hang waiting for input.
--html / --png — eval view of each merged group
Additive to the .gpx output (never instead of it): renders each group's merged track through
the same analyzed view gpx-stabilizer's own CLI uses (clean track + drop markers, hdop
overlays), so a group can be eyeballed right after extraction with no separate
gpx-stabilizer --html/--png pass on the merged .gpx. --html writes one
<out>/gopro-view.html (one scrolling panel per group); --png writes one
<out>/<group>.png per group (--width/--height, default 1280×720; needs
@resvg/resvg-js — see gpx-stabilizer's png.js).
Library — telemetry export
A render-agnostic API (telemetry samples + video metadata + recording UTC anchor + timezone) for a
renderer to consume. Full contract:
docs/export-contract.md.
import { readGoproTelemetry } from "gpx-from-gopro";
// one call: probe + extract [+ stabilize] + timezone + start anchor
const { meta, points, timezone, startUtc } = await readGoproTelemetry("clip.mp4", {
rate: 1, // Hz; omit for native ~18 Hz
stabilize: true, // clean the points first (boolean | StabilizeOptions)
});Short-circuits to empty points / null timezone / null startUtc on a video with no GPS track.
Also exported:
probeGoproMeta(path)— cheap moov-only probe (geometry / fps / duration /hasGpsgate).extractGoproPoints(path, { rate? })— rawTrackPoint[](native ~18 Hz;ratein Hz to downsample).timezoneAt({ lat, lon })/timezoneOfPoints(points)— offline IANA timezone lookup.recordingStartUtc(points)—{ startUtc, fix }from the first good-fix sample.stabilize— re-exported from the core package for convenience.
License
MIT
