Do you have loads of GoPro movies eating up disk space? Looking for a way to compress them, but in such a way that quality is still very high and they continue to play nice with GoPro software, like Quik? I did too.

I've owned a GoPro Hero 3+ and now more recently a Hero 5. With the right conditions and settings it takes some really nice video, but sometimes they're just a little bit too space hungry for my liking.

Whether you are uploading videos to the cloud, streaming them via a home NAS, or just short on drive space, you might find yourself wanting to reduce the size of your video files.

I've previously written about how to compress videos generally, but for GoPro clips we need to do a little more. That's because there are additional streams embedded in the videos, as well as proprietary metadata.

Here I will walkthrough the process of compressing a video shot from my Hero 5, using FFmpeg. If you just want to skip straight to the final command, scroll to the end.

If you have a different model GoPro, some of the metadata might be slightly different (earlier GoPro's do not have a GPS sensor for example). Hopefully there is enough information here so that you can tweak as necessary for your own camera

Here's our test video:

Snow capped mountains in NZ, shot at 2.7k, weighing in at 78.9MB for 10s

This video was shot at 2.7k to capture the amazing landscapes in New Zealand. I'd like to retain a decent quality, but reduce the size from 78.9MB.

If you haven't already, install FFmpeg. Now based on my previous post, we could start off by re-encoding just the video at a slightly higher crf factor, like this:

ffmpeg -i GOPR5687.MP4 -c:a copy -c:v h264 -crf 22 output.mp4

Running this spits out a 30.4MB file, around 61.5% smaller, and the quality is fine for my purposes. Not bad!

The range of the CRF scale is 0–51, where 0 is lossless, 23 is the default, and 51 is worst quality possible. A lower value generally leads to higher quality, and a subjectively sane range is 17–28. Consider 17 or 18 to be visually lossless or nearly so; it should look the same or nearly the same as the input but it isn't technically lossless. You should experiment with different crf values to find the sweet-spot for you.

Is that it? Are we done? Not quite.

Keeping GoPro metadata

If we try to import our new smaller video into Quik so that we can do some basic editing, we hit a roadblock.

As far as Quik is concerned, the new smaller clip just doesn't cut the mustard

Hmm, it seems something has happened to the video during the conversion, and now GoPro's software cannot use it.

Let's investigate.

We can use FFmpeg with no options specified to sneak a peak at the metadata in the videos without actually doing anything to them, and compare the original with our new, smaller file.

Here's the original:

~/Desktop  ffmpeg -i GOPR5687.MP4
ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
...
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'GOPR5687.MP4':
  Metadata:
    major_brand     : mp41
    minor_version   : 538120216
    compatible_brands: mp41
    creation_time   : 2017-08-07T14:43:26.000000Z
    location        : -43.7941+170.1170/
    location-eng    : -43.7941+170.1170/
    firmware        : HD5.02.01.57.00
  Duration: 00:00:10.41, start: 0.000000, bitrate: 60633 kb/s
    Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc, bt709), 2704x1520 [SAR 1:1 DAR 169:95], 60541 kb/s, 59.94 fps, 59.94 tbr, 60k tbn, 119.88 tbc (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : 	GoPro AVC
      encoder         : GoPro AVC encoder
      timecode        : 14:58:24:55
    Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : 	GoPro AAC
      timecode        : 14:58:24:55
    Stream #0:2(eng): Data: none (tmcd / 0x64636D74), 0 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : 	GoPro TCD
      timecode        : 14:58:24:55
    Stream #0:3(eng): Data: none (gpmd / 0x646D7067), 33 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : 	GoPro MET
    Stream #0:4(eng): Data: none (fdsc / 0x63736466), 14 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : 	GoPro SOS

And here's our smaller conversion:

~/Desktop  ffmpeg -i output.mp4
ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
  ...
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'output.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.12.100
    location-eng    : -43.7941+170.1170/
    location        : -43.7941+170.1170/
  Duration: 00:00:10.41, start: 0.000000, bitrate: 23356 kb/s
    Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc), 2704x1520 [SAR 1:1 DAR 169:95], 23252 kb/s, 59.94 fps, 59.94 tbr, 60k tbn, 119.88 tbc (default)
    Metadata:
      handler_name    : VideoHandler
      timecode        : 14:58:24:55
    Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
    Stream #0:2(eng): Data: none (tmcd / 0x64636D74), 0 kb/s
    Metadata:
      handler_name    : TimeCodeHandler
      timecode        : 14:58:24:55

Straight away we can see that the output from the original file is considerably longer than the one for our converted file.

There are two main differences between the two:

  1. The original file contains 5 separate streams embedded inside it, our transcode just 2
  2. The original file uses proprietary handler_name's like GoPro AVC, whereas our transcode uses generic versions, like VideoHandler

What happened is that during our transcoding we essentially stripped out loads of data which the GoPro camera writes into the file, meaning we've lost some information as well as the ability to use our files in GoPro software.

Not ideal, let's fix that.

Multiple Streams

As you would expect, a video is generally made up of both an audio and a video part, and each part is referred to as a stream.

What might not be so intuitive at first though is that a file can, and often does, include more streams than just one audio and one video. For example, we might have several different audio streams for different spoken languages, plus a subtitle stream. A decent video player will allow us to choose which streams we want to use when we play the file, perhaps french audio with english subtitles.

In the case of our GoPro, we have several additional data streams, one of which contains the GPS data that is recorded if it is switched on (seen above as GoPro MET).

By default, FFmpeg includes just one audio and one video stream in the output file, choosing what it considers to be the best of each type.

To tell FFmpeg to copy all streams we can use -map 0. Also, to tell it to copy streams even if it doesn't recognise their content (necessary for some GoPro data streams), we can use -copy_unknown also.

ffmpeg -i GOPR5687.MP4 -map 0 -copy_unknown -map_metadata 0 \
    -c copy -c:v h264 -crf 22 output.mp4

Since we want to preserve most of the streams without modification, we pass -c copy first, which says to just copy each stream in the input directly to the output. Then, we override the copy codec just for the video stream, using -c:v h264 as before.

I've also added -map_metadata 0 to copy all of the global metadata from input to output.

