---
title: "Extract the GPS track from a GoPro video in C#"
url: https://recursiveai.org/onemedia/cookbook/extract-gps-track-from-gopro-video/
description: "Extract the GPS track from a GoPro video in C# and save it as GPX: OneMedia decodes the GPMF telemetry track (also CAMM, Apple mebx and DJI) into typed GPS, accelerometer and gyroscope samples. Checked point for point against ExifTool. A preview."
version: "0.6.0-preview.1"
---

# Extract the GPS track from a GoPro video in C#

`media.Telemetry` decodes the sensor data a camera records next to the video into typed channels: GPS fixes with time, altitude and speed, accelerometer and gyroscope readings, camera settings. This program writes the GPS fixes of a GoPro clip to a GPX file. Telemetry is a preview in 0.6.0-preview.1.

```shell
dotnet new console -o GoProGps && cd GoProGps
dotnet add package RecursiveAI.OneMedia --version 0.6.0-preview.1
curl -L -o hero6.mp4 https://raw.githubusercontent.com/gopro/gpmf-parser/9a7150632892c7356c91145c889016f07b0ed48d/samples/hero6.mp4
```

`hero6.mp4` is a 23-second clip recorded on a GoPro HERO6 Black, from GoPro’s own [gpmf-parser](https://github.com/gopro/gpmf-parser) repository, which GoPro publishes under the Apache License 2.0 or the MIT licence (the samples carry no separate terms). Put this in `Program.cs` and run `dotnet run`:

```csharp
using System.Globalization;
using System.Xml.Linq;
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Metadata;

using MediaFile media = Media.Open("hero6.mp4");

// Every telemetry stream in the file, decoded to typed samples.
foreach (TelemetryChannel channel in media.Telemetry)
    Console.WriteLine($"{channel.Format} {channel.Kind,-14} {channel.Name}: {channel.Samples.Count} samples");

List<GpsSample> fixes = media.Telemetry
    .Where(c => c.Kind == TelemetryKind.Gps)
    .SelectMany(c => c.Samples.OfType<GpsSample>())
    .ToList();

// Write them as a GPX track: latitude, longitude, altitude and GPS time.
XNamespace gpx = "http://www.topografix.com/GPX/1/1";
string F(double v) => v.ToString("R", CultureInfo.InvariantCulture);
var segment = new XElement(gpx + "trkseg", fixes.Select(f => new XElement(gpx + "trkpt",
    new XAttribute("lat", F(f.Latitude)), new XAttribute("lon", F(f.Longitude)),
    f.Altitude is double alt ? new XElement(gpx + "ele", F(alt)) : null,
    f.GpsTime is DateTime t ? new XElement(gpx + "time", t.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)) : null)));
var doc = new XElement(gpx + "gpx", new XAttribute("version", "1.1"), new XAttribute("creator", "OneMedia"),
    new XElement(gpx + "trk", segment));
File.WriteAllText("hero6.gpx", "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + doc);

GpsSample first = fixes[0], last = fixes[^1];
Console.WriteLine($"{fixes.Count} fixes written to hero6.gpx");
Console.WriteLine($"first: {first.Latitude}, {first.Longitude}, {first.Altitude} m at {first.GpsTime:O} ({first.Fix})");
Console.WriteLine($"last:  {last.Latitude}, {last.Longitude}, {last.Altitude} m at {last.GpsTime:O}");

// Once the telemetry has been read, its first fix also counts as location evidence.
foreach (FactCandidate<GeoLocation> c in media.Location.Candidates)
    Console.WriteLine($"location candidate: {c.Value.Latitude}, {c.Value.Longitude} from {c.Source}");
```

```text
gpmf Accelerometer  ACCL: 4667 samples
gpmf Gyroscope      GYRO: 4668 samples
gpmf Gps            GPS5: 417 samples
gpmf Scalar         FCNM: 344 samples
gpmf CameraSettings ISOE: 689 samples
gpmf CameraSettings SHUT: 689 samples
gpmf CameraSettings WBAL: 689 samples
gpmf Scalar         WRGB: 689 samples
gpmf Scalar         FACE: 55 samples
417 fixes written to hero6.gpx
first: 33.126515, -117.3271676, -17.228 m at 2018-01-24T19:27:58.6740000Z (Fix3D)
last:  33.1266036, -117.3273038, -19.54 m at 2018-01-24T19:28:21.6193889Z
location candidate: 33.1265, -117.3272 from udta:©xyz
location candidate: 33.126515, -117.3271676 from telemetry:gpmf:GPS5
```

```shell
head -12 hero6.gpx
```

```text
<?xml version="1.0" encoding="utf-8"?>
<gpx version="1.1" creator="OneMedia" xmlns="http://www.topografix.com/GPX/1/1">
  <trk>
    <trkseg>
      <trkpt lat="33.126515" lon="-117.3271676">
        <ele>-17.228</ele>
        <time>2018-01-24T19:27:58.674Z</time>
      </trkpt>
      <trkpt lat="33.1265159" lon="-117.3271672">
        <ele>-17.231</ele>
        <time>2018-01-24T19:27:58.732Z</time>
      </trkpt>
```

The location fact now has two candidates: the position GoPro writes as text in the user data (`©xyz`, four decimal places), which ranks first, and the first telemetry fix. GoPro writes GPS at about 18 Hz in 23 one-second payloads; each payload’s samples are spread evenly over its time, and the GPS time is the payload’s `GPSU` time plus the same spread. `media.OpenTelemetry(track)` is the pull reader for long files: `media.Telemetry` reads the whole track into memory.

## Checked against ExifTool

ExifTool 13.60 reads the same GPS5 samples with `-ee`. This compares every point of the GPX file with its output:

```shell
exiftool -n -ee -p '$GPSLatitude,$GPSLongitude,$GPSAltitude' hero6.mp4 2>/dev/null > exiftool.csv
cat > compare.py <<'EOF'
import csv, xml.etree.ElementTree as ET
ns = {"g": "http://www.topografix.com/GPX/1/1"}
ours = [(float(p.get("lat")), float(p.get("lon")), float(p.find("g:ele", ns).text))
        for p in ET.parse("hero6.gpx").iterfind(".//g:trkpt", ns)]
theirs = [tuple(map(float, row)) for row in csv.reader(open("exiftool.csv"))]
print(f"OneMedia {len(ours)} points, ExifTool {len(theirs)} points")
for i, name in enumerate(["latitude", "longitude", "altitude"]):
    print(f"largest {name} difference: {max(abs(a[i] - b[i]) for a, b in zip(ours, theirs)):.1e}")
EOF
python3 compare.py
```

```text
OneMedia 417 points, ExifTool 417 points
largest latitude difference: 0.0e+00
largest longitude difference: 0.0e+00
largest altitude difference: 0.0e+00
```

The same 417 points, equal in latitude, longitude and altitude. (ExifTool prints a Perl warning about a GoPro `AACD` field on this file; `2>/dev/null` hides it.)

## Other cameras

The same call reads Google’s CAMM track (phones and 360° cameras), Apple’s QuickTime timed metadata (`mebx`), DJI’s per-frame text and `djmd` tracks, and the GPS in AVCHD camcorder data; the [telemetry page](https://recursiveai.org/onemedia/docs/telemetry/) gives the channel layout of each. They were not run for this page. In the README’s words, telemetry was checked on “10 published GoPro clips (HERO5 to HERO8, Fusion, MAX, Karma) against ExifTool (equal counts, values within 1e-13) and against GoPro’s own `gpmfdemo` release binary”, with “CAMM and `mebx` fixtures against ExifTool and ffprobe”; “No real DJI telemetry file was available”, and for CAMM `time_gps_epoch` “no real CAMM device file was available to settle it”.

## Limits

- **A preview, not checked on device recordings.** The README’s wording for the facts layer applies to telemetry too: “Checked against ExifTool 13.60 and MediaInfo 26.10 on published samples and on files we generate, not on a corpus of device recordings.” Its API may change.
- **Not compared value by value:** the GoPro HUES, FACE, SCEN, MFGI and VPTS streams; they come out as raw items with their byte ranges.
- **Reading only.** To remove the GPS from a file, see [Remove GPS location and metadata from a video](https://recursiveai.org/onemedia/cookbook/remove-gps-location-from-video/).
- **Sidecars.** A DJI `.SRT` file next to the video is read with `DjiSrt.Read(path)`, not through `media.Telemetry`.
