Skip to content

Instantly share code, notes, and snippets.

@coldcue
Last active August 11, 2026 19:33
Show Gist options
  • Select an option

  • Save coldcue/dd846b84b5d8818587db3e9112dbee7e to your computer and use it in GitHub Desktop.

Select an option

Save coldcue/dd846b84b5d8818587db3e9112dbee7e to your computer and use it in GitHub Desktop.
Batch re-encode DJI Osmo Action 5 Pro footage to HEVC (libx265, HLG-safe, metadata transfer)
#!/bin/bash
# Batch re-encode DJI action-camera footage to HEVC (libx265, crf 24, medium
# preset). Developed and tested with DJI Osmo Action 5 Pro recordings:
# 4K 29.97 fps HEVC ~56 Mbps, 10-bit HLG HDR (BT.2020 / ARIB STD-B67),
# AAC-LC stereo audio, plus DJI extra tracks (djmd/dbgi telemetry with
# per-frame accelerometer, shutter speed and color temperature; a tmcd
# timecode track; embedded cover-art/preview JPEGs).
#
# What it does per file:
# - keeps only the main video + audio streams; audio is copied untouched
# - drops the DJI telemetry tracks and embedded cover art (telemetry is a
# timed data stream only readable by DJI's own apps; it cannot be
# preserved through a re-encode)
# - 10-bit depth and HLG color signaling pass through automatically
# (verified with ffprobe: Main 10, bt2020/arib-std-b67 tags survive)
# - transfers container metadata with exiftool afterwards: capture dates,
# camera name ("Encoder: DJI OsmoAction5 Pro"), original SD-card path,
# firmware info, and the source file's modification time
# - output is .mp4 (same filename, new extension) for wide player/TV
# compatibility
#
# Already-encoded files are skipped, so the script can be re-run to resume.
# If an encode fails or is interrupted, the partial output is removed and the
# script stops instead of moving on to the next file.
#
# Encoder settings were chosen from measured preset/crf matrices on 60 s test
# segments of Osmo Action 5 Pro motorcycle POV footage (hardware VideoToolbox
# encoding was rejected after visual comparison).
#
# Usage: encode_dji_video.sh SRC_DIR DST_DIR
# SRC_DIR folder with the source .mp4/.MP4 files
# DST_DIR output folder, created if missing (must differ from SRC_DIR)
#
# Recommended invocation for a multi-day run on macOS (prevents idle/system
# sleep while on AC power; keep the lid open or use clamshell mode):
# caffeinate -is ./encode_dji_video.sh /path/to/source /path/to/output
# Treat unset variables as errors.
set -u
if [ $# -ne 2 ]; then
echo "Usage: ${0##*/} SRC_DIR DST_DIR" >&2
exit 1
fi
SRC="$1"
DST="$2"
if [ ! -d "$SRC" ]; then
echo "Source folder does not exist: $SRC" >&2
exit 1
fi
# Outputs keep the source basename, so encoding into the source folder would
# collide with the originals.
if [ "$(cd "$SRC" && pwd)" = "$(cd "$DST" 2>/dev/null && pwd)" ]; then
echo "SRC_DIR and DST_DIR must be different folders" >&2
exit 1
fi
# Check required tools are installed before starting a multi-day run.
missing=""
for cmd in ffmpeg ffprobe exiftool; do
command -v "$cmd" >/dev/null 2>&1 || missing="$missing $cmd"
done
if [ -n "$missing" ]; then
echo "Missing required tools:$missing" >&2
echo "Install with: brew install$missing" >&2
exit 1
fi
mkdir -p "$DST"
# Collect sources: *.mp4 in any case (DJI names files DJI_*.MP4), no error
# if none.
shopt -s nullglob nocaseglob
files=("$SRC"/*.mp4)
shopt -u nullglob nocaseglob
n=${#files[@]}
if [ "$n" -eq 0 ]; then
echo "No .mp4/.MP4 files found in $SRC" >&2
exit 1
fi
i=0
# Total length of the whole batch, shown alongside each file's own duration.
total_dur=$(for f in "${files[@]}"; do \
ffprobe -v error -show_entries format=duration -of csv=p=0 "$f"; \
done | awk '{s+=$1} END{printf "%d:%02d:%02d", s/3600, (s%3600)/60, s%60}')
for f in "${files[@]}"; do
i=$((i + 1))
# Output as .mp4 (same basename, extension normalized to lowercase .mp4).
name="${f##*/}"
base="${name%.*}"
out="$DST/$base.mp4"
# Skip files that already have an output — makes the script resumable.
# A completed file is never touched; partial files are deleted below on
# failure, so anything present here is a finished encode.
if [ -e "$out" ]; then
echo "[$i/$n] Skipping (already exists): $out"
continue
fi
dur=$(ffprobe -v error -pretty -show_entries format=duration -of csv=p=0 "$f")
echo "[$i/$n] Encoding: $name ($(du -h "$f" | cut -f1), $dur of $total_dur total)"
# -map 0:v:0 -map 0:a:0 keep only the main video and audio streams;
# drops the DJI djmd/dbgi telemetry tracks and the
# embedded cover-art JPEG (a timecode track is still
# added automatically from the source's timecode
# metadata)
# -c:v libx265 software HEVC encoder
# -preset medium -crf 24 chosen from measured preset/crf comparisons on
# Osmo Action 5 Pro test segments
# no-sao=1 disable the SAO in-loop filter, which trades
# ringing suppression for detail retention; keeps
# more road/foliage texture in this footage
# -tag:v hvc1 Apple-compatible codec tag (default hev1 won't play
# in QuickTime/Photos)
# -c:a copy pass the AAC audio through untouched
# -movflags +faststart move the moov atom to the front so playback can
# start before the whole file is downloaded/streamed
# (https://trac.ffmpeg.org/wiki/Encode/H.264#faststartforwebvideo)
ffmpeg -nostdin -loglevel error -stats -i "$f" -map 0:v:0 -map 0:a:0 \
-c:v libx265 -preset medium -crf 24 -x265-params no-sao=1 \
-tag:v hvc1 \
-c:a copy \
-movflags +faststart \
"$out"
# Stop on any failure, including Ctrl+C (ffmpeg exits nonzero on SIGINT).
# Delete the partial output so the next run resumes at this file.
status=$?
if [ "$status" -ne 0 ]; then
echo "Encode failed or interrupted (exit $status): $f" >&2
rm -f "$out"
exit "$status"
fi
# Transfer container metadata from the source: capture dates (movie, track
# and media level — ffmpeg writes them as zeros), camera name (Encoder:
# "DJI OsmoAction5 Pro"), original SD-card file path, DJI firmware Category
# string, and the source file's modification time.
# Excluded: embedded preview JPEGs (deliberately stripped in the encode) and
# the old container's brand tags. The DJI telemetry track is a data stream,
# not tags — it is dropped by the encode and cannot be transferred.
# exiftool preserves the faststart layout (moov stays at the front).
echo "[$i/$n] Transferring metadata: $name"
if ! exiftool -q -overwrite_original -api LargeFileSupport=1 \
-TagsFromFile "$f" -All:All \
--ItemList:CoverArt --ItemList:PreviewImage --ItemList:ThumbnailImage \
--Keys:MajorBrand --Keys:MinorVersion --Keys:CompatibleBrands \
"-FileModifyDate<FileModifyDate" "$out"; then
echo "Metadata transfer failed: $out (encode kept)" >&2
exit 1
fi
echo "[$i/$n] Done: $out ($(du -h "$out" | cut -f1))"
done
echo "All files encoded."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment