Last active
October 9, 2022 21:48
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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