Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Reference: https://www.bluebill.net/timestamps.html


Table of Contents

Fixing GoPro Video Creation Time

Install Pre-Requisits

Install ffmpeg, coreutils and jq.Check Creation Time

Code Block
./correctTime.sh DIVE_2_1_GH010089.MP4 
DIVE_2_1_GH010089.MP4

Video Create Time (Zulu):  2023-06-13T19:12:11.000000Z
Video Create Time (local): 2023-06-13T15:12:11 -0400 (EDT)
--------
Video Create Time (without timezone):2023-06-13T19:12:11
Video Create Time (to local):2023-06-13T19:12:11 -0400 (EDT)
--------
Video Create Time (to UTC):2023-06-13T23:12:11.000000Z

To update the metadata use --update

In the above output we see that the local time was recorded as Zulu time.  The local time was 19:12:11.

Fix Creation Time

Code Block
./correctTime.sh DIVE_2_1_GH010089.MP4 --update

Verify Creation Time

Code Block
./correctTime.sh DIVE_2_1_GH010089.corrected.MP4 
DIVE_2_1_GH010089.corrected.MP4

Video Create Time (Zulu):  2023-06-13T23:12:11.000000Z
Video Create Time (local): 2023-06-13T19:12:11 -0400 (EDT)
--------
Video Create Time (without timezone):2023-06-13T23:12:11
Video Create Time (to local):2023-06-13T23:12:11 -0400 (EDT)
--------
Video Create Time (to UTC):2023-06-14T03:12:11.000000Z

To update the metadata use --update

Command to Update Creation Time:

Code Block
ffmpeg -I <MOVIE> -c copy -metadata creation_time=2023-06-13-23:12:11.000000Z <OUTPUT>

Add Timestamp to Video

Code Block
./timestamp.sh DIVE_2_1_GH010089.corrected.MP4

Changes to the timestamp.sh script:

  • Removed -vcodec libx265
  • Changed location of timestamp to top right corner
Code Block
...
#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}"

ffmpeg -i "${INPUT}" -vf drawtext="fontsize=30:fontcolor=white:text='%{pts\:localtime\:${EPOCH}}':x=(w-text_w) - 10:y=10" -crf 28 "${OUTPUT}"
...

Joining Video Files

Code Block
echo file DIVE_2_1_GH010089.corrected.MP4 >  mylist.txt 
echo file DIVE_2_2_GH020089.corrected.MP4 >>  mylist.txt 

ffmpeg -f concat -i mylist.txt -c copy output.mp4

Get Video File Information

Code Block
ffprobe -loglevel 0 -print_format json -show_format -show_streams "${INPUT}" | jq -r .format.tags.creation_time
brew install ffmpeg
brew install coreutils
brew install jq


Revise Scripts

Using the scripts from https://www.bluebill.net/timestamps.html

Revise timestamp.sh and correctTime.sh to use 'gdate' instead of 'date'.  

See below for revised scripts. 


Fixing GoPro Video Creation Time


Check Creation Time

Code Block
./correctTime.sh DIVE_2_1_GH010089.MP4 
DIVE_2_1_GH010089.MP4

Video Create Time (Zulu):  2023-06-13T19:12:11.000000Z
Video Create Time (local): 2023-06-13T15:12:11 -0400 (EDT)
--------
Video Create Time (without timezone):2023-06-13T19:12:11
Video Create Time (to local):2023-06-13T19:12:11 -0400 (EDT)
--------
Video Create Time (to UTC):2023-06-13T23:12:11.000000Z

To update the metadata use --update

In the above output we see that the local time was recorded as Zulu time.  The local time was 19:12:11.


Fix Creation Time

Code Block
./correctTime.sh DIVE_2_1_GH010089.MP4 --update


Verify Creation Time

Code Block
./correctTime.sh DIVE_2_1_GH010089.corrected.MP4 
DIVE_2_1_GH010089.corrected.MP4

Video Create Time (Zulu):  2023-06-13T23:12:11.000000Z
Video Create Time (local): 2023-06-13T19:12:11 -0400 (EDT)
--------
Video Create Time (without timezone):2023-06-13T23:12:11
Video Create Time (to local):2023-06-13T23:12:11 -0400 (EDT)
--------
Video Create Time (to UTC):2023-06-14T03:12:11.000000Z

To update the metadata use --update


Command to Update Creation Time:

Code Block
ffmpeg -I <MOVIE> -c copy -metadata creation_time=2023-06-13-23:12:11.000000Z <OUTPUT>


Add Timestamp to Video

Code Block
./timestamp.sh DIVE_2_1_GH010089.corrected.MP4


Changes to the timestamp.sh script:

  • Removed -vcodec libx265
  • Changed location of timestamp to top right corner


Code Block
...
#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}"

ffmpeg -i "${INPUT}" -vf drawtext="fontsize=30:fontcolor=white:text='%{pts\:localtime\:${EPOCH}}':x=(w-text_w) - 10:y=10" -crf 28 "${OUTPUT}"
...


Joining Video Files

Code Block
echo file DIVE_2_1_GH010089.corrected.MP4 >  mylist.txt 
echo file DIVE_2_2_GH020089.corrected.MP4 >>  mylist.txt 

ffmpeg -f concat -i mylist.txt -c copy output.mp4


Get Video File Information

