#!/usr/bin/env bash set -o errexit -o nounset -o pipefail IFS=$'\n' # 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/cloud.ctbk.de/FSCK/2025/VOC/Talk-Intros/" OUTROS_PATH="${HOME}/Dokumente/cloud.ctbk.de/FSCK/2025/VOC/Talk-Outro/" CHUNKS_PATH="${HOME}/Dokumente/syncthing/fsck2025/streamingrechner/Recordings/" OUTPUT_PATH="${HOME}/Dokumente/syncthing/cwtv/cwtv-synthing/rendered/" WORKDIR=$(mktemp -d) function finish { rm -r "$WORKDIR" } trap finish EXIT # Function to convert image to video with audio convert_image_to_video() { local IMAGE_FILE="${1}" local VIDEO_FILE="${2}" local DURATION="${3}" local FADE_TYPE local FADE_START local FADE_DURATION if [[ "$4" == 0 ]] then # Fade-out FADE_TYPE="out" FADE_DURATION="${5}" FADE_START="$(echo "${DURATION} - ${FADE_DURATION} - 0.02" | bc | awk '{printf "%.5f\n", $0}')" # Note: The 0.02 = 1/50 is a hack to ensure we don't miss the last (fully black) frame else # Fade-in FADE_TYPE="in" FADE_DURATION="${4}" FADE_START="0" fi echo "Rendering fade:" echo "Type: ${FADE_TYPE}" echo "Start: ${FADE_START}" echo "Duration: ${FADE_DURATION}" ffmpeg \ -loop 1 \ -framerate 50 \ -i "${IMAGE_FILE}" \ -f lavfi -i anullsrc=r=48000:cl=stereo \ -vf "fade=type=${FADE_TYPE}:start_time=${FADE_START}:duration=${FADE_DURATION},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 \ -t "${DURATION}" "${VIDEO_FILE}" } # STEP 1 # 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") BASEPATH="$(basename "${SELECTED_INTRO}")" DEFAULT_OUTPUT_NAME="${BASEPATH%.*}" read -r -p "Please enter a name for the outputfile (path and extension will be added automatically): " -i "${DEFAULT_OUTPUT_NAME}" -e OUTPUT_NAME CONFIG_FILE="${OUTPUT_PATH}/${OUTPUT_NAME}.config" # find the start-offset for the first chunk read -r -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 -r -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 -r -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 -r -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 # print and log choices echo ; echo ; echo cat < "$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"