{"id":622,"date":"2024-09-13T15:02:40","date_gmt":"2024-09-13T19:02:40","guid":{"rendered":"https:\/\/waterloobae.com\/?p=622"},"modified":"2025-08-20T14:26:29","modified_gmt":"2025-08-20T18:26:29","slug":"video-editing-bash-scripts-with-ffmpeg","status":"publish","type":"post","link":"https:\/\/waterloobae.com\/blog\/2024\/09\/13\/video-editing-bash-scripts-with-ffmpeg\/","title":{"rendered":"Video Editing Bash scripts with FFmpeg"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I asked ChatGPT to create some Bash scripts that encapsulate FFmpeq commands to make my video editing easier. I do not do much craze editing but just trimming,  cutting-off and so on. Here are mostly used my scripts.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Thumbnail creation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I try to create thumbnail out of my video. The script below captures the moment and print two sets of texts on it. <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">   #!\/bin\/bash\n\n# Check for correct number of arguments\nif [ \"$#\" -ne 5 ]; then\n    echo \"Usage: $0 &lt;video_file> &lt;time_frame> &lt;output_thumbnail> &lt;text1> &lt;text2>\"\n    exit 1\nfi\n\n# Assign arguments to variables\nVIDEO_FILE=\"$1\"\nTIME_FRAME=\"$2\"\nOUTPUT_THUMBNAIL=\"$3\"\nTEXT1=\"$4\"\nTEXT2=\"$5\"\n\n# Create a temporary image file for the thumbnail\nTEMP_IMAGE=$(mktemp \/tmp\/thumbnail.XXXXXX.png)\n\n# Extract the frame at the specified timeframe and scale to 1080p\nffmpeg -ss \"$TIME_FRAME\" -i \"$VIDEO_FILE\" -vframes 1 -q:v 2 -vf \"scale=1280:720\" \"$TEMP_IMAGE\"\n\n# Add two texts to the image using ffmpeg\nffmpeg -i \"$TEMP_IMAGE\" -vf \"drawtext=text='$TEXT1':fontcolor=yellow:fontsize=62:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)\/2:y=(h-text_h)\/2+140, drawtext=text='$TEXT2':fontcolor=white:fontsize=96:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)\/2:y=(h-text_h)\/2+220\" -y \"$OUTPUT_THUMBNAIL\"\n\n# Clean up the temporary image file\nrm \"$TEMP_IMAGE\"\n\n# Compress the thumbnail to ensure it's less than 2MB\nconvert \"$OUTPUT_THUMBNAIL\" -quality 85 -resize 1920x1080\\> \"$OUTPUT_THUMBNAIL\"\n\necho \"Thumbnail created and saved as $OUTPUT_THUMBNAIL\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Created thumbnails can be seen from my <a href=\"https:\/\/www.youtube.com\/@SwytchTrike\">YouTube channel<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Cutting-off<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This scripts cut off not necessary parts of video. In my case, I cut off cross walk waiting time during my commute. This takes one text file with time ranges as an input and it is possible to enter multiple time ranges. These ranges are ones you would like to keep.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"yaml\" class=\"language-yaml\">00:01:00 00:02:00\n00:05:00 00:10:00\n00:12:00 00:12:10<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Scripts give blurring around 1 seconds of beginning and ending of segments for smooth transitions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Blurring Faces<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This script one more package called <a href=\"https:\/\/github.com\/ORB-HD\/deface\" data-type=\"link\" data-id=\"https:\/\/github.com\/ORB-HD\/deface\">Deface<\/a> and it uses machine running to recognize face and blurring them. Since this package removes sound. This script saves sound first and combines sound back after blurring the face.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">#!\/bin\/bash\n\n# Check if the correct number of arguments is provided\nif [ \"$#\" -ne 1 ]; then\n    echo \"Usage: $0 &lt;input_video_file>\"\n    exit 1\nfi\n\n# Assign argument to variable\nINPUT_VIDEO=$1\nEXTRACTED_AUDIO=\"${INPUT_VIDEO%.*}_extracted_audio.mp3\"\nANONYMIZED_VIDEO=\"${INPUT_VIDEO%.*}_anonymized.mp4\"\nFINAL_OUTPUT=\"${INPUT_VIDEO%.*}_blurred.mp4\"\nSTABILIZED_OUTPUT=\"${FINAL_OUTPUT%.*}_final.mp4\"\nTRANSFORM_FILE=\"${FINAL_OUTPUT%.*}_transform.trf\"\n\n# Email details\nemail_subject=\"Video Processing Completed\"\nemail_body=\"The video processing job has been completed.\"\nrecipient_email=\"terry.bae@gmail.com\"  # Replace with your email address\n\n# Activate the virtual environment\nsource ~\/venv\/bin\/activate\n\n# Extract audio from the original video\necho \"Extracting audio from the original video...\"\nffmpeg  -hwaccel auto -i \"$INPUT_VIDEO\" -q:a 0 -map a \"$EXTRACTED_AUDIO\"\nif [[ $? -ne 0 ]] || [ ! -s \"$EXTRACTED_AUDIO\" ]]; then\n    echo \"Error: Failed to extract audio from the video or audio is empty.\"\n    deactivate\n    exit 1\nfi\n\n# Blur faces on the input video using deface\necho \"Blurring faces on the input video...\"\ndeface --scale 2560x1440 --thresh 0.5 \"$INPUT_VIDEO\"\nif [[ $? -ne 0 ]] || [ ! -s \"$ANONYMIZED_VIDEO\" ]]; then\n    echo \"Error: Failed to blur faces on the input video or anonymized video is empty.\"\n    deactivate\n    exit 1\nfi\n\n# Add the extracted audio back to the anonymized video\necho \"Adding extracted audio back to the anonymized video...\"\nffmpeg -hwaccel auto -i \"$ANONYMIZED_VIDEO\" -i \"$EXTRACTED_AUDIO\" -c copy -map 0:v:0 -map 1:a:0 \"$FINAL_OUTPUT\"\nif [[ $? -ne 0 ]] || [ ! -s \"$FINAL_OUTPUT\" ]]; then\n    echo \"Error: Failed to add audio to the anonymized video or final video is empty.\"\n    deactivate\n    exit 1\nfi\n\n# Step 1: Generate the stabilization transform file\n# echo \"Generating stabilization transform file...\"\n# \n# ffmpeg  -hwaccel auto -i \"$FINAL_OUTPUT\" -vf vidstabdetect=shakiness=5:accuracy=15:result=\"$TRANSFORM_FILE\" -f null -\n#  if [[ $? -ne 0 ]]; then\n#    echo \"Error: Failed to generate stabilization transform file.\"\n#    deactivate\n#    exit 1\n# fi\n\n# Step 2: Apply the stabilization transform to the video\n# echo \"Applying stabilization to the final output video...\"\n# ffmpeg -hwaccel auto -i \"$FINAL_OUTPUT\" -vf vidstabtransform=input=\"$TRANSFORM_FILE\",unsharp=5:5:0.8:3:3:0.4 -vcodec h264_videotoolbox -b:v 5000k -acodec copy \"$STABILIZED_OUTPUT\"\n# if [[ $? -ne 0 ]] || [ ! -s \"$STABILIZED_OUTPUT\" ]]; then\n#    echo \"Error: Failed to stabilize the video or stabilized video is empty.\"\n#    deactivate\n#    exit 1\n# fi\n\nClean up intermediate files\necho \"Cleaning up intermediate files...\"\nrm \"$EXTRACTED_AUDIO\" \"$TRANSFORM_FILE\"\n\n# Deactivate the virtual environment\ndeactivate\n\n# Send an email notification\n# echo -e \"$email_body\" | mail -s \"$email_subject\" \"$recipient_email\"\n\n# echo \"Process completed successfully. Stabilized output: $STABILIZED_OUTPUT\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">End part of the script has logic for stabilize video. They are commented out since I do not use them any more.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Misc Editing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Other tasks like concatenating, trimming, extracting sound and combining sound can be done by following script.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">#!\/bin\/bash\n\n# Function to display menu and get user choice\nfunction show_menu {\n    echo \"Select a command to execute:\"\n    echo \"1) Concatenate: ffmpeg -f concat -i &lt;input_text_file> -c copy &lt;output_video_file>\"\n    echo \"2) Extract Sound: ffmpeg -i &lt;input_video_file> -q:a 0 -map a &lt;output_sound_file>\"\n    echo \"3) Combine Sound: ffmpeg -i &lt;input_video_file1> -i &lt;input_sound_file2> -c copy -map 0:v:0 -map 1:a:0 &lt;output_video_file>\"\n    echo \"4) Trim: ffmpeg -ss &lt;start_time> -to &lt;end_time> -i &lt;input_video_file> -c copy &lt;output_video_file>\"\n    echo \"5) Mute: ffmpeg -i &lt;input_video_file> -vcodec copy -an &lt;output_video_file>\"\n    read -p \"Enter choice [1-5]: \" choice\n}\n\nshow_menu\n\n# Execute the selected command\ncase $choice in\n    1)\n        read -p \"Enter input text file: \" INPUT_FILE\n        read -p \"Enter output video file: \" OUTPUT_FILE\n        echo \"Concatenating files...\"\n        ffmpeg -hwaccel auto -f concat -i \"$INPUT_FILE\" -c copy \"$OUTPUT_FILE\"\n        ;;\n    2)\n        read -p \"Enter input video file: \" INPUT_FILE\n        read -p \"Enter output sound file: \" OUTPUT_FILE\n        echo \"Extracting sound from video...\"\n        ffmpeg -hwaccel auto -i \"$INPUT_FILE\" -q:a 0 -map a \"$OUTPUT_FILE\"\n        ;;\n    3)\n        read -p \"Enter first input video file: \" INPUT_FILE1\n        read -p \"Enter second input sound file: \" INPUT_FILE2\n        read -p \"Enter output video file: \" OUTPUT_FILE\n        echo \"Combining video and sound...\"\n        ffmpeg -hwaccel auto -i \"$INPUT_FILE1\" -i \"$INPUT_FILE2\" -c copy -map 0:v:0 -map 1:a:0 \"$OUTPUT_FILE\"\n        ;;\n    4)\n        read -p \"Enter start time 00:00:00 : \" START_TIME\n        read -p \"Enter end time 00:00:00 : \" END_TIME\n        read -p \"Enter input video file: \" INPUT_FILE\n        read -p \"Enter output video file: \" OUTPUT_FILE\n        echo \"Trimming video...\"\n        ffmpeg -hwaccel auto -ss \"$START_TIME\" -to \"$END_TIME\" -i \"$INPUT_FILE\" -c copy \"$OUTPUT_FILE\"\n        ;;\n    5)\n        read -p \"Enter input video file: \" INPUT_FILE\n        read -p \"Enter output video file: \" OUTPUT_FILE\n        echo \"Muting video...\"\n\tffmpeg -hwaccel auto -i \"$INPUT_FILE\" -vcodec copy -an \"$OUTPUT_FILE\" \n\t;;\n    *)\n        echo \"Invalid choice.\"\n        exit 1\n        ;;\nesac\n\necho \"Command executed successfully.\"\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>I asked ChatGPT to create some Bash scripts that encapsulate FFmpeq commands to make my video editing easier. I do not do much craze editing but just trimming, cutting-off and so on. Here are mostly used my scripts. 1. Thumbnail creation I try to create thumbnail out of my video. The script below captures the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,22],"tags":[],"class_list":["post-622","post","type-post","status-publish","format-standard","hentry","category-it-tips","category-video"],"_links":{"self":[{"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/posts\/622","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/comments?post=622"}],"version-history":[{"count":3,"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/posts\/622\/revisions"}],"predecessor-version":[{"id":631,"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/posts\/622\/revisions\/631"}],"wp:attachment":[{"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/media?parent=622"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/categories?post=622"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/waterloobae.com\/blog\/wp-json\/wp\/v2\/tags?post=622"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}