Re-Encoding GoPro footage for size reduction

The GoPro 10 I own & use for most of both capturing action & archiving some daily activities, has to really struggle hard, to encode H.265 video in such a small form factor. Because of the computation power required to produce H.265 streams, it quickly becomes obvious that there were some shortcuts taken in the encoding algorithms used in our cameras.

The obvious one, is the Constant BitRate (CBR) of recorded videos, which is the least energy hungry BitRate management method, for live recordings. But, when targeting high bitrate is perfectly fine for action footage, where VBR/Predictive algorithms would choose to use more data for encoding. When you so happen to record some slow or even still footage, we find out that these files are unnecessarily bloated.

With modern HEVC (H.265) encoders (x.265 is the CPU encoder for H.265, while NVENC is the Nvidia GPU encoder), re-encoding to Variable BitRate files can be done super fast and even completely automated.

The issue with embedded metadata streams

Because the GoPro engineers have decided to include GPS data in a purpose made GPMD stream & a compatibility TMCD stream, used in playback of MOV formats in Apple QuickTime software, many people who have attempted conversion of GoPro footage, have ran into the wall of FFMPEG not willing to re-mux these streams, because according to spec, they shouldn't be part of the MP4 format & container.

The stream copy process of binary data (which TMCD & GPMD are) is quite straight forward, so other than sanity checks done by FFMPEG programmers, there isn't anything stopping us from successfully transplanting these streams between different files.

So, I have made necessary changes to the FFMPEG code & built a windows binary (write to me, if you would like a linux binary aswell. I don't plan to use this software on linux, so I didn't bother with figuring out the build process for it).
This binary can be acquired here:

Release Temporary Workaround for GoPro footage re-encoding · HighPriest/FFmpeg
Win64 FFMPEG binary (based on up-to-date master branch of the FFMPEG project), which permits remuxing of unknown streams, like the TMCD stream with which the GoPro community has been having issues…

Issue of transferring creation metadata on Windows

For archival footage like videos of our lives, it is important to maintain basic data like creation date. Unfortunately, FFmpeg seems to not have a feature of maintaining creation date of the original files, so we need to do it manually. Fortunately, I have been able to dig out a method, which does it 100% reliably.

param(
    [Parameter(Mandatory=$false)]
    [string]$source_dir = "E:\Video\GoPro\",
  
    [Parameter(Mandatory=$false)]
    [string]$target_dir = "C:\Video_Encode"
)

Write-Host "Source Directory: $source_dir"
Write-Host "Target Directory: $target_dir"

Get-ChildItem $source_dir *.mp4 | ForEach-Object {
    Write-Host "$($target_dir)\$($_.BaseName).mp4"
    $target = Get-Item -LiteralPath "$($target_dir)\$($_.BaseName).mp4";
    foreach ($prop in 'CreationTime', 'LastWriteTime', 'LastAccessTime'){
        Write-Host $_.'LastWriteTime'
        Write-Host $target.$prop
        $target.$prop = $_.'LastWriteTime'     }
}

Many posts & stack-overflow answers on the internet, mention digging deep into structure of the files metadata. Luckily, PowerShell (At least my current v7.4.4) puts all the metadata we need at our fingertips as a named parameter! It can be freely read and edited without any restrictions 😃

This code is included in the complete solution at the end of the post, so you don't have to bother yourself with figuring it out yourself.

Scripting the conversion process

If you acquire the linked, modified binary, we can complete re-encoding of the GoPro footage, while maintaining all the other metadata.

Release Temporary Workaround for GoPro footage re-encoding · HighPriest/FFmpeg
Win64 FFMPEG binary (based on up-to-date master branch of the FFMPEG project), which permits remuxing of unknown streams, like the TMCD stream with which the GoPro community has been having issues…

For now, I have decided to use a simple PowerShell script, which allows me to schedule conversion of entire directories of files originating from a GoPro.

# Define the directory containing the MP4 files
$videoDirectory = "E:\Video\GoPro\GoPro"

# Define the target directory for the encoded files
$targetDirectory = "C:\Video_Encode\NewGoPro"

# Ensure the target directory exists
if (-not (Test-Path -Path $targetDirectory)) {
    New-Item -ItemType Directory -Force -Path $targetDirectory | Out-Null
}

# Get all MP4 files in the directory
$gxFiles = Get-ChildItem -Path $videoDirectory -Filter GX*.MP4 -Recurse
$ghFiles = Get-ChildItem -Path $videoDirectory -Filter GH*.MP4 -Recurse

# Loop through each GX*.MP4 file
foreach ($source in $gxFiles) {
    # Construct the output file name by replacing the extension with .mp4
    $outputPath = Join-Path -Path $targetDirectory -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension($source.Name) + ".mp4")
    
    Write-Host $source.FullName

    # Execute the FFmpeg command to compress the video
    & ffmpeg.exe -n -i $source.FullName `
    -map 0 -map -0:d:2 -copy_unknown -map_metadata 0 `
    -c copy -c:v hevc_nvenc -recast_media `
    -write_tmcd 0 `
    -spatial-aq 1 -aq-strength 8 -b_ref_mode 2 -lookahead_level 15 -tune hq -qp 34 -pix_fmt yuv420p -level '5.1' -no-scenecut 1 -strict_gop 1 `
    -tag:v hvc1 -vf scale=out_range=pc:out_color_matrix=bt709 -color_range:v 'pc' -colorspace:v bt709 `
    -tag:d:0 'tmcd' -tag:d:1 'gpmd' `
    $outputPath

    $target = Get-Item -LiteralPath $outputPath;

    foreach ($prop in 'CreationTime', 'LastWriteTime', 'LastAccessTime'){
        Write-Host $source.'LastWriteTime'
        Write-Host $target.$prop
        $target.$prop = $source.'CreationTime'
    }
}

# Loop through each GH*.MP4 file
foreach ($source in $ghFiles) {
    # Construct the output file name by replacing the extension with .mp4
    $outputPath = Join-Path -Path $targetDirectory -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension($source.Name) + ".mp4")
    
    Write-Host $source.FullName

    # Execute the FFmpeg command to compress the video
    & ffmpeg.exe -n -i $source.FullName `
    -map 0 -map -0:d:2 -copy_unknown -map_metadata 0 `
    -c copy -c:v libx265 -recast_media `
    -write_tmcd 0 -ss 00:00:50 -t 00:00:40 `
    -crf 29 -pix_fmt yuv420p -level '5.1' -preset 'medium' -no-scenecut 1 `
    -tag:v hvc1 -vf scale=out_range=pc:out_color_matrix=bt709 -color_range:v 'pc' -colorspace:v bt709 `
    -tag:d:0 'tmcd' -tag:d:1 'gpmd' `
    $outputPath

    $target = Get-Item -LiteralPath $outputPath;

    foreach ($prop in 'CreationTime', 'LastWriteTime', 'LastAccessTime'){
        Write-Host $source.'LastWriteTime'
        Write-Host $target.$prop
        $target.$prop = $source.'CreationTime'
    }
}

At one point, when I am going to acquire a dedicated NAS machine, which could ingest the source footage and automatically re-encode & organize it. I am planning to use completely automated software like

Tdarr
Tdarr Transcode Automation

and purpose made scripts like one found here

GitHub - PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR: Tdarr plugin to transcode H264 or reconvert HEVC using NVENC with bframes, 10bit, and (optional) HDR
Tdarr plugin to transcode H264 or reconvert HEVC using NVENC with bframes, 10bit, and (optional) HDR - PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR