Skip to content

MPV:Example:TrimRange

mpvffmpeg를 사용하여 스샷(s key) 2장의 구간을 비디오로 잘라내는 bash 샘플

Bash script

#!/usr/bin/env bash

ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit; pwd)

USAGE="
Usage: ${BASH_SOURCE[0]} [options] video

Available options are:
  -h, --help       Print this message.
  -i {sec}         Specify update interval. (seconds)
  -t {dir}         Specify a temporary directory.
  -v, --verbose    Be more verbose/talkative during the operation.
  --               Stop handling options.
"

OPEN_FILE_PY_CODE="
from tkinter import filedialog as fd
print(fd.askopenfilename())
"

TEMP_DIR="$ROOT_DIR/temp"
VERBOSE=0
INTERVAL_SECONDS=1
INPUT_PATH=
MPV_PID=

function print_error
{
    # shellcheck disable=SC2145
    echo -e "\033[31m$@\033[0m" 1>&2
}

function print_message
{
    # shellcheck disable=SC2145
    echo -e "\033[32m$@\033[0m"
}

function print_verbose
{
    if [[ $VERBOSE -eq 1 ]]; then
        echo "$@"
    fi
}

trap 'cancel_black' INT

function cancel_black
{
    print_error "An interrupt signal was detected."
    if [[ -n $MPV_PID ]]; then
        kill "$MPV_PID"
    fi
    exit 1
}

function print_usage
{
    echo "$USAGE"
}

if ! command -v mpv &> /dev/null; then
    print_error "Not found mpv executable"
    exit 1
fi

if ! command -v ffmpeg &> /dev/null; then
    print_error "Not found ffmpeg executable"
    exit 1
fi

while [[ -n $1 ]]; do
    case $1 in
    -h|--help)
        print_usage
        exit 0
        ;;
    -v|--verbose)
        VERBOSE=1
        shift
        ;;
    -i)
        INTERVAL_SECONDS=$2
        shift 2
        ;;
    -t)
        TEMP_DIR=$2
        shift 2
        ;;
    --)
        shift
        break
        ;;
    -*)
        print_error "Unknown option: $1"
        exit 1
        ;;
    *)
        INPUT_PATH="$1"
        break
        ;;
    esac
done

if [[ -z $INPUT_PATH ]]; then
    INPUT_PATH=$(python -c "$OPEN_FILE_PY_CODE")
fi

if [[ ! -f "$INPUT_PATH" ]]; then
    print_error "Input file is required"
    exit 1
fi

if [[ -n $(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type f -a -name '*.jpg') ]]; then
    print_error "An image file exists in the temporary directory."
    print_error "Please remove all image files before proceeding."
    exit 1
fi

INPUT_PARENT="${INPUT_PATH%/*}"
INPUT_FILENAME="${INPUT_PATH##*/}"
INPUT_NAME=${INPUT_FILENAME%.*}
INPUT_EXT=${INPUT_FILENAME#*.}

mpv \
    --screenshot-template=%P \
    --screenshot-directory="$TEMP_DIR" \
    --screenshot-format=jpg \
    --screenshot-jpeg-quality=0 \
    "$INPUT_PATH" &> /dev/null &

MPV_PID=$!
echo "'mpv' process ID is $MPV_PID"
echo "To exit, press Ctrl+C"

function do_update
{
    print_verbose "Next update $(date "+%Y-%m-%d %H:%M:%S") ..."

    local files=()
    mapfile -t files < <(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type f -a -name '*.jpg' | sort)

    local files_count="${#files[*]}"
    if [[ $files_count -lt 2 ]]; then
        return 0
    fi

    local file0="${files[0]}"
    local file1="${files[1]}"

    local filename0="${file0##*/}"
    local filename1="${file1##*/}"

    local name0=${filename0%.*}
    local name1=${filename1%.*}

    local suffix0=${name0//:/}
    local suffix1=${name1//:/}

    local output="${INPUT_PARENT}/${INPUT_NAME}-${suffix0}_${suffix1}.${INPUT_EXT}"

    print_message "Trim video: $name0 ~ $name1"
    ffmpeg \
        -hide_banner \
        -loglevel error \
        -i "$INPUT_PATH" \
        -ss "$name0" \
        -to "$name1" \
        -c:v copy \
        "$output"

    rm -v "$file0" "$file1"
}

while true; do
    do_update

    if ! ps --pid $MPV_PID &> /dev/null; then
        echo "'mpv' process has terminated."
        exit 0
    fi

    sleep "${INTERVAL_SECONDS}s" &> /dev/null
done

Windows Git-Bash 에서 작동하는 샘플

우선 다음 프로그램들을 설치하자:

  • git-bash - Windows 용 Git 을 설치하면 된다.
  • python - MS Store 에서 설치 가능.
  • mpv - MS Store 에서 설치 가능. (Unofficial)
  • ffmpeg - FFmpeg 홈페이지에서 다운로드 가능.

위 패키지들은 choco, scoop, winget, 직접 설치 중 하나의 방식으로 설치하자.

만약 winget을 사용한다면 다음 명령으로 전부 설치 가능:

winget install --id Git.Git -e --source winget
winget install python
winget install mpv
winget install ffmpeg

다음 스크립트를 추가하자:

#!/usr/bin/env bash

if ! command -v mpv &> /dev/null; then
    echo "Not found mpv executable"
    exit 1
fi

if ! command -v ffmpeg &> /dev/null; then
    echo "Not found ffmpeg executable"
    exit 1
fi

USAGE="
Usage: ${BASH_SOURCE[0]} [options] video

Available options are:
  --interval={sec}  Specify update interval. (seconds)
  -i {sec}          Same '--interval' flag.
  --temp={dir}      Specify a temporary directory.
  -t {dir}          Same '--temp' flag.
  --extension={ext} Specify a output extension.
  -e {ext}          Same '--temp' flag.
  -h, --help        Print this message.
  --                Stop handling options.
"

SELECT_FILENAME_PY_CODE="
from tkinter import filedialog

print(filedialog.askopenfilename())
"

MPV_PID=

trap 'on_interrupt_trap' INT

function on_interrupt_trap
{
    echo "An interrupt signal was detected."

    if [[ -n $MPV_PID ]]; then
        echo -v "Send a KILL signal to the mpv ($MPV_PID)"
        kill "$MPV_PID"
    fi

    exit 1
}

function print_usage
{
    echo "$USAGE"
}

function mpv_cut_main
{
    local temp_dir=
    local interval_seconds=1
    local input_path=
    local output_ext=

    while [[ -n $1 ]]; do
        case $1 in
        --interval=*)
            interval_seconds=${1:11}
            shift
            ;;
        -i|--interval)
            interval_seconds=$2
            shift 2
            ;;
        --temp=*)
            temp_dir=${1:7}
            shift
            ;;
        -t|--temp)
            temp_dir=$2
            shift 2
            ;;
        --extension=*)
            output_ext=${1:12}
            shift
            ;;
        -e|--extension)
            output_ext=$2
            shift 2
            ;;
        -h|--help)
            print_usage
            exit 0
            ;;
        --)
            shift
            break
            ;;
        -*)
            echo -s "Invalid flag: $1"
            return 1
            ;;
        *)
            input_path="$1"
            break
            ;;
        esac
    done

    if [[ -z $input_path ]]; then
        input_path=$(python -c "$SELECT_FILENAME_PY_CODE")
    fi
    if [[ ! -f "$input_path" ]]; then
        echo "Not found input file."
        exit 1
    fi

    local input_parent="${input_path%/*}"
    local input_filename="${input_path##*/}"
    local input_name=${input_filename%.*}
    local input_ext=${input_filename#*.}

    if [[ -z $output_ext ]]; then
        output_ext=$input_ext
    fi

    if [[ -z $temp_dir ]]; then
        temp_dir="${input_path}.temp"
    fi
    if [[ ! -d "$temp_dir" ]]; then
        mkdir -vp "$temp_dir"
    fi

    if [[ -n $(find "$temp_dir" -mindepth 1 -maxdepth 1 -type f -a -name '*.jpg') ]]; then
        echo "An image file exists in the temporary directory."
        echo "Please remove all image files before proceeding."
        exit 1
    fi

    mpv \
        --screenshot-template=%P \
        --screenshot-directory="$temp_dir" \
        --screenshot-format=jpg \
        --screenshot-jpeg-quality=0 \
        "$input_path" &> /dev/null &

    MPV_PID=$!
    echo "'mpv' process ID is $MPV_PID"
    echo "To exit, press Q in mpv ..."

    local files
    declare -a files

    while true; do
        # echo "Next iter $(date "+%Y-%m-%d %H:%M:%S") ..."
        mapfile -t files < <(
            find "$temp_dir" -mindepth 1 -maxdepth 1 -type f -a -name '*.jpg' | sort
        )

        local files_count="${#files[*]}"
        if [[ $files_count -ge 2 ]]; then
            local file0="${files[0]}"
            local file1="${files[1]}"

            local filename0="${file0##*/}"
            local filename1="${file1##*/}"

            local begin_text=${filename0%.*}
            local end_text=${filename1%.*}

        local begin=$(echo $begin_text | sed -e 's/_/:/g')
        local end=$(echo $end_text | sed -e 's/_/:/g')

        local begin_short=$(echo $begin_text | sed -e 's/_//g')
        local end_short=$(echo $end_text | sed -e 's/_//g')

            local suffix="${begin_short}_${end_short}"
            local output="${input_name}-${suffix}.${output_ext}"

            echo "Input file: $input_path"
            echo "Output file: $output"
            echo "Cutting video playback section in progress ($begin ~ $end) ..."
            ffmpeg \
                -hide_banner \
                -loglevel error \
                -i "$input_path" \
                -ss "$begin" \
                -to "$end" \
                -c copy \
                "$output"

            local exit_code=$?
            if [[ $exit_code -eq 0 ]]; then
                echo "Video cutting was successful: $output"
            else
                echo "Failed to cut video: $exit_code"
            fi

            rm -v "$file0" "$file1"
        fi

        if ! ps -p $MPV_PID &> /dev/null; then
            echo "'mpv' process has terminated."
            exit 0
        fi

        sleep "${interval_seconds}s" &> /dev/null
    done
}

mpv_cut_main "$@"

주의할 점

mpv의 스냅샷 파일이 Windows 와 Linux 계열과 다르다. 스크립트 작성시 주의하자.

See also