Code Block
ffprobe -loglevel 0 -print_format json -show_format -show_streams "${INPUT}" | jq -r .format.tags.creation_time
Code Block
{
    "streams": [
        {
Code Block
{
    "streams": [
        {
            "index": 0,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/48000",
            "codec_tag_string": "mp4a",
            "codec_tag": "0x6134706d",
            "sample_fmt": "fltp",
            "sample_rate": "48000",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/48000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 35213578,
            "duration": "733.616208",
            "bit_rate": "119736",
            "max_bit_rate": "128000",
            "nb_frames": "34391",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2023-07-16T15:22:49.000000Z",
                "language": "eng",
                "handler_name": "Core Media Audio"
            }
        },
        {
            "index": 1,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "High",
            "codec_type": "video",
            "codec_time_base": "1001/120000",
            "codec_tag_string": "avc1",
            "codec_tag": "0x31637661",
            "width": 1280,
            "height": 720,
            "coded_width": 1280,
            "coded_height": 720,
            "has_b_frames": 0,
            "sample_aspect_ratio": "1:1",
            "display_aspect_ratio": "16:9",
            "pix_fmt": "yuv420p",
            "level": 32,
            "color_range": "tv",
            "color_space": "bt709",
            "color_transfer": "bt709",
            "color_primaries": "bt709",
            "chroma_location": "left",
            "field_order": "progressive",
            "refs": 1,
            "is_avc": "true",
            "nal_length_size": "4",
            "r_frame_rate": "60000/1001",
            "avg_frame_rate": "60000/1001",
            "time_base": "1/60000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 44016973,
            "duration": "733.616217",
            "bit_rate": "8069838",
            "bits_per_raw_sample": "8",
            "nb_frames": "43973",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_picindex": 0,
                "timedcodec_thumbnailsname": 0
            }"aac",
            "tagscodec_long_name": {
"AAC (Advanced Audio  Coding)",
            "creation_timeprofile": "2023-07-16T15:22:49.000000ZLC",
                "languagecodec_type": "undaudio",
                "handler_name"codec_time_base": "Core Media Video"
            }
1/48000",
          }
    ]"codec_tag_string": "mp4a",
    "format": {
        "filenamecodec_tag": "Dive2.mp40x6134706d",
            "nbsample_streamsfmt": 2"fltp",
            "nbsample_programsrate": 0"48000",
        "format_name": "mov,mp4,m4a,3gp,3g2,mj2"    "channels": 2,
            "formatchannel_long_namelayout": "QuickTime / MOVstereo",
        "start_time    "bits_per_sample": "0.000000",
            "durationr_frame_rate": "733.6162170/0",
        "size    "avg_frame_rate": "751679577",
0/0",
            "bittime_ratebase": "81969781/48000",
            "probestart_scorepts": 1000,
            "tagsstart_time": {"0.000000",
            "majorduration_brandts": "mp42"35213578,
            "minor_versionduration": "1733.616208",
            "compatiblebit_brandsrate": "isommp41mp42119736",
            "creationmax_bit_timerate": "2023-07-16T15:22:49.000000Z"128000",
        }
    }
}

Scripts

#!/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
Code Block
languagebash
titletimestamp.sh
collapsetrue
#!/usr/bin/env bash

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

# uuid"nb_frames": "34391",
            = 70204dea-f8b9-11ec-a408-cbfc46763958
# author"disposition": {
       = Troy Williams
# email      = troy.williams@bluebill.net
# date"default": 1,
        = 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
Code Block
languagebash
titlecorrect_times.sh
collapsetrue
 "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2023-07-16T15:22:49.000000Z",
                "language": "eng",
                "handler_name": "Core Media Audio"
            }
        },
        {
            "index": 1,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "High",
            "codec_type": "video",
            "codec_time_base": "1001/120000",
            "codec_tag_string": "avc1",
            "codec_tag": "0x31637661",
            "width": 1280,
            "height": 720,
            "coded_width": 1280,
            "coded_height": 720,
            "has_b_frames": 0,
            "sample_aspect_ratio": "1:1",
            "display_aspect_ratio": "16:9",
            "pix_fmt": "yuv420p",
            "level": 32,
            "color_range": "tv",
            "color_space": "bt709",
            "color_transfer": "bt709",
            "color_primaries": "bt709",
            "chroma_location": "left",
            "field_order": "progressive",
            "refs": 1,
            "is_avc": "true",
            "nal_length_size": "4",
            "r_frame_rate": "60000/1001",
            "avg_frame_rate": "60000/1001",
            "time_base": "1/60000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 44016973,
            "duration": "733.616217",
            "bit_rate": "8069838",
            "bits_per_raw_sample": "8",
            "nb_frames": "43973",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2023-07-16T15:22:49.000000Z",
                "language": "und",
                "handler_name": "Core Media Video"
            }
        }
    ],
    "format": {
        "filename": "Dive2.mp4",
        "nb_streams": 2,
        "nb_programs": 0,
        "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
        "format_long_name": "QuickTime / MOV",
        "start_time": "0.000000",
        "duration": "733.616217",
        "size": "751679577",
        "bit_rate": "8196978",
        "probe_score": 100,
        "tags": {
            "major_brand": "mp42",
            "minor_version": "1",
            "compatible_brands": "isommp41mp42",
            "creation_time": "2023-07-16T15:22:49.000000Z"
        }
    }
}


Scripts

ScriptDescription 
This script 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. This fixes issue with GoPro Videos.
Adds Timestamp in the top right hand corner of the video.