If we inspect the video again, it's better. We have now 5 streams as we wanted:

 ~/Desktop  ffmpeg -i output.mp4
ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
...
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'output.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    creation_time   : 2017-08-07T14:43:26.000000Z
    encoder         : Lavf58.12.100
    location-eng    : -43.7941+170.1170/
    location        : -43.7941+170.1170/
  Duration: 00:00:10.41, start: 0.000000, bitrate: 23407 kb/s
    Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc), 2704x1520 [SAR 1:1 DAR 169:95], 23252 kb/s, 59.94 fps, 59.94 tbr, 60k tbn, 119.88 tbc (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : VideoHandler
      timecode        : 14:58:24:55
    Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : SoundHandler
    Stream #0:2(eng): Data: none (tmcd / 0x64636D74), 0 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : TimeCodeHandler
      timecode        : 14:58:24:55
    Stream #0:3(eng): Data: none (gpmd / 0x646D7067), 32 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : GoPro MET
    Stream #0:4(eng): Data: none (stts / 0x73747473), 14 kb/s (default)
    Metadata:
      creation_time   : 2017-08-07T14:43:26.000000Z
      handler_name    : DataHandler

We still have two issues though!

  1. The video clip will still not open in Quik... probably has something to do with those generic handler names
  2. While we were transcoding, ffmpeg spat out a warning for the SOS stream:

[mp4 @ 0x7ff4cc006c00] Unknown hldr_type for fdsc, writing dummy values3.0kbits/s speed=0.148x

Let's continue and try to fix those now.

Correcting Handler Names

Like most things, it is possible to customise the handler name with FFmpeg. We just need to specify the handler metadata tag for each stream and give the correct name.

For example, -metadata:s:v: handler='  GoPro AVC' sets the handler_name for the video stream to be called GoPro AVC.

Although the written metadata is called handler_name, the tag to set it is just handler. This has caused confusion for at least some others

We can now try to transcode the video, making sure to set the names for the audio and video tracks:

ffmpeg -i GOPR5687.MP4 -copy_unknown -map_metadata 0 \
-c copy -c:v h264 -crf 22 =\
-map 0 \
-metadata:s:v: handler='  GoPro AVC' \
-metadata:s:a: handler='  GoPro AAC' \
output.mp4

This time, all is well if we try to import it into Quik.

Quik now happily imports our video

Great, so we can override the handler names to the GoPro specific ones! One slight snag though, the streams are not always in the same order. That means if we set the handler names by index, for some videos we might end up using a name of GoPro AAC for the video, when that's the name for the audio.

This might not be a problem if you're just encoding a single file, as you can check it first to see what order it has been recorded in. But, if you want to batch convert many with the same command in a script for example, it will bite you.

Selecting streams by name

Fortunately, we can work around this by explicitly listing the input streams that we want to map, and then follow that with the handler names we would like for each in the same order. This works because FFmpeg will map the output streams in the same order as you list them from the input.

We can explicitly list the video and audio streams easily, as there is only one of them. This just requires -map 0:v or -map 0:a respectively, which says to map the video or audio stream from the 0th (first) input file.

The data tracks are a bit more complicated, as there are 3 of them and they may appear in any order. Luckily, we can also pick the input streams by name:

-map 0:m:handler_name:'  GoPro MET'

Here we specify that we want to map the stream which has a handler_name of GoPro MET in its metadata, from the 0th input file.

Perfect, now we have all the pieces we need!

Putting it all together

So, to compress the GoPro video named GOPR5687.MP4 to a smaller size, but still have it keep it's metadata and work in Quik, we can use something like:

ffmpeg -i GOPR5687.MP4 -copy_unknown -map_metadata 0 \
-c copy -c:v h264 -crf 22 -pix_fmt yuvj420p \
-map 0:v -map 0:a \
-map 0:m:handler_name:' GoPro TCD' \
-map 0:m:handler_name:' GoPro MET' \
-map 0:m:handler_name:' GoPro SOS' \
-tag:d:1 'gpmd' -tag:d:2 'gpmd' \
-metadata:s:v: handler='        GoPro AVC' \
-metadata:s:a: handler='        GoPro AAC' \
-metadata:s:d:0 handler='       GoPro TCD' \
-metadata:s:d:1 handler='       GoPro MET' \
-metadata:s:d:2 handler='       GoPro SOS (original fdsc stream)' \
output.mp4 \
&& touch -r GOPR5687.MP4 output.mp4

Here with the -map lines we extract, in order, the video, the audio, and then the TCD, MET and SOS data streams. Then in the -metadata lines we name each stream accordingly.

Watch out! What looks like a space before the handler_name's is actually a TAB character. Yes, GoPro really does record handler names starting with a tab. And yes, for some streams like MET, if it doesn't match exactly, Quik won't recognise it. Spent some time tearing my hair out before I realised this...

Remember the warning about dummy data being used for the SOS fdsc stream I mentioned earlier?

That happens because FFmpeg doesn't know what an fdsc stream is, so strangely instead of just copying it anyway (which is what we asked), it decides to stuff the whole thing with garbage data. It seems like this SOS stream is only used for file recovery anyway, and isn't that important.

Nevertheless, I'd still prefer to copy it over if possible. To do this, I've added a small hack which retags the 'fdsc' stream as 'gpmd' using -tag:d:2 'gpmd'. Because FFmpeg is familiar with this type, it will happily copy across the data. Then, when I rename the handler, I've given it a name to indicate that it was originally the fdsc stream.

If you don't care about keeping this stream, you can omit these tags. Hopefully in the future FFmpeg will just copy all the data if -copy_unknown is specified anyway, so that there is no need for it anyway

I've explicitly specified the output pixel format as -pix_fmt yuvj420p also, so there is no guesswork on behalf of the codec.

If you have any issues with colour reproduction, you might also want to look at colour profile settings. See here or here for a little more info.

Finally, I've added atouch command at the end, in order to copy across the original file modification timestamps to the newly created file. Handy if we're going to be sorting the files by date!

So now if we run the above command, we generate a much smaller video that retains our metadata, and keeps Quik happy:

As far as Quik is concerned, the file now came straight from the GoPro ;)
In my tests, the GPS guage data is copied over and you can activate the guages within Quik to overlay onto your video. However, the data doesn't seem to match up correctly, despite all being there (and I can extract the gps coordinates recorded without issue). I suspect there is some timing data that is not copied exactly. There is a reddit thread that is relevant to this. There has also been commits to the FFmpeg codebase from a GoPro engineer to support the gpmd stream, so perhaps in time this will work correctly.

That's all there is to it! And if you want to automate this process for a number of videos, I created a tool called Shrinkwrap to do this. If you're familiar with Docker you might want to check it out, using the GoPro5 preset.

If you have any comments or improvements, feel free to leave them below!

Post cover image source