Search code examples
bashshellffmpeg

How can I add chapters to a mp4 file using bash script and ffmpeg


I'm trying to add chapters to an mp4 file using bash and ffmpeg. The chapters are located in a file called chapters.txt. When the bash script is run which is called add_chapters.sh it reads the chapters.txt file and creates a file called chapters.ffmetadata. The issue is that the START and END times in the file chapters.ffmetadata aren't being created correctly. How can I fix this?

  1. I have a file called chapters.txt that has the chapters in it.

    00:00:00=Intro
    00:02:12=What are selections
    00:03:19=Booleans
    
  2. The bash file I run is add_chapters.sh

    The code in it is:

    #!/bin/bash
    
    input_file="chapters.txt"
    output_file="chapters.ffmetadata"
    
    # Create the FFmetadata chapter file
    echo ";FFMETADATA1" > "$output_file"
    
    previous_seconds=0
    
    while IFS="=" read -r timestamp chapter_name; do
        # Convert start time to seconds
        seconds=$(date -u -d "1970-01-01 $timestamp" +"%s")
    
        # Calculate the end time (previous chapter's start time)
        end_seconds=$previous_seconds
    
        # Update previous_seconds for the next iteration
        previous_seconds=$seconds
    
        echo "[CHAPTER]" >> "$output_file"
        echo "TIMEBASE=1/1000" >> "$output_file"
        echo "START=${end_seconds}000" >> "$output_file"
        echo "END=${seconds}000" >> "$output_file"
        echo "title=$chapter_name" >> "$output_file"
    done < "$input_file"
    
    echo "Chapter file '$output_file' created successfully."
    
    # Run FFmpeg to add chapters to the video
    ffmpeg -i input_video.mp4 -i chapters.ffmetadata -map_metadata 1 -c:v copy -c:a copy -y output_video.mp4
    
  3. It creates a file called chapters.ffmetadata

    ;FFMETADATA1
    [CHAPTER]
    TIMEBASE=1/1000
    START=0000
    END=0000
    title=Intro
    [CHAPTER]
    TIMEBASE=1/1000
    START=0000
    END=132000
    title=What are selections
    [CHAPTER]
    TIMEBASE=1/1000
    START=132000
    END=199000
    title=Booleans
    [CHAPTER]
    TIMEBASE=1/1000
    START=199000
    END=0000
    title=
    

    The START and END times aren't being calculated correctly. How can I fix this?


UPDATE #1

I'm almost there thanks to @Freeman code / answer.

The issues in the generated chapters.ffmetadata file are just.

  1. The first START= and END= are 0000

  2. The Intro Chapter doesn't show up (most likely caused by the first START= and END= being 0000)

  3. The Chapters seem to be shifted down by one timecode line (most likely caused by the first START= and END= being 0000)

    ;FFMETADATA1 [CHAPTER] TIMEBASE=1/1000 START=0000 END=0000 title=Intro [CHAPTER] TIMEBASE=1/1000 START=0000 END=132000 title=What are selections? [CHAPTER] TIMEBASE=1/1000 START=132000 END=199000 title=Booleans [CHAPTER] TIMEBASE=1/1000 START=199000 END=0000 title= [CHAPTER] TIMEBASE=1/1000 START=0000 END=4459000 title=


UPDATE #2

Strange; I tried the changes @Freeman suggested but an error comes up now.

Chapter end time 0 before start 199000 chapters.ffmetadata: Cannot allocate memory

;FFMETADATA1
[CHAPTER]
TIMEBASE=1/1000
START=0000
END=0000
title=Intro
[CHAPTER]
TIMEBASE=1/1000
START=0000
END=132000
title=What are selections?
[CHAPTER]
TIMEBASE=1/1000
START=132000
END=199000
title=Booleans
[CHAPTER]
TIMEBASE=1/1000
START=199000
END=0000
title=
[CHAPTER]
TIMEBASE=1/1000
START=0000
END=4459000
title=

UPDATE #3 using his awk changes

;FFMETADATA1
[CHAPTER]
TIMEBASE=1/1000
START=0000
END=0000
title=Intro
[CHAPTER]
TIMEBASE=1/1000
START=0000
END=132000
title=What are selections?
[CHAPTER]
TIMEBASE=1/1000
START=132000
END=199000
title=Booleans
[CHAPTER]
TIMEBASE=1/1000
START=199000
END=0000
title=

Solution

  • The end time of each chapter is the start time of the next chapter. You thus have to know the start time for the next chapter (or, for the last chapter, the end of the file) before you can output it.

    Using the command from https://superuser.com/a/945604 to get the total duration, and Awk to perform the overall parsing,

    #!/bin/sh
    
    input_file="chapters.txt"
    output_file="chapters.ffmetadata"
    
    awk -F= -v duration=$(ffprobe -v error \
            -select_streams v:0 -show_entries stream=duration \
            -of default=noprint_wrappers=1:nokey=1 \
           input_video.mp4) '
      function chapter(start, end, title) {
        print "[CHAPTER]"
        print "TIMEBASE=1/1000"
        print "START=" start "000"
        print "END=" end "000"
        print "title=" title }
      BEGIN { print ";FFMETADATA1" }
      { split($1, hms, ":") ;
          end=3600 * hms[1] + 60 * hms[2] + hms[3] }
      title { chapter(start, end, title) }
      { start=end; title=$2 }
      END { chapter(start, duration, title) }' "$input_file" > "$output_file"
    
    ffmpeg -i input_video.mp4 -i "$output_file" -map_metadata 1 -c:v copy -c:a copy -y output_video.mp4
    

    Nothing here uses Bash features, so I used a /bin/sh shebang.

    Properly speaking, you probably want to make the input video and output video file names into command-line arguments.

    If you are unfamiliar with Awk, learning enough to write a script like this should not take long. A basic one-hour tutorial will already be quite sufficient, and pay back handsomely if you need to solve other problems like this.

    Demo of the Awk part: https://ideone.com/scXWNJ