Add Timestamp to Videos#

I am involved with some experimental work, and a lot of the video footage is captured with a GoPro. They are durable and work well in the field. Unfortunately, they don’t support timestamp overlay with the date and time on the video. I used FFmpeg and a shell script to automate the process. The following script will take the video, extract the creation_time tag from the video and use that to generate the timestamp overlay.

The script will also re-encode the video to h.265, making it smaller, in some cases reducing the file size to 10 % of the original size.

timestamp.sh#

timestamp_sh#
  1#!/usr/bin/env bash
  2
  3# -----------
  4# SPDX-License-Identifier: MIT
  5# Copyright (c) 2022 Troy Williams
  6
  7# uuid       = 70204dea-f8b9-11ec-a408-cbfc46763958
  8# author     = Troy Williams
  9# email      = troy.williams@bluebill.net
 10# date       = 2022-06-30
 11# -----------
 12
 13# this script will add timestamps (like security cameras) to the bottom left
 14# corner of the video. It will use the creation date as the starting point of
 15# the timestamp counter. The output video will be re-encoded to h265 format.
 16
 17# NOTE: Some videos indicate they are in UTC, but in fact are not.They will need
 18# be correct otherwise the timestamp will be incorrect.
 19
 20# NOTE: If the creation_time isn't set, the video will be skipped.
 21
 22# Usage:
 23
 24# $ ./timestamp.sh 20210613_191928.mp4
 25
 26# loop through files in a folder:
 27# $ for FILE in ./2022-06-28/*; do ./timestamp.sh "${FILE}"; done
 28# $ for FILE in ./2022-06-28/*; do ./timestamp.sh "${FILE}" --over-write; done
 29
 30# Requirements:
 31
 32# JSON parser: $ sudo apt install jq
 33
 34# References
 35
 36# https://www.cyberciti.biz/faq/unix-linux-bsd-appleosx-bash-assign-variable-command-output/
 37# https://stackoverflow.com/questions/1955505/parsing-json-with-unix-tools
 38# https://www.baeldung.com/linux/use-command-line-arguments-in-bash-script
 39# https://stackoverflow.com/a/965069
 40# https://www.howtogeek.com/529219/how-to-parse-json-files-on-the-linux-command-line-with-jq/
 41# https://cameronnokes.com/blog/working-with-json-in-bash-using-jq/
 42# https://stackoverflow.com/questions/965053/extract-filename-and-extension-in-bash
 43
 44# https://ourcodeworld.com/articles/read/1484/how-to-get-the-information-and-metadata-of-a-media-file-audio-or-video-in-json-format-with-ffprobe
 45# ffprobe -loglevel 0 -print_format json -show_format -show_streams GX013438.MP4
 46
 47# ----------------
 48# Set the script to halt on errors
 49set -e
 50
 51if [[ -z "$1" ]]; then
 52    echo "USAGE: $ 1./timestamp.sh video.mp4"
 53    exit 1
 54fi
 55
 56# Get the movie name
 57# MOVIE=${1}
 58
 59# What is the full path of the movie name?
 60INPUT=$(readlink -f "$1")
 61
 62# Strip longest match of */ from start
 63filename="${INPUT##*/}"
 64
 65# Substring from 0 thru pos of filename
 66dir="${INPUT:0:${#INPUT} - ${#filename}}"
 67
 68# Strip shortest match of . plus at least one non-dot char from end
 69base="${filename%.[^.]*}"
 70
 71# Substring from len of base thru end
 72ext="${filename:${#base} + 1}"
 73
 74# If we have an extension and no base, it's really the base
 75if [[ -z "$base" && -n "$ext" ]]; then
 76    base=".$ext"
 77    ext=""
 78fi
 79
 80# Create the new file name
 81OUTPUT="${dir}"/"${base}".stamped."${ext}"
 82
 83# ----------
 84# Debugging Code
 85# echo "MOVIE = ${MOVIE}"
 86# echo "INPUT = ${INPUT}"
 87# echo "filename = ${filename}"
 88# echo "dir = ${dir}"
 89# echo "base = ${base}"
 90# echo "ext = ${ext}"
 91# echo "OUTPUT = ${OUTPUT}"
 92# ----------
 93
 94# need to use raw mode `-r` in jq call to return a raw string otherwise it will
 95# be trouble to deal with
 96STARTDATE=$(ffprobe -loglevel 0 -print_format json -show_format -show_streams "${INPUT}" | jq -r .format.tags.creation_time)
 97
 98if [[ -z "${STARTDATE}" ]]; then
 99    echo "ERROR - No creation_time in metadata! ${INPUT}"
100    exit 1
101fi
102
103# Convert the date to EPOCH. This will be used to set the time for the draw text
104# method.
105EPOCH=$(date --date="${STARTDATE}" +%s)
106
107echo "Video Create Time: ${STARTDATE} (${EPOCH})"
108
109# we assume that the STARTDATE is in UTC 0000, Zulu time, GMT and that we want
110# to convert it to the local time on the computer.
111ffmpeg -i "${INPUT}" -vf drawtext="fontsize=30:fontcolor=yellow:text='%{pts\:localtime\:${EPOCH}}':x=(w-text_w) - 10:y=(h-text_h) - 10" -vcodec libx265 -crf 28 "${OUTPUT}"
112
113if [ "${3:-"invalid"}" == "--over-write" ]; then
114
115    echo "Moving ${OUTPUT} -> ${INPUT}"
116    mv "${OUTPUT}" "${INPUT}"
117
118else
119    echo "To overwrite the existing video, use --over-write"
120fi

I did notice that the GoPro we were using didn’t have the correct time embedded in the video. It was the local time with no timezone. The above script would attempt to convert the time to local time and the stamp would be wrong. I wrote the following script to fix that issue. It extracts the creation_time, removes the timezone and assigns the local timezone. It then converts this back to UTC and write it back to the video.

correct_times.sh#

correct_times.sh#
  1#!/usr/bin/env bash
  2
  3# -----------
  4# SPDX-License-Identifier: MIT
  5# Copyright (c) 2022 Troy Williams
  6
  7# uuid       = e577e81c-f938-11ec-9c1a-0d95d112ee30
  8# author     = Troy Williams
  9# email      = troy.williams@bluebill.net
 10# date       = 2022-07-01
 11# -----------
 12
 13# This script will take the video and correct the creation time issue with it.
 14# It assumes that the the creation time did not have the proper timezone set.
 15# This means that other programs will attempt to convert from GMT/Zulu time and
 16# get the wrong time stamp.
 17
 18# This script assumes that the video has the correct local time, but was
 19# inserted without a timezone. It will extract that time, and configure it as a
 20# local time. It will then convert the local time properly to UTC +00:00 and
 21# write it back to the video.
 22
 23# Generally this script would be executed before the `timestamp.sh` script.
 24
 25#NOTE: This script should only be run if the timestamps stored in the video are
 26#not correctly set to UTC time.
 27
 28# Usage:
 29
 30# List the creation time and what it would be in local time:
 31# $ ./correct_times.sh GX013443.MP4
 32
 33# Write the corrected local time to the video.
 34# $ ./correct_times.sh GX013443.MP4 --update
 35
 36# Write the corrected local time to the video.and overwrite the existing file
 37# $ ./correct_times.sh GX013443.MP4 --update --over-write
 38
 39# loop through files in a folder:
 40# $ for FILE in ./2022-06-28/*; do ./correct_times.sh "${FILE}"; done
 41# $ for FILE in ./2022-06-28/*; do ./correct_times.sh "${FILE}" --update; done
 42# $ for FILE in ./2022-06-28/*; do ./correct_times.sh "${FILE}" --update --over-write; done
 43
 44
 45# Requirements:
 46
 47# JSON parser: $ sudo apt install jq
 48
 49# References
 50
 51# https://stackoverflow.com/questions/16548528/command-to-get-time-in-milliseconds
 52# https://stackoverflow.com/questions/28016578/how-can-i-parse-create-a-date-time-stamp-formatted-with-fractional-seconds-utc
 53# https://serverfault.com/questions/1020446/linux-get-utc-offset-from-current-or-other-timezones-on-given-date-and-time
 54# https://man7.org/linux/man-pages/man1/date.1.html
 55
 56# ----------------
 57# Set the script to halt on errors
 58set -e
 59
 60if [[ -z "$1" ]]; then
 61    echo "USAGE: $ 1./correct_times video.mp4 --update --over-write"
 62    exit 1
 63fi
 64
 65# Get the movie name
 66MOVIE=$1
 67
 68echo "${MOVIE}"
 69# need to use raw mode `-r` in jq call to return a raw string otherwise it will be trouble to deal with
 70CREATED=$(ffprobe -loglevel 0 -print_format json -show_format -show_streams "${MOVIE}" | jq -r .format.tags.creation_time)
 71
 72if [[ -z "${CREATED}" ]]; then
 73    echo "ERROR - No creation_time in metadata! ${MOVIE}"
 74    exit 1
 75fi
 76
 77echo
 78echo "Video Create Time (Zulu):  ${CREATED}"
 79
 80# Convert the creation time to local time
 81LOCALTIME=$(date --date="${CREATED}" "+%FT%T %z (%Z)")
 82echo "Video Create Time (local): ${LOCALTIME}"
 83
 84echo "--------"
 85# strip the timezone from the string
 86STRIPPED=$(TZ=UTC date --date="${CREATED}" "+%FT%T")
 87echo "Video Create Time (without timezone):${STRIPPED}"
 88
 89# Express the string in the local timezone
 90TO_LOCAL=$(date --date="${STRIPPED}" "+%FT%T %z (%Z)")
 91echo "Video Create Time (to local):${TO_LOCAL}"
 92
 93echo "--------"
 94# convert the local time string to UTC
 95CONVERTED=$(TZ=UTC date --date="${TO_LOCAL}" "+%FT%T.%6NZ") # this is the correct time string
 96echo "Video Create Time (to UTC):${CONVERTED}"
 97echo
 98
 99# Video Create Time (Zulu):  2022-06-28T12:34:27.000000Z
100# Video Create Time (local): 2022-06-28T08:34:27 -0400 (EDT)
101# --------
102# Video Create Time (without timezone):2022-06-28T12:34:27
103# Video Create Time (to local):2022-06-28T12:34:27 -0400 (EDT)
104# --------
105# Video Create Time (to UTC):2022-06-28T16:34:27.000000Z
106
107# -----------
108
109OUTPUT="${MOVIE%%.*}".corrected."${MOVIE##*.}"
110
111if [ "${2:-"invalid"}" == "--update" ]; then
112
113    # Write the converted timezone to the video
114    echo "Updating metadata ${MOVIE} -> ${OUTPUT}"
115    ffmpeg -i "${MOVIE}" -c copy -metadata creation_time="${CONVERTED}" "${OUTPUT}"
116
117    if [ "${3:-"invalid"}" == "--over-write" ]; then
118
119        echo "Moving ${OUTPUT} -> ${MOVIE}"
120        mv "${OUTPUT}" "${MOVIE}"
121
122    else
123        echo "To overwrite the existing video, use --over-write"
124    fi
125
126else
127    echo "To update the metadata use --update"
128fi

Note

2022-08-20 - fixed a couple of bugs in the scripts that prevented them from handling files with spaces in their names correctly.