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