Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GoPro Videofixer

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:

  1. Reads the GPS telemetry embedded in the first chapter (position + true UTC time of the first GPS fix, i.e. roughly when recording started).
  2. 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.
  3. 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.
  4. 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).

Setup (one-time)

brew install ffmpeg exiftool
cd /Users/axel/Github_Projects/GoPro_Videofixer
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txt

This has already been done in this project folder.

Usage

Import via Image Capture as usual, then point the script at the destination folder:

./gopro-videofixer ~/Pictures/GoPro-Import

Or 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

Speed overlay

--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.

Output format

Two switchable output formats, via --format:

  • original (default) — keeps the source resolution/framerate, quality-based encode (--quality/--crf depending 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 (default 16M, 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.

Encoder

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.

Optional: read straight from the camera

brew install libgphoto2 gphoto2
./gopro-videofixer --from-camera

This 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.

Notes / limitations

  • Clip grouping matches GoPro's chaptering scheme: G<letter><chapter><clip-id>.MP4 (e.g. GH01/GH02/GH03... or GX01/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 CreateDate metadata 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 0 to 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. Run python3 data/build_racetracks.py to refresh the list from Wikidata (needs internet); a couple of known-bad upstream coordinates are corrected in that script's COORD_FIXES table (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 gpmd stream through (-map'd by codec tag, -c:d copy), not by blanket-copying every non-video/audio stream. GoPro files also carry a tmcd (timecode) and sometimes an fdsc track that report as an unsupported codec to ffmpeg's mp4 muxer and fail the whole encode if copied — only gpmd (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, since gpmd can't survive a filter_complex graph 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).

About

Fixes the problem that on Mac, GoPro videos will appear broken into chapters and with a date from 1970

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages