A more comprehensive alternative to exiftool when working with GPMD.

If you follow this blog you will have seen me use exiftool to extract telemetry (and other data) from video files many times.

As we work exclusively with GoPro cameras, I wanted to try out another way to do this, specifically designed for GoPro cameras.

Occasionally I have used gpmf-extract from Juan Irache. For a long time I have also wanted to test his gopro-telemetry script which is used to power goprotelemetryextractor.com/.

Here is some useful information and code snippets to help you get started quickly and to understand the key parts of the scripts capabilities.

Follow along

For this example I will be using the following equirectangular video (GS018422.mp4) with GPS enabled, though any GoPro video can be used in the same way. Note, in the case of dual GoPro Fusion fisheyes, the front video file (GPFR) should be used, as this is where the gpmf stream is stored.

1. Install required modules

First-things-first, you’ll need node.js installed. Everything you need to do this is here.

gopro-telemetry takes extracted telemetry from a video file (using gpmf-extract).

To do this, first clone the repository and install the required node modules:

# cloning the repository is not necessary for this tutorial as a clean place for the code
git clone https://github.com/JuanIrache/gopro-telemetry
cd gopro-telemetry
npm init -y
npm i gpmf-extract gopro-telemetry --save

2. Copy the example

In the repository you will find the following example code (that I have slightly changed it with path_to_your_file.mp4 replaced with GS018422.mp4 and output_path.json with GS018422-full-telemetry.json);

const gpmfExtract = require('gpmf-extract');
const goproTelemetry = require(`gopro-telemetry`);
const fs = require('fs');

const file = fs.readFileSync('GS018422.mp4');

gpmfExtract(file)
.then(extracted => {
goproTelemetry(extracted, {}, telemetry => {
fs.writeFileSync('GS018422-full-telemetry.json', JSON.stringify(telemetry));
console.log('Telemetry saved as JSON');
});
})
.catch(error => console.error(error));

Paste this code into a new file GS018422-full-telemetry.js;

vi GS018422-full-telemetry.js 

Now copy GS018422.mp4 into the directory;

pip install gdown
cd samples
# Use test 360 GS018421.360
gdown --id 1SYjVOwQcALg8gQLq8BLLbALEW33PlVT2

3. Run the example

And finally, run the newly created .js file like so;

node GS018422-full-telemetry.js

If all has been successful, you will see a message like this;

Telemetry saved as JSON

And in the directory, you should see a .json file called GS018422-full-telemetry.json (you can modify the filename in GS018422-full-telemetry.js by replacing the value GS018422-full-telemetry.json with your desired filename).

Here is a prettified version of the output file.

4. Examining the output

Depending on the camera, model, settings and accessories used, you will see values reported for various streams, e.g. (ACCL: 3-axis accelerometer, GYRO: 3-axis gyroscope, GPS5: Latitude, longitude, altitude (WGS 84), 2D ground speed, and 3D speed…)

You can find information on what many sensors are called (and what cameras / modes produce data for them) here.

For our work we’re mainly interested values from the GPS, accelerometer and gyroscope sensors. Here are some snippets of how this data is presented (I have added comments into the code to describe some of the values too):

GPS GPS5

