forked from cwtv/stuff
247 lines
8.8 KiB
Bash
Executable File
247 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# License MIT
|
|
# Authors 2024
|
|
# thunfisch
|
|
# iiidefix
|
|
# l3d
|
|
|
|
# This script is designed to help you automatically select the correct intro
|
|
# and outro files (prerendered with built-in fades) and the correct chunks
|
|
# of a chunked recording (e.g. OBS with automatic split every 5 minutes).
|
|
# It will open the first and last chunk in mpv if you want, so that you
|
|
# can find the start and end points (in seconds), you manually need to transfer
|
|
# them into the script for now (no idea how to convince mpv to output it).
|
|
# After that the script will render the introfile with the first chunk and the
|
|
# last chunk with the outrofile, and then in the last step assemble everything
|
|
# into the final recording with audio normalization.
|
|
# It will also assume, that the intros and outros are pictures...
|
|
|
|
# This script requires you to have ffmpeg and fzf installed.
|
|
which ffmpeg >/dev/null || (echo "Please install ffmpeg" ; exit 1)
|
|
which fzf >/dev/null || (echo "Please install fzf" ; exit 1)
|
|
which mpv >/dev/null || (echo "Please install mpv" ; exit 1)
|
|
|
|
INTROS_PATH="$HOME/Dokumente/syncthing/intros"
|
|
OUTROS_PATH="$HOME/Dokumente/syncthing/outros"
|
|
CHUNKS_PATH="$HOME/Dokumente/syncthing/"
|
|
OUTPUT_PATH="$HOME/Videos/WK25/rendered"
|
|
|
|
# temp dir
|
|
WORKDIR=$(mktemp -d)
|
|
function finish {
|
|
rm -r "$WORKDIR"
|
|
}
|
|
trap finish EXIT
|
|
|
|
# Select the appropriate files
|
|
SELECTED_INTRO="$(find "$INTROS_PATH" -type f | sort --reverse --human-numeric-sort | fzf --delimiter / --with-nth -1 --prompt "Intro File:")"
|
|
SELECTED_OUTRO="$(find "$OUTROS_PATH" -type f | sort --reverse --human-numeric-sort | fzf --delimiter / --with-nth -1 --prompt "Outro File:")"
|
|
SELECTED_CHUNKS="$(find "$CHUNKS_PATH" -type f | sort --reverse | fzf --delimiter / --with-nth -1 -m --prompt "Video Chunks (use tab to select multiple):" | sort )"
|
|
readarray -t CHUNKS_ARRAY < <(echo "$SELECTED_CHUNKS")
|
|
|
|
FOOBARWTF="$(basename "$SELECTED_INTRO")"
|
|
DEFAULT_OUTPUT_NAME="${FOOBARWTF%.*}"
|
|
read -p "Please enter a name for the outputfile (path and extension will be added automatically): " -i "$DEFAULT_OUTPUT_NAME" -e OUTPUT_NAME
|
|
|
|
# Function to convert image to video with audio
|
|
convert_image_to_video() {
|
|
local image_file="$1"
|
|
local output_video="$2"
|
|
local duration="$3"
|
|
local fade_in="$4"
|
|
local fade_out="$5"
|
|
|
|
if [[ $fade_in == 0 ]]
|
|
then
|
|
ffmpeg -loop 1 \
|
|
-framerate 50 \
|
|
-t "$duration" \
|
|
-i "$image_file" -f lavfi \
|
|
-i anullsrc=r=48000:cl=stereo \
|
|
-vf "fade=t=out:st=$(($duration - $fade_out)):d=$fade_out,format=pix_fmts=yuv420p,fps=50" \
|
|
-c:a aac -b:a 192k \
|
|
-c:v libx264 -threads 0 -pix_fmt yuv420p -crf 18 \
|
|
-profile:v high -level 4.1 -disposition default -color_range tv \
|
|
-metadata:s:a:0 language=native \
|
|
-t "$duration" "$output_video"
|
|
else
|
|
ffmpeg -loop 1 \
|
|
-framerate 50 \
|
|
-t "$duration" \
|
|
-i "$image_file" -f lavfi \
|
|
-i anullsrc=r=48000:cl=stereo \
|
|
-filter_complex "fade=t=in:st=0:d=$fade_in,format=pix_fmts=yuv420p,fps=50" \
|
|
-c:a aac -b:a 192k \
|
|
-c:v libx264 -threads 0 -pix_fmt yuv420p -crf 18 \
|
|
-profile:v high -level 4.1 -disposition default -color_range tv \
|
|
-metadata:s:a:0 language=native \
|
|
-t "$duration" "$output_video"
|
|
fi
|
|
}
|
|
|
|
|
|
# find the start-offset for the first chunk
|
|
read -p "Do you want to play the first chunk ${CHUNKS_ARRAY[0]} to find the start-offset? (y/n) [n]: " PLAY_FIRST_CHUNK
|
|
PLAY_FIRST_CHUNK="${PLAY_FIRST_CHUNK:-n}"
|
|
|
|
[[ "$PLAY_FIRST_CHUNK" == "y" ]] && mpv "${CHUNKS_ARRAY[0]}" --osd-level=3 --osd-status-msg='${=time-pos}' --really-quiet
|
|
|
|
read -p "Enter the start-offset in seconds for the first chunk ${CHUNKS_ARRAY[0]} [0]: " START_OFFSET
|
|
START_OFFSET="${START_OFFSET:-0}"
|
|
|
|
|
|
# find the end-offset for the last chunk
|
|
read -p "Do you want to play the last chunk ${CHUNKS_ARRAY[-1]} to find the end-offset? (y/n) [n]: " PLAY_LAST_CHUNK
|
|
PLAY_LAST_CHUNK="${PLAY_LAST_CHUNK:-n}"
|
|
|
|
[[ "$PLAY_LAST_CHUNK" == "y" ]] && mpv "${CHUNKS_ARRAY[-1]}" --osd-level=3 --osd-status-msg='${=time-pos}' --really-quiet
|
|
|
|
read -p "Enter the end-offset in seconds for the last chunk ${CHUNKS_ARRAY[0]} [1]: " END_OFFSET
|
|
END_OFFSET="${END_OFFSET:-1}"
|
|
|
|
# Check if intro is an image and convert if necessary
|
|
EXT_INTRO="${SELECTED_INTRO##*.}"
|
|
if [[ "$EXT_INTRO" == "png" || "$EXT_INTRO" == "jpg" || "$EXT_INTRO" == "jpeg" ]]; then
|
|
INTRO_VIDEO="$WORKDIR/intro_converted.mkv"
|
|
convert_image_to_video "$SELECTED_INTRO" "$INTRO_VIDEO" 4 0.5 0
|
|
SELECTED_INTRO="$INTRO_VIDEO"
|
|
fi
|
|
|
|
# Check if outro is an image and convert if necessary
|
|
EXT_OUTRO="${SELECTED_OUTRO##*.}"
|
|
if [[ "$EXT_OUTRO" == "png" || "$EXT_OUTRO" == "jpg" || "$EXT_OUTRO" == "jpeg" ]]; then
|
|
OUTRO_VIDEO="$WORKDIR/outro_converted.mkv"
|
|
convert_image_to_video "$SELECTED_OUTRO" "$OUTRO_VIDEO" 6 0 1
|
|
SELECTED_OUTRO="$OUTRO_VIDEO"
|
|
fi
|
|
|
|
cat <<EOT
|
|
|
|
|
|
|
|
I will be rendering with the following configuration:
|
|
+ Selected Intro: ${SELECTED_INTRO}
|
|
+ Selected Outro: ${SELECTED_OUTRO}
|
|
+ Video Chunks:
|
|
+ First chunk: ${CHUNKS_ARRAY[0]}
|
|
Starting at second ${START_OFFSET}
|
|
+ Last chunk: ${CHUNKS_ARRAY[$((${#CHUNKS_ARRAY[@]} - 1))]}
|
|
Ending at second ${END_OFFSET}
|
|
+ All chunks:
|
|
EOT
|
|
for index in "${!CHUNKS_ARRAY[@]}"
|
|
do
|
|
echo " + $index: ${CHUNKS_ARRAY[index]}"
|
|
done
|
|
echo "Export: ${OUTPUT_PATH}/${OUTPUT_NAME}.mkv"
|
|
echo ; echo ; echo
|
|
read -p "Do you want to proceed with this configuration? (y/n) [y]" PROCEED
|
|
PROCEED="${PROCEED:-y}"
|
|
|
|
[[ "$PROCEED" == "y" ]] || (echo "aborting"; exit 1)
|
|
echo "doing ffmpeg things here"
|
|
|
|
# combine the videos...
|
|
ARRAY_LENGTH="${#CHUNKS_ARRAY[@]}"
|
|
if [[ ${ARRAY_LENGTH} -lt 2 ]]
|
|
then
|
|
echo "Too few chunks, this script can't handle this yet. Please do that on your own."
|
|
exit 1
|
|
fi
|
|
|
|
# STEP 1
|
|
# temp dir
|
|
WORKDIR=$(mktemp -d)
|
|
function finish {
|
|
rm -r "$WORKDIR"
|
|
}
|
|
trap finish EXIT
|
|
|
|
# STEP 2
|
|
# Dauer des Intros ermitteln
|
|
echo "==== STEP 2 ===="
|
|
DURATION_INTRO=$(ffprobe -i "$SELECTED_INTRO" -show_entries format=duration -v quiet -of csv="p=0")
|
|
|
|
# Sicherstellen, dass die Dauer gültig ist
|
|
if [[ -z "$DURATION_INTRO" || "$DURATION_INTRO" == "N/A" ]]; then
|
|
echo "Fehler: Die Dauer des Intros konnte nicht ermittelt werden."
|
|
exit 1
|
|
fi
|
|
|
|
# Offset berechnen und negative Werte verhindern
|
|
OFFSET=$(echo "scale=2; $DURATION_INTRO - 0.5" | bc)
|
|
if (( $(echo "$OFFSET < 0" | bc -l) )); then OFFSET=0; fi
|
|
|
|
# Prüfen, ob CHUNKS_ARRAY existiert und nicht leer ist
|
|
if [[ -z "${CHUNKS_ARRAY[0]}" ]]; then
|
|
echo "Fehler: CHUNKS_ARRAY ist leer oder nicht definiert."
|
|
exit 1
|
|
fi
|
|
|
|
CROSSFADE_DURATION="0.5"
|
|
|
|
# Intro mit Crossfade zum ersten Chunk
|
|
ffmpeg -i "$SELECTED_INTRO" -ss "$START_OFFSET" -i "${CHUNKS_ARRAY[0]}" \
|
|
-filter_complex \
|
|
"[0:v:0]format=pix_fmts=yuv420p,fps=50[va]; \
|
|
[1:v:0]format=pix_fmts=yuv420p,fps=50[vb]; \
|
|
[va][vb]xfade=transition=fade:duration=${CROSSFADE_DURATION}:offset=${OFFSET}[v]; \
|
|
[1:a:0]afade=t=in:st=0:d=${CROSSFADE_DURATION}[a]; \
|
|
[0:a:0][a]concat=n=2:v=0:a=1[a0]" \
|
|
-map '[v]' -map '[a0]' \
|
|
-c:a aac -b:a 192k \
|
|
-c:v libx264 -threads 4 -pix_fmt yuv420p -crf 18 -profile:v high -level 4.1 -disposition default \
|
|
-movflags +faststart \
|
|
-metadata:s:a:0 language=native \
|
|
"${WORKDIR}/introcombined.mkv"
|
|
|
|
# STEP 3
|
|
# Outro mit Crossfade vom letzten Chunk
|
|
echo "==== STEP 3 ===="
|
|
FOO=$(echo "${END_OFFSET} - ${CROSSFADE_DURATION}" | bc)
|
|
ffmpeg -i "$SELECTED_OUTRO" -t "$END_OFFSET" -i "${CHUNKS_ARRAY[$((${#CHUNKS_ARRAY[@]} - 1))]}" \
|
|
-filter_complex \
|
|
"[1:v:0]format=pix_fmts=yuv420p,fps=50[v1]; \
|
|
[0:v:0]format=pix_fmts=yuv420p,fps=50[v0]; \
|
|
[v1][v0]xfade=transition=fade:duration=${CROSSFADE_DURATION}:offset=${FOO}[v]; \
|
|
[1:a:0]afade=t=out:st=${FOO}:d=${CROSSFADE_DURATION}[a1]; \
|
|
[0:a:0]afade=t=in:st=0:d=${CROSSFADE_DURATION}[a0]; \
|
|
[a1][a0]acrossfade=d=1[a]" \
|
|
-map "[v]" -map "[a]" \
|
|
-c:a aac -b:a 192k \
|
|
-c:v libx264 -threads 0 -pix_fmt yuv420p -crf 18 -profile:v high -level 4.1 -disposition default \
|
|
-metadata:s:a:0 language=native \
|
|
"${WORKDIR}/outrocombined.mkv"
|
|
|
|
# STEP 4
|
|
# Encoded intro+outro und alle Chunks in between mit c:v copy und audio dynnorm + encode
|
|
echo "==== STEP 4 ===="
|
|
|
|
CHUNKLIST="${WORKDIR}/chunklist.txt"
|
|
|
|
echo "file '${WORKDIR}/introcombined.mkv'" > "$CHUNKLIST"
|
|
|
|
FFMPEG_CONCAT_CHUNKS=""
|
|
if [[ ${ARRAY_LENGTH} -gt 2 ]]
|
|
then
|
|
for index in $(seq 1 $(( ${ARRAY_LENGTH} - 2 )) )
|
|
do
|
|
FFMPEG_CONCAT_CHUNKS="${FFMPEG_CONCAT_CHUNKS}|${CHUNKS_ARRAY[index]}"
|
|
echo "file '${CHUNKS_ARRAY[index]}'" >> "$CHUNKLIST"
|
|
done
|
|
fi
|
|
|
|
echo "file '${WORKDIR}/outrocombined.mkv'" >> "$CHUNKLIST"
|
|
|
|
ffmpeg \
|
|
-f concat -safe 0 -i "$CHUNKLIST" \
|
|
-af dynaudnorm \
|
|
-c:v copy \
|
|
-c:a aac -b:a 192k \
|
|
-metadata:s:a:0 language=native \
|
|
"${OUTPUT_PATH}/${OUTPUT_NAME}.mkv"
|
|
|
|
echo "Video Exported to ${OUTPUT_PATH}/${OUTPUT_NAME}.mkv"
|