Skip to content

Instantly share code, notes, and snippets.

@djalilhebal
Last active October 9, 2022 21:48
Show Gist options
  • Save djalilhebal/6605f3cedc9effadc8bd778cf20ffdd5 to your computer and use it in GitHub Desktop.
Save djalilhebal/6605f3cedc9effadc8bd778cf20ffdd5 to your computer and use it in GitHub Desktop.
Video editing using Bash and ffmpeg to cut, concat, and create dynamic text overlays (SVG)
#!/bin/bash
###
## @file FFmpeg - Hated By Life Itself
## @date 2020-10-30
## @author Abdeldjalil Hebal (@djalilhebal)
## @license WTFPL
##
## @description Video editing using Bash and ffmpeg to cut, concat, and create dynamic text overlays (SVG).
##
## This is the best I can do with my current "video editing skills":
## Using ffmpeg: cut, concat, compose (overlay), combine (add tracks), and convert stuff...
## This weekend project's goals: Trying ffmpeg's `-to` option, `overlay` filter, and SVG rasterization;
## in addition to Bash's arrays and splitting strings.
##
## NOTES:
## - Script linted with https://www.shellcheck.net
## - Tested with snap ffmpeg version 4.1.4 (without `--enable-librsvg`) and rsvg-convert version 2.40.20.
## - Update 2020-11-08: Tested with homebrew-ffmpeg version 4.3.1 (with `--enable-librsvg`).
##
## LINKS:
## - "Hated by Life Itself" on VocaDB (Vocaloid Database): https://vocadb.net/S/164273
## - ffmpeg on Snapcraft: https://snapcraft.io/ffmpeg
## - homebrew-ffmpeg on GitHub: https://github.com/homebrew-ffmpeg/homebrew-ffmpeg
## - FFmpeg - overlay filter (options and examples): https://ffmpeg.org/ffmpeg-filters.html#overlay-1
## - FFmpeg - Concatenate (apparently not all formats are concat'able): https://trac.ffmpeg.org/wiki/Concatenate
## - StackOverflow - about ffmpeg support for SVG rasterization: https://stackoverflow.com/questions/48216871/
## (TLDR: ffmpeg needs to be built with `--enable-librsvg`)
###
PROJECT_ID='ffmpeg-hated-by-life-itself';
SOURCES_DIR="$HOME/Downloads/Hated by Life Itself/";
# FORMAT: `youtubeId|fromTimestamp|toTimestamp|singer|lyrics|themeColor`
CLIPS=(
# VOCALOID. Kaito! \^o^/
"M1_2oeFuoto|00:00|00:54|Gakupo and KAITO|{ lyrics go here }|blue"
# Danganronpa V3 music video. Not on Mafumafu's channel
"uMRsSUtNLIw|00:50|02:06|Mafumafu|{ lyrics go here }|purple"
# English cover
"XVuydzqDAxU|02:06|02:48|Oktavia|Numb to the pain, can I die? Though I'm not afraid...|white"
# It has in-video transcriptions/translations (JP, KO, and EN)
"FoAaGR4at8I|02:57|03:36|Raon Lee|Happiness, farewells, affection, and friendship...|orange"
# VOCALOID. MMD music video
"q195eEm8s4I|03:32|03:50|Len and Miku|Regardless, we will always live in desperation...|yellow"
);
# About `~/tmp`, I had to do it this way because I wanted to use /tmp which snap ffmpeg can't access;
# I'm following the solution suggested by https://askubuntu.com/a/1264341
WORKING_DIR="$HOME/tmp/$PROJECT_ID";
CONCAT_LIST_FILE="$WORKING_DIR/concat-list.txt";
CONCAT_OUTPUT_FILE="$WORKING_DIR/concat-result.mts";
FINAL_OUTPUT_FILE="$WORKING_DIR/final.mp4";
# 0. Prepare.
set -e # Bash option to exit immediately if a command exits with a non-zero status.
mkdir -p "$WORKING_DIR";
rm -f "$WORKING_DIR"/*;
#rm -f "$CONCAT_LIST_FILE";
###
## Overlay component. Renders to SVG static markup (a la React's `renderToStaticMarkup` method).
##
## Note: We could use an associative array ("clipObj") instead, but that would be too much bashism.
## Also, passing associative arrays as arguments isn't clean or straightforward either.
##
## @param {string} $1 clip info string
## @returns {string} SVG markup
###
renderOverlayToStaticMarkup() {
local clip="$1";
# Parse clip info string
# FORMAT: `youtubeId|fromTimestamp|toTimestamp|singer|lyrics|themeColor`
IFS='|' read -ra parts <<< "$clip"; # good enough as long as IFS affects only this command.
local youtubeId="${parts[0]}";
local fromTimestamp="${parts[1]}";
local toTimestamp="${parts[2]}";
local singer="${parts[3]}";
local lyrics="${parts[4]}";
local themeColor="${parts[5]}";
local youtubeLink="https://youtu.be/$youtubeId";
echo "<svg width=\"1280\" height=\"720\">
<g class=\"clip-info\" x=\"0\" y=\"50%\" width=\"100%\" height=\"33%\">
<rect class=\"clip-info__background\" style=\"opacity: 0.5; fill: $themeColor;\" width=\"100%\" height=\"33%\" x=\"0\" y=\"33%\" />
<text class=\"clip-info__singer\" x=\"20\" y=\"40%\" transform=\"translate(0, 20)\"
style=\"font-size: 60px; font-weight: bold; stroke: white; stroke-width: 2px;\">
VOCALS: $singer
</text>
<text class=\"clip-info__lyrics\" x=\"20\" y=\"50%\" style=\"font-size: 40px;\">
$lyrics
</text>
<text class=\"clip-info__source\" x=\"20\" y=\"60%\" style=\"font-size: 40px; font-family: mono;\">
SOURCE: $youtubeLink ($fromTimestamp - $toTimestamp)
</text>
</g>
</svg>";
}
###
## Maybe pre-rasterize.
## Use `ffmpeg` if it supports SVG (i.e. was built with `--enable-librsvg`); otherwise, fall back
## to `librsvg2-bin`'s `rsvg2-convert` to accomplish the same thing (that is, to rasterize the SVG).
##
## @param {string} $1 path to the SVG file
## @returns {string} path to the raw SVG or the rasterized version (PNG)
###
getAppropriateOverlayFile() {
local svgFile="$1";
local pngFile="$svgFile.png";
local ffmpegSvgFlag=$(ffmpeg -buildconf | grep -- "--enable-librsvg" || echo -n "");
if [ -n "$ffmpegSvgFlag" ]; then
echo "$svgFile";
else
# To install rsvg-convert: `apt install librsvg2-bin`
#rsvg-convert --version || exit 1
rsvg-convert "$svgFile" -o "$pngFile";
echo "$pngFile";
fi
}
# 1. Process parts ("for clip of $CLIPS do...")
i="0";
while [ -n "${CLIPS[$i]}" ]; do
clip="${CLIPS[$i]}";
echo "Processing $clip";
# 1.1. Parse clip info
IFS='|' read -ra parts <<< "$clip";
youtubeId="${parts[0]}";
fromTimestamp="${parts[1]}";
toTimestamp="${parts[2]}";
#singer="${parts[3]}";
#lyrics="${parts[4]}";
#themeColor="${parts[5]}";
# 1.2 Find the original video file (downloaded using `youtube-dl`)
# PATTERN: <title>-<youtubeId>.{mp4,mkv,webm,avi,whatever}
#videoInputFile=$(find "$SOURCES_DIR" -name "*-$youtubeId.*"); # <- this is actually good enough.
videoInputFile=$(find "$SOURCES_DIR" -maxdepth 1 -type f -and -name "*-$youtubeId.*" -print);
# 1.3. Render the overlay to "static markup"
svgInputFile="$WORKING_DIR/clip-$i-$youtubeId.svg";
svgStaticMarkup=$(renderOverlayToStaticMarkup "$clip");
echo "$svgStaticMarkup" > "$svgInputFile";
# the above file (SVG) or the pre-rasterized version (PNG)
appropriateOverlayFile=$(getAppropriateOverlayFile "$svgInputFile");
# 1.4 Clip the original video and add the overlay
# Overlay the 2nd video (SVG image) on top of the first video (actual video)
# - Note: `-filter_complex 'overlay'` is equivalent to `-filter_complex '[0:v][1:v] overlay=x=0:y=0'`
# - Maybe "size" should be be the same as the SVG's, same as alias `hd720`
outputFile="$WORKING_DIR/clip-$i-$youtubeId.mts";
ffmpeg \
-ss "$fromTimestamp" -to "$toTimestamp" \
-i "$videoInputFile" \
-i "$appropriateOverlayFile" \
-filter_complex '[0:v][1:v] overlay' \
-q 0 \
-s 1280x720 \
-y \
"$outputFile";
# 1.5 Append to the concat list
echo "file '$outputFile'" >> "$CONCAT_LIST_FILE";
i=$((i + 1));
done;
# 2. Concat parts.
# About `-safe 0`, see https://stackoverflow.com/a/56029574
ffmpeg -f concat -safe 0 -i "$CONCAT_LIST_FILE" -c copy -y "$CONCAT_OUTPUT_FILE";
# 3. Convert to the final output's format (inferred from its extension).
ffmpeg -i "$CONCAT_OUTPUT_FILE" -q 0 -y "$FINAL_OUTPUT_FILE"
# 4. Play the resulting video.
ffplay "$FINAL_OUTPUT_FILE"
echo 'DONE.';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment