Fixes the two GoPro-on-Mac annoyances that come from importing via Image Capture:
- imported file dates reset to 1970
- long recordings get split into chaptered files (
GH010369.MP4,GH020369.MP4, ...)
For every chaptered clip found in a source folder, the script:
- Reads the GPS telemetry embedded in the first chapter (position + true UTC time of the first GPS fix, i.e. roughly when recording started).
- Resolves that position to a place name -- a known racetrack if the fix is close to one, otherwise the nearest city -- and a timezone, fully offline (no API key, no internet needed at run time), and converts the timestamp to local time.
- Concatenates all chapters of the clip in order and transcodes them into a single
H.264 MP4 (source resolution/framerate kept, not forced to 1080p), carrying the
original GPS/telemetry track (GPMF, tag
gpmd) through into the output untouched. - Names the result
<City>_<YYYY-MM-DD>_<HH-MM>.mp4, e.g.Munich_2026-08-15_14-34.mp4.
If a clip has no GPS fix (GPS was off), the city becomes UnknownLocation and the
timestamp falls back to the camera's own clock (no timezone correction possible).
brew install ffmpeg exiftool
cd /Users/axel/Github_Projects/GoPro_Videofixer
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txtThis has already been done in this project folder.
Import via Image Capture as usual, then point the script at the destination folder:
./gopro-videofixer ~/Pictures/GoPro-ImportOr point it directly at a mounted camera's DCIM/100GOPRO folder if you mount it as a
drive instead of using Image Capture.
Useful flags:
./gopro-videofixer ~/Pictures/GoPro-Import --list-only # just show groups + filenames, no encoding
./gopro-videofixer ~/Pictures/GoPro-Import --dry-run # print the ffmpeg commands instead of running them
./gopro-videofixer ~/Pictures/GoPro-Import -o ~/Movies/GoPro # custom output folder (default: <source>/Exported)
./gopro-videofixer ~/Pictures/GoPro-Import --quality 75 # higher videotoolbox quality (1-100, default 65)
./gopro-videofixer ~/Pictures/GoPro-Import --encoder libx264 --crf 18 --preset slow # software encode instead
./gopro-videofixer ~/Pictures/GoPro-Import --track-radius-km 0 # never use racetrack names, always nearest city
./gopro-videofixer ~/Pictures/GoPro-Import --format quicktime # standardize to QuickTime's 1080p export preset
./gopro-videofixer ~/Pictures/GoPro-Import --format quicktime --bitrate 20M # ...at a custom bitrate
./gopro-videofixer ~/Pictures/GoPro-Import --overlay-speed # burn in a speed readout
./gopro-videofixer ~/Pictures/GoPro-Import --overlay-speed --speed-unit mph--overlay-speed burns in a speed readout (bottom-left) using the GPS telemetry's own
GPSSpeed reading (the receiver's own Doppler-derived speed, once per second) rather
than deriving it from position deltas, so it's smooth rather than noisy. It's generated
as a temporary SRT track and rendered in with ffmpeg's subtitles filter as part of the
normal encode -- no extra pass. --speed-unit controls kmh (default) vs mph. Clips
with no GPS fix at all are encoded normally with a warning; clips where GPS only locks
partway through will simply show no reading until the first fix.
Two switchable output formats, via --format:
original(default) — keeps the source resolution/framerate, quality-based encode (--quality/--crfdepending on encoder). Smaller files for the same visual quality, but file size and resolution vary per clip.quicktime— matches QuickTime Player's own 1080p export preset: scales down to fit 1920x1080 if the source is larger (never upscales smaller sources, keeps aspect ratio), encodes at a fixed--bitrate(default16M, matching QuickTime's own output) instead of a quality scale, and standardizes audio to 48kHz stereo AAC. Use this when you want consistent, predictable output specs across clips regardless of source resolution.
By default this uses Apple's hardware H.264 encoder (VideoToolbox) via -c:v h264_videotoolbox, since software libx264 on a fanless MacBook Air chokes on GoPro
footage (single-digit fps, 600%+ CPU, thermal throttling on long clips). Hardware
encoding is dramatically faster and uses a fraction of the CPU, at the cost of somewhat
larger files for the same visual quality compared to software x264. Quality is
controlled with --quality (1-100, higher = better/larger; default 65) rather than
CRF. Pass --encoder libx264 to fall back to software encoding with the usual
--crf/--preset controls if you want the best possible compression efficiency and
don't mind the wait.
brew install libgphoto2 gphoto2
./gopro-videofixer --from-cameraThis uses gphoto2 (PTP) to pull files into a temp folder before processing. GoPro
cameras connect as PTP devices, not USB mass storage, so they normally show up in
Image Capture rather than Finder — not seeing it in Finder doesn't mean anything's
wrong.
macOS runs its own PTP-claiming background service (ptpcamerad), which grabs any
connected PTP device within a fraction of a second — including right after being
killed, as long as the camera is still plugged in — and blocks gphoto2 from opening
it ("Could not claim the USB device"). --from-camera handles this automatically
(kills ptpcamerad right before talking to the camera, with one retry), so you
shouldn't need to do anything manually; ptpcamerad restarts on its own afterwards
and Image Capture keeps working normally.
Every --from-camera run currently pulls the camera's entire contents (all photos
and videos, however large the card is) into a fresh temp folder — there's no
incremental "only new files" mode yet. For a card with a big backlog, importing via
Image Capture (or downloading just the files you want with plain gphoto2 --get-file) may be faster for now.
- Clip grouping matches GoPro's chaptering scheme:
G<letter><chapter><clip-id>.MP4(e.g.GH01/GH02/GH03... orGX01/GX02... for HEVC-encoded clips), grouped by the 4-digit clip id and ordered by chapter. - That clip id is just a counter that increments per recording -- it wraps after 9999 and resets if the SD card is reformatted, so it's not a stable unique identifier. If a source folder ends up with footage from different sessions/cards that happen to reuse the same clip id, the script cross-checks that consecutive "chapters" are actually back-to-back (chapter N's camera-clock end time lines up with chapter N+1's start, within 60s) before merging them, and splits them into separate clips instead of silently concatenating unrelated footage if they don't line up.
- The GPS timestamp comes from the telemetry stream (true satellite UTC), not the
file's
CreateDatemetadata atom, since GoPros are known to sometimes write local time into that field. Timezone conversion is derived from the GPS coordinates themselves (timezonefinder), so the printed local time should be correct even if your Mac is set to a different timezone. - City names come from
reverse_geocoder's offline worldwide-cities database — this gives the nearest known city, not a precise reverse-geocoded address. - Racetrack names come from a bundled list (
data/racetracks.json, ~1160 circuits) built from Wikidata. If the GPS fix is within--track-radius-km(default 5km) of a known track, the track name is used instead of the nearest city. Set--track-radius-km 0to always use the nearest city. Wikidata's coordinate for a track is usually a single reference point, not the full circuit outline, so very large circuits may need a bigger radius to be recognized everywhere on track. Runpython3 data/build_racetracks.pyto refresh the list from Wikidata (needs internet); a couple of known-bad upstream coordinates are corrected in that script'sCOORD_FIXEStable (found by spot-checking after a track resolved to the wrong place — Wikidata is crowd-sourced, so more of these may turn up over time). - The output's GPS/telemetry track is preserved by re-muxing the specific
gpmdstream through (-map'd by codec tag,-c:d copy), not by blanket-copying every non-video/audio stream. GoPro files also carry atmcd(timecode) and sometimes anfdsctrack that report as an unsupported codec to ffmpeg's mp4 muxer and fail the whole encode if copied — onlygpmd(the actual GPS/sensor data) is targeted. For multi-chapter clips this needs a stream-copy concat pass first (via the concat demuxer, into a temp file cleaned up afterward) before the quality re-encode, sincegpmdcan't survive afilter_complexgraph the way the old single-pass concat did. Verify any output file still has it:ffprobe -v error -show_entries stream=index,codec_type,codec_tag_string -of default=noprint_wrappers=1 yourfile.mp4 # look for a "codec_tag_string=gpmd" line exiftool -ee -G3 yourfile.mp4 | grep -c GPSLatitude # non-zero = telemetry present
- Audio is only included in the output if every chapter has an audio track; otherwise the output is video-only.
- Safe to re-run/interrupt: before encoding a clip, the script checks whether its
output path already exists and compares its duration against the source chapters'
total duration. A match means it's already fully encoded, so that clip is skipped;
a mismatch (shorter, or unreadable) means it's a truncated leftover from an earlier
crash/interruption, so it's re-encoded in place. Only an existing file that's
longer than the clip could ever produce is left alone as an unrelated file, with
a
-2,-3, ... suffix appended instead (e.g. two different clips starting in the same minute).