Skip to content

Instantly share code, notes, and snippets.

@wjlafrance
Last active February 19, 2023 09:20
Show Gist options
  • Save wjlafrance/d5ceb7957287d37b41fa38f43d71dab8 to your computer and use it in GitHub Desktop.
Save wjlafrance/d5ceb7957287d37b41fa38f43d71dab8 to your computer and use it in GitHub Desktop.
Ingest GoPro videos, extract telemetry with exiftool, and merge/compress with ffmpeg
#------------------------------------------------------------------------------
# File: gpx.fmt
#
# Description: Example ExifTool print format file to generate a GPX track log
#
# Usage: exiftool -p gpx.fmt -ee3 FILE [...] > out.gpx
#
# Requires: ExifTool version 10.49 or later
#
# Revisions: 2010/02/05 - P. Harvey created
# 2018/01/04 - PH Added IF to be sure position exists
# 2018/01/06 - PH Use DateFmt function instead of -d option
# 2019/10/24 - PH Preserve sub-seconds in GPSDateTime value
# 2023/02/19 - WJL Change version to 1.1 to support Garmin VIRB Edit
#
# Notes: 1) Input file(s) must contain GPSLatitude and GPSLongitude.
# 2) The -ee3 option is to extract the full track from video files.
# 3) The -fileOrder option may be used to control the order of the
# generated track points when processing multiple files.
# 4) Coordinates are written at full resolution. To change this,
# remove the "#" from the GPSLatitude/Longitude tag names below
# and use the -c option to set the desired precision.
#------------------------------------------------------------------------------
#[HEAD]<?xml version="1.1" encoding="utf-8"?>
#[HEAD]<gpx version="1.1"
#[HEAD] creator="ExifTool $ExifToolVersion"
#[HEAD] xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
#[HEAD] xmlns="http://www.topografix.com/GPX/1/1"
#[HEAD] xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/0/gpx.xsd">
#[HEAD]<trk>
#[HEAD]<number>1</number>
#[HEAD]<trkseg>
#[IF] $gpslatitude $gpslongitude
#[BODY]<trkpt lat="$gpslatitude#" lon="$gpslongitude#">
#[BODY] <ele>$gpsaltitude#</ele>
#[BODY] <time>${gpsdatetime#;my ($ss)=/\.\d+/g;DateFmt("%Y-%m-%dT%H:%M:%SZ");s/Z/${ss}Z/ if $ss}</time>
#[BODY]</trkpt>
#[TAIL]</trkseg>
#[TAIL]</trk>
#[TAIL]</gpx>
#!/usr/bin/env bb
(require '[clojure.tools.cli :refer [parse-opts]])
(require '[babashka.process :refer [shell]])
(def sibling-files
(set (map #(.getName %)
(file-seq (clojure.java.io/file ".")))))
(defn ingest-video [video-number]
(let [files (filter #(contains? sibling-files %)
(map #(format "GH%02d%s.MP4" % video-number)
(range 1 100)))]
(println "Ingesting files:" files)
(let [gpx-data (->> (shell {:out :string} (str "exiftool -ee -p gpx.fmt " (str/join " " files)))
:out
str/split-lines)
timestamp (->> gpx-data
(map #(re-matches #"\s*<time>(.+)</time>" %))
(remove nil?)
first
second
(filter #(Character/isDigit %))
(apply str))
gpx-filename (str "GoPro-" timestamp ".gpx")
files-filename (str "GoPro-" timestamp ".files")
mp4-filename (str "GoPro-" timestamp ".mp4")]
(println "Writing filelist: " files-filename)
(spit files-filename (str/join "\n" (map #(str "file '" % "'") files)))
(println "Writing GPX file:" gpx-filename)
(spit gpx-filename (str/join "\n" gpx-data))
(println "Ready to run ffmpeg:")
(println (str " $ ffmpeg -f concat -i " files-filename " -vcodec libx265 -crf 30 -acodec copy " mp4-filename)))))
(defn preflight-ingest-video [first-filename]
(if (not (and (= "GH" (subs first-filename 0 2))
(= ".MP4" (subs first-filename 8 12))))
(println "Filename does not match expected pattern.")
(let [video-number (subs first-filename 4 8)]
(if (not (= "01" (subs first-filename 2 4)))
(println "Does not appear to be the first video. Try" (str "GH01" video-number ".MP4"))
(ingest-video video-number)))))
(def required-opts #{:input})
(def cli-options
[["-i" "--input INPUT.MP4" "First video file"
:validate-fn #(contains? sibling-files %)
:validate-msg "File not found in current directory"]])
(defn missing-required?
"Returns true if opts is missing any of the required-opts"
[opts]
(not-every? opts required-opts))
(let [{:keys [options errors]} (parse-opts *command-line-args* cli-options)]
(if errors
(doseq [error errors]
(println error))
(if (missing-required? options)
(println "Missing required option(s).")
(preflight-ingest-video (:input options)))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment