14

I have a long MKV file which I want to split into its individual chapters.

Running ffmpeg -i long.mkv gives me all the information about the chapters embedded in the file:

 Duration: 01:23:45.80, start: 0.000000, bitrate: 8116 kb/s
    Chapter #0.0: start 0.000000, end 235.000000
    Metadata:
      title           : Chapter 01
    Chapter #0.1: start 235.000000, end 450.160000
    Metadata:
      title           : Chapter 02
    Chapter #0.2: start 450.160000, end 789.400000
    ...

There are 10 chapters in the file - I want to end up with 10 separate files.

It looks like -map_chapters might to something similar - but I can't find any documentation on it.

Hennes
  • 65,804
  • 7
  • 115
  • 169

7 Answers7

26

split mkv video by chapters using mkvmerge

mkvmerge -o output.mkv --split chapters:all input.mkv

https://www.bunkus.org/videotools/mkvtoolnix/doc/mkvmerge.html

Endoro
  • 3,014
9

I can't find a reliable way to do this with ffmpeg / avconv - but I can find a way to do this with HandBrakeCLI.

 HandBrakeCLI -c 3 -i whatever.mkv -o 3.mkv

Will extract chapter 3 from an mkv.

5

brute force solution, hehe:

ffmpeg -i long.mkv | grep 'start.*end.*[0-9]*' | sed -r 's/.*#[0-9]\.([0-9]*).* ([0-9]*\.[0-9]*).*( [0-9]*\.[0-9]*)/ ffmpeg -i long.mkv -ss \2 -to\3 -acodec copy -vcodec copy chapter\1.mkv/g;'

You can add xargs to run the output in cowboy style: | xargs -I cmd bash -c 'cmd'

0

This is my solution, it works well on ubuntu-16.04.02-LTS. It is based on another posted solution but has improved handling of chapters and the generated files for each chapter.

This is a sample execution:

$ mkv-split-chapters some-mkv-file.mkv
Filename: some-mkv-file
Extension: mkv
Filedir: .
ffmpeg -i some-mkv-file.mkv -ss 0.000000 -to 394.800000 -acodec copy -vcodec copy ./some-mkv-file-#00.mkv
[...]
ffmpeg -i some-mkv-file.mkv -ss 394.800000 -to 767.160000 -acodec copy -vcodec copy ./some-mkv-file-#01.mkv
[...]
ffmpeg -i some-mkv-file.mkv -ss 757.160000 -to 1216.720000 -acodec copy -vcodec copy ./some-mkv-file-#02.mkv
[...]

This is the script:

$ cat /usr/local/bin/mkv-split-chapters
#!/bin/bash
file="$1"
if [ -z "$file" ]; then
        echo "Missing file argument!"
        exit 1
fi

filename=$(basename "$file")
fileextension="${filename##*.}"
filename="${filename%.*}"
filedir=$(dirname "$file")
echo "Filename: $filename"
echo "Extension: $fileextension"
echo "Filedir: $filedir"
ffmpeg -i $file 2>&1 | grep 'Chapter' | grep 'start' | grep ', end' | awk "{
        chapter=\$2
        # replace : with nil
        gsub(/:/, \"\", chapter)
        start=\$4
        # remove everything but 0-9.
        gsub(/[^0123456789\.]/, \"\", start)
        end=\$6
        command=sprintf(\"ffmpeg -i $file -ss %s -to %s -acodec copy -vcodec copy $filedir/$filename-%s.$fileextension\n\", start, end, chapter)
        print(command)
        system(command)
}"

The script is also available here:

https://github.com/dpsenner/mkv-split-chapters

0

You can use the open source GUI program called LosslessCut. It losslessly cuts popular formats like mp4 and mkv instantly without reencoding the video. It works really well.

0

Improving upon @gabriel_agm's answer, here's my helper function. My ffmpeg uses a colon instead of a dot to separate title:chapter stuff, and outputted the useful info on stderr. I also add subtitle extraction.

split_by_chapter() {
   for word in "${@}" ;
   do
      ffmpeg -i "${word}" 2>&1 | sed -n -r -e "/start.*end.*[0-9]*/{s/.*#[0-9]*:([0-9]*).* ([0-9]*\.[0-9]*).*( [0-9]*\.[0-9]*)/ffmpeg -i ${word} -ss \2 -to\3 -acodec copy -vcodec copy -scodec copy ${word}-chapter\1.mkv \;/g;p;}"
   done
}

So to run the output you would then do this:

$( split_by_chapter file1.mkv file2.mkv file3.mkv )

Which then of course makes files like:

$ ls file*mkv
file1-chapter0.mkv file1-chapter1.mkv file1-chapter2.mkv
file2-chapter0.mkv file2-chapter1.mkv file3-chapter0.mkv

The caveats from other comments about keyframes limitations still apply, but it's tolerable.

bgStack15
  • 2,314
0

FFmpeg can't directly split on chapters, so you will have to either use another tool (like mkvmerge if it's an mkv, see Endoro's answer) or use a script to do it in two steps: first extract the chapter timestamps with ffprobe, then pass the timestamps to ffmpeg to split.

Note that the other answers rely on parsing the human-readable output of ffmpeg, which is not as reliable.

You can get the chapter information in json with ffprobe -hide_banner -v warning -output_format json -show_chapters $video_with_chapters

  • -hide_banner just hides the version/build information that ffmpeg/ffprobe normally prints on every invocation
  • -v warning only show warnings or worse on stderr
  • -output_format json output the data in json
  • -show_chapters show chapter information on stdout according to output_format

stdout will look something like:

{
  "chapters": [
    {
      "id": 3673034544977082062,
      "time_base": "1/1000000000",
      "start": 0,
      "start_time": "0.000000",
      "end": 270000000000,
      "end_time": "270.000000",
      "tags": {
        "title": "Chapter 01"
      }
    },
    {
      "id": -8793060959530591199,
      "time_base": "1/1000000000",
      "start": 270000000000,
      "start_time": "270.000000",
      "end": 300000000000,
      "end_time": "300.000000",
      "tags": {
        "title": "Chapter 02"
      }
    }
  ]
}

Then to slice out a single chapter, pass start_time to -ss (seek to this position) and end_time to -to (decode until this position) as input options (before the -i):

ffmpeg -ss 270.000000 -to 300.000000 -i long.mkv -c copy output-chapter-2.mkv


Here's a full script:

#!/usr/bin/env bash
set -e
video_with_chapters="$1"
video_basename="$(basename -- "$video_with_chapters")"
output_prefix="chapter-"
output_extension=".${video_basename##*.}"
chapters_json="$(ffprobe -hide_banner -v warning -output_format json -show_chapters "$video_with_chapters")"
chapter_count="$(echo "$chapters_json" | jq ".chapters|length")"
if (( "$chapter_count" == 0 )); then
  echo "$0: no chapters in $video_with_chapters" >&2
  exit 1
fi
for ((i=0; i < "$chapter_count"; i++ )); do
  chapter_start="$(echo "$chapters_json" | jq ".chapters[$i].start_time" -r)"
  chapter_end="$(echo "$chapters_json" | jq ".chapters[$i].end_time" -r)"
  if [[ "$chapter_start" == "$chapter_end" ]]; then
    # Some files have chapters as ranges of timestamps, other files have empty ranges and the chapters mark a point in time.
    if (( "$i" + 1 == "$chapter_count" )); then
      chapter_end=
    else
      chapter_end="$(echo "$chapters_json" | jq ".chapters[$((i+1))].end_time" -r)"
    fi
  fi
  input_args=(-ss "$chapter_start")
  if [[ -n "$chapter_end" ]]; then
    input_args+=(-to "$chapter_end")
  fi
  output_fn="${output_prefix}$(printf "%03d" "$i")${output_extension}"
  ffmpeg \
    -hide_banner -v warning \
    "${input_args[@]}" -i "$video_with_chapters" \
    -c copy "$output_fn"
done
echo "Split into chapters done" >&2

More thorough version at https://github.com/shelvacu/ffmpeg-chapter-split

Shelvacu
  • 111