"GPS5": {
  "samples": [
    {
      "value": [
        51.2725456, // latitude
        -0.8459696, // longitude
        82.008, // altitude
        0.044, // 2D ground speed
        0.04 // 3D speed
      ],
      "cts": 162.33, // milliseconds since first frame
      "date": "2021-09-04T07:24:07.744Z",
      "sticky": {
        "fix": 3, // 0, 2D, 3D
        "precision": 164, // geometric dilution of precision * 100
        "altitude system": "MSLV"
      }
    },

Which is reported like so (snippet from end of file);

        "name":"GPS (Lat., Long., Alt., 2D speed, 3D speed)",
        "units":["deg","deg","m","m/s","m/s"]},

Accelerometer ACCL

"ACCL": {
  "samples": [
    {
      "value": [
        -9.424460431654676, // x
        -0.47721822541966424, // y
        -1.4004796163069544 // z
      ],
      "cts": 176.875, // milliseconds since first frame
      "date": "2021-09-04T07:24:07.744Z",
      "sticky": {
        "temperature [°C]": 28.021484375
      }
    },

Which is reported like so;

        "name":"Accelerometer (z,x,y)",
        "units":"m/s2"},

Gyroscope GYRO

"GYRO": {
  "samples": [
    {
      "value": [
        -0.07348242811501597, // x
        -0.03194888178913738, // y
        0.003194888178913738 // z
      ],
      "cts": 176.073, // milliseconds since first frame
      "date": "2021-09-04T07:24:07.744Z",
      "sticky": {
        "temperature [°C]": 28.021484375
      }
    },

Which is reported like so;

        "name":"Gyroscope (z,x,y)",
        "units":"rad/s"}

x, y, and z axes

GoPro IMU Orientation

5. Experiment with some custom options

Using the script as defined above will output all available data.

There is also a comprehensive set of options that can also be used in the script to filter the type and date values produced.

To help understand how these can be passed, take a look in the options shown in the readme here.

example.js in the /samples directory shows how some of the options can be used too.

Let us use these in our script by modifying it like so;

const gpmfExtract = require('gpmf-extract');
const goproTelemetry = require(`gopro-telemetry`);
const fs = require('fs');

const file = fs.readFileSync('GS018422.mp4');

gpmfExtract(file)
.then(extracted => {
goproTelemetry(extracted, {
stream: ['GPS5','ACCL'],
GPS5Fix: 3,
GPS5Precision: 500,
WrongSpeed: 50
}, telemetry => {
fs.writeFileSync('GS018422-gps-acl-only.json', JSON.stringify(telemetry));
console.log('Telemetry saved as JSON');
});
})
.catch(error => console.error(error));

Here are the custom options I have used:

  • stream: Filters the results by device stream (often a sensor) name.
    • I am using the GPS stream (GPS5) and the accelerometer stream (ACCL)
  • GPS5Fix: Will filter out GPS5 samples where the type of GPS lock is lower than specified (0: no lock, 2: 2D lock, 3: 3D Lock).
    • I only want a good fix to avoid noise (3D / 3)
  • GPS5Precision: Will filter out GPS5 samples where the Dilution of Precision is higher than specified (this DOP value is * 100, e.g. 500 = 5)
    • I want a fairly good DOP, again for reducing noise (5 / 500)
  • WrongSpeed: will filter out GPS positions that generate higher speeds than indicated in meters per second
    • I want to remove any two consecutive points where speed between them is greater than 50 m/s (180 km/h) as these are clearly erroneous (I was walking when shooting the video)

Now paste this code into a new file called GS018422-gps-acl-only.js;

vi GS018422-gps-acl-only.js

Once this is done, run it in the same way as before;

node GS018422-gps-acl-only.js

You should notice the difference in the two files (GS018422-full-telemetry.json and GS018422-gps-acl-only.json) – mainly the smaller file of the latter, due to the fact it only contains GPS and accelerometer data as defined by the stream filter.

6. Change the output format

You can also change the schema of the output too using the presets available.

This can be useful to use with other software the accepts standard geo formats like .gpx or .kml for example.

Below is an example, again using only the GPS5 stream, but this time exporting to .gpx not .json:

const gpmfExtract = require('gpmf-extract');
const goproTelemetry = require(`gopro-telemetry`);
const fs = require('fs');

const file = fs.readFileSync('GS018422.mp4');

// AS KML

gpmfExtract(file)
.then(extracted => {
goproTelemetry(extracted, {
stream: 'GPS5',
GPS5Fix: 3,
GPS5Precision: 500,
WrongSpeed: 50,
preset: 'kml'
}, telemetry => {
fs.writeFileSync('GS018422-gps-only.kml', JSON.stringify(telemetry));
console.log('Telemetry saved as KML');
});
})
.catch(error => console.error(error));

// AS GPX

gpmfExtract(file)
.then(extracted => {
goproTelemetry(extracted, {
stream: 'GPS5',
GPS5Fix: 3,
GPS5Precision: 500,
WrongSpeed: 50,
preset: 'gpx'
}, telemetry => {
fs.writeFileSync('GS018422-gps-only.gpx', JSON.stringify(telemetry));
console.log('Telemetry saved as GPX');
});
})
.catch(error => console.error(error));

Here I’ve set two outputs using the additon of the preset field, one set as gpx the other as kml, that output to files to GS018422-gps-only.kml and GS018422-gps-only.gpx respectively.

The trkpt’s in the resulting .gpx file are printed like so;

<trkpt lat=\"28.7015115\" lon=\"-13.9204121\">\n              
  <ele>243.304</ele>\n              
  <time>2020-08-02T11:59:05.905Z</time>\n              
  <fix>3d</fix>\n              
  <hdop>107</hdop>\n              
  <cmt>altitude system: MSLV; 2dSpeed: 1.067; 3dSpeed: 0.73</cmt>\n          
</trkpt>

And the Placemark’s in the resulting .kml are printed like so;

<Placemark>\n            
  <description>GPS Fix: 3; GPS Accuracy: 164; altitude system: MSLV; 2D Speed: 0.044; 3D Speed: 0.04</description>\n            
  <Point>\n                
    <altitudeMode>absolute</altitudeMode>\n                
    <coordinates>-0.8459696,51.2725456,82.008</coordinates>\n            
  </Point>\n            
  <TimeStamp>\n                
    <when>2021-09-04T07:24:07.744Z</when>\n            
  </TimeStamp>\n        
</Placemark>\

Hopefully this post has given you enough to get started. Now it is time for you to play with the settings for your own requirements.

A note on videos larger than 2GB

When working with large videos, you will run into an error that looks something like this:

node:fs:419
      throw new ERR_FS_FILE_TOO_LARGE(size);
      ^

RangeError [ERR_FS_FILE_TOO_LARGE]: File size (3027585470) is greater than 2 GB
    at new NodeError (node:internal/errors:371:5)
    at tryCreateBuffer (node:fs:419:13)
    at Object.readFileSync (node:fs:464:14)
    at Object.<anonymous> (/Users/dgreenwood/gfm-telemetry.js:15:17)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47 {
  code: 'ERR_FS_FILE_TOO_LARGE'
}

It is an issue caused mostly by Node’s size limitations.

There is an easy way around this to obtain the telemetry file; make a copy of the input video, stripping out its video and audio streams but keeping the telemetry stream to reduce the filesize. This can be done using ffmpeg like so:

ffmpeg -i INPUT.mp4 -map 0:2 OUTPUT-telemetry-only.MP4

This will reduce a 4GB video to a video of 30MB containing only telemetry.

Note, in this example the telemetry is in the stream 0:2. You should check the correct telemetry stream for your video using ffprobe (because it can vary for cameras and modes used). How to do this is described here.

Now you can run gopro-telemetry on the newly created video (OUTPUT-telemetry-only.MP4) to obtain the telemetry output without issue.

Update 2022-04-21

See a real world use-case of how the .json telemetry can be used in this post; Overlaying GoPro Telemetry Dynamically onto Videos


Posted by:

David G

David G, Trek View Chief Explorer