Document Banner

Add Timestamp to Videos

Troy Williams

2022-07-01

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

#!/usr/bin/env bash

# -----------
# SPDX-License-Identifier: MIT
# Copyright (c) 2022 Troy Williams

# uuid       = 70204dea-f8b9-11ec-a408-cbfc46763958
# author     = Troy Williams
# email      = troy.williams@bluebill.net
# date       = 2022-06-30
# -----------

# this script will add timestamps (like security cameras) to the bottom left
# corner of the video. It will use the creation date as the starting point of
# the timestamp counter. The output video will be re-encoded to h265 format.

# NOTE: Some videos indicate they are in UTC, but in fact are not.They will need
# be correct otherwise the timestamp will be incorrect.

# NOTE: If the creation_time isn't set, the video will be skipped.

# Usage:

# $ ./timestamp.sh 20210613_191928.mp4

# loop through files in a folder:
# $ for FILE in ./2022-06-28/*; do ./timestamp.sh "${FILE}"; done
# $ for FILE in ./2022-06-28/*; do ./timestamp.sh "${FILE}" --over-write; done

# Requirements:

# JSON parser: $ sudo apt install jq

# References

# https://www.cyberciti.biz/faq/unix-linux-bsd-appleosx-bash-assign-variable-command-output/
# https://stackoverflow.com/questions/1955505/parsing-json-with-unix-tools
# https://www.baeldung.com/linux/use-command-line-arguments-in-bash-script
# https://stackoverflow.com/a/965069
# https://www.howtogeek.com/529219/how-to-parse-json-files-on-the-linux-command-line-with-jq/
# https://cameronnokes.com/blog/working-with-json-in-bash-using-jq/
# https://stackoverflow.com/questions/965053/extract-filename-and-extension-in-bash

# 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
# ffprobe -loglevel 0 -print_format json -show_format -show_streams GX013438.MP4

# ----------------
# Set the script to halt on errors
set -e

if [[ -z "$1" ]]; then
    echo "USAGE: $ 1./timestamp.sh video.mp4"
    exit 1
fi

# Get the movie name
# MOVIE=${1}

# What is the full path of the movie name?
INPUT=$(readlink -f "$1")

# Strip longest match of */ from start
filename="${INPUT##*/}"

# Substring from 0 thru pos of filename
dir="${INPUT:0:${#INPUT} - ${#filename}}"

# Strip shortest match of . plus at least one non-dot char from end
base="${filename%.[^.]*}"

# Substring from len of base thru end
ext="${filename:${#base} + 1}"

# If we have an extension and no base, it's really the base
if [[ -z "$base" && -n "$ext" ]]; then
    base=".$ext"
    ext=""
fi

# Create the new file name
OUTPUT="${dir}"/"${base}".stamped."${ext}"

# ----------
# Debugging Code
# echo "MOVIE = ${MOVIE}"
# echo "INPUT = ${INPUT}"
# echo "filename = ${filename}"
# echo "dir = ${dir}"
# echo "base = ${base}"
# echo "ext = ${ext}"
# echo "OUTPUT = ${OUTPUT}"
# ----------

# need to use raw mode `-r` in jq call to return a raw string otherwise it will
# be trouble to deal with
STARTDATE=$(ffprobe -loglevel 0 -print_format json -show_format -show_streams "${INPUT}" | jq -r .format.tags.creation_time)

if [[ -z "${STARTDATE}" ]]; then
    echo "ERROR - No creation_time in metadata! ${INPUT}"
    exit 1
fi

# Convert the date to EPOCH. This will be used to set the time for the draw text
# method.
EPOCH=$(date --date="${STARTDATE}" +%s)

echo "Video Create Time: ${STARTDATE} (${EPOCH})"

# we assume that the STARTDATE is in UTC 0000, Zulu time, GMT and that we want
# to convert it to the local time on the computer.
ffmpeg -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}"

if [ "${3:-"invalid"}" == "--over-write" ]; then

    echo "Moving ${OUTPUT} -> ${INPUT}"
    mv "${OUTPUT}" "${INPUT}"

else
    echo "To overwrite the existing video, use --over-write"
fi

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

#!/usr/bin/env bash

# -----------
# SPDX-License-Identifier: MIT
# Copyright (c) 2022 Troy Williams

# uuid       = e577e81c-f938-11ec-9c1a-0d95d112ee30
# author     = Troy Williams
# email      = troy.williams@bluebill.net
# date       = 2022-07-01
# -----------

# This script will take the video and correct the creation time issue with it.
# It assumes that the the creation time did not have the proper timezone set.
# This means that other programs will attempt to convert from GMT/Zulu time and
# get the wrong time stamp.

# This script assumes that the video has the correct local time, but was
# inserted without a timezone. It will extract that time, and configure it as a
# local time. It will then convert the local time properly to UTC +00:00 and
# write it back to the video.

# Generally this script would be executed before the `timestamp.sh` script.

#NOTE: This script should only be run if the timestamps stored in the video are
#not correctly set to UTC time.

# Usage:

# List the creation time and what it would be in local time:
# $ ./correct_times.sh GX013443.MP4

# Write the corrected local time to the video.
# $ ./correct_times.sh GX013443.MP4 --update

# Write the corrected local time to the video.and overwrite the existing file
# $ ./correct_times.sh GX013443.MP4 --update --over-write

# loop through files in a folder:
# $ for FILE in ./2022-06-28/*; do ./correct_times.sh "${FILE}"; done
# $ for FILE in ./2022-06-28/*; do ./correct_times.sh "${FILE}" --update; done
# $ for FILE in ./2022-06-28/*; do ./correct_times.sh "${FILE}" --update --over-write; done


# Requirements:

# JSON parser: $ sudo apt install jq

# References

# https://stackoverflow.com/questions/16548528/command-to-get-time-in-milliseconds
# https://stackoverflow.com/questions/28016578/how-can-i-parse-create-a-date-time-stamp-formatted-with-fractional-seconds-utc
# https://serverfault.com/questions/1020446/linux-get-utc-offset-from-current-or-other-timezones-on-given-date-and-time
# https://man7.org/linux/man-pages/man1/date.1.html

# ----------------
# Set the script to halt on errors
set -e

if [[ -z "$1" ]]; then
    echo "USAGE: $ 1./correct_times video.mp4 --update --over-write"
    exit 1
fi

# Get the movie name
MOVIE=$1

echo "${MOVIE}"
# need to use raw mode `-r` in jq call to return a raw string otherwise it will be trouble to deal with
CREATED=$(ffprobe -loglevel 0 -print_format json -show_format -show_streams "${MOVIE}" | jq -r .format.tags.creation_time)

if [[ -z "${CREATED}" ]]; then
    echo "ERROR - No creation_time in metadata! ${MOVIE}"
    exit 1
fi

echo
echo "Video Create Time (Zulu):  ${CREATED}"

# Convert the creation time to local time
LOCALTIME=$(date --date="${CREATED}" "+%FT%T %z (%Z)")
echo "Video Create Time (local): ${LOCALTIME}"

echo "--------"
# strip the timezone from the string
STRIPPED=$(TZ=UTC date --date="${CREATED}" "+%FT%T")
echo "Video Create Time (without timezone):${STRIPPED}"

# Express the string in the local timezone
TO_LOCAL=$(date --date="${STRIPPED}" "+%FT%T %z (%Z)")
echo "Video Create Time (to local):${TO_LOCAL}"

echo "--------"
# convert the local time string to UTC
CONVERTED=$(TZ=UTC date --date="${TO_LOCAL}" "+%FT%T.%6NZ") # this is the correct time string
echo "Video Create Time (to UTC):${CONVERTED}"
echo

# Video Create Time (Zulu):  2022-06-28T12:34:27.000000Z
# Video Create Time (local): 2022-06-28T08:34:27 -0400 (EDT)
# --------
# Video Create Time (without timezone):2022-06-28T12:34:27
# Video Create Time (to local):2022-06-28T12:34:27 -0400 (EDT)
# --------
# Video Create Time (to UTC):2022-06-28T16:34:27.000000Z

# -----------

OUTPUT="${MOVIE%%.*}".corrected."${MOVIE##*.}"

if [ "${2:-"invalid"}" == "--update" ]; then

    # Write the converted timezone to the video
    echo "Updating metadata ${MOVIE} -> ${OUTPUT}"
    ffmpeg -i "${MOVIE}" -c copy -metadata creation_time="${CONVERTED}" "${OUTPUT}"

    if [ "${3:-"invalid"}" == "--over-write" ]; then

        echo "Moving ${OUTPUT} -> ${MOVIE}"
        mv "${OUTPUT}" "${MOVIE}"

    else
        echo "To overwrite the existing video, use --over-write"
    fi

else
    echo "To update the metadata use --update"
fi

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