zigford.org

About | Links | Scripts
Sharing linux/windows scripts and tips

Video editing from the command line

October 05, 2019 — Jesse Harris

I previously posted about capturing video in Linux. While this isn't exactly part 2 of that post there is enough crossover to warrant a link. This post is about how I have strangely found video editing to be much easier from the command line than a gui app.


Firstly, there isn't a shortage of gui apps for editing video on Linux for very basic editing. My requirements are very simple. I need to be able to cut and join various video files, and product 2 output files. A file for hosting on the web and a file for archival purposes.

Problem 1 - Audio Sync

I started out myself with pitivi and tried a few others. My trouble with each editor was speed and accuracy (also my brain). On some videos I had to adjust the audio sync and was driven mad by moving audio tracks around. I had alot of trouble knowing if the audio was starting too early or too late and which way to move the audio to correct it.

With a gui editor, this involves click and dragging an audio track one way or another (for accuracy, you have to zoom in very far). Once zoomed in and you move the track, you then need to zoom out, place the play head, click play and then make a judgement if you made it better or worse.

For some reason, my brain could never tell exactly which way to move the audio until it was way too far in the wrong direction. This was a source of frustration that I initially solved using ffmpeg on the command line.

Using ffmpeg I wrote a very simple sh script to output a small time slice of the video many times with different audio offsets. Then it was simply a matter of watching each video and picking the right one. The resulting audio offset that was used for that video can then be used on the entire clip.

Firstly, start by creating a small snippet of video where you will clearly be able to see if the video is synced up.

        ffmpeg -i bigclip.mp4 -c copy -ss 00:01:55 -to 00:02:00 Small.mp4

Now we will use the following script

        #!/bin/sh

        # fixaudiodelay.sh

        Offset="${2}"
        inputFile="${1}"
        outputFile="${inputFile%%.*}-delayed${Offset}.mp4"
        out="${3:-$outputFile}"
        ffmpeg -i "${inputFile}" -itsoffset "${Offset}" \
            -i "${inputFile}" -map 0:v -map 1:a -c copy "${out}"

The script will take parameter 1 is the clip, and parameter 2 is the audio offset. We can use seq to generate a list of offsets to try and xargs to run them.

        seq -2.0 .1 2.0 | xargs -n1 ./fixaudiodelay.sh Small.mp4

The result is 41 clips with .1 second offset difference. Play them all and find the best offset. Then when you could use ./fixaudiodelay.sh BigClip.mp4 -1.7 (or whatever was the best offset) to correct the entire file.

Problem 2 - Perfect splitting

The second problem I had with gui editors, is that many home videos are completely different scenes butted up against each other on the tape. When transferring these to a new modern format, we want our audience to be able to skip the scenes.

With the gui editors, again we have the problem of accuracy along with the inability to at once, save all the clips from the single file. Say you have 4 distinct clips in a file. How can you mark each clip as a separate file then press encode and walk away? You can't you would have to create 3 separate projects and encode each one separately.

Solving it with ffmpeg is easier than you might think. When I showed cutting a clip earlier, you can use the -ss parameter to declare where to start from in the clip, and the -to parameter to declare the end. How I've solved it is like this.

  1. Create a track file which lists all the clips start, end and titles. I chose to separate each clip by a line, and each parameter by a - dash. Eg

         00:00:00-00:10:05-SnowTrip
         00:10:05.6-00:13:02-PlayingAtThePark
    
  2. I came up with the start and end positions, simply by marking down the times while watching the clip using mpv or vlc. However, the proof is in the pudding, so I use ffmpeg with some parameters to produce test video files. These won't be good enough quality to upload to the web, but are sufficient for verifying the time stamps and are very quick to encode and see the results.

    Note You can't use the copy codec when cutting clips. As the keyframes are not recreated

         while IFS=- read START END TITLE 
         do
             OUTFILE="${TITLE}-Test.mp4"
             ffmpeg -nostdin -i BigClip.mp4 -c:v libx264 \
                 -c:a aac -ss $START -to $END -preset ultrafast \
                 $OUTFILE
         done < tracks.txt
    
  3. After viewing the resulting files, I can make minute adjustments to the track file so the clips are perfect. Then I adjust for a better file for the web by changing the preset to veryslow and adding the following parameters

                 -crf 23 -movflags faststart
    
  4. Additionally in some cases, I add some video filters. I've found the following work well:

    Desired affect Parameter
    Increase volume -filter:a "volume=2.0"
    Deinterlace -vf "yadif"
    Archival Quality Replace libx264 with libx265

Problem 3 - Joining videos

The last problem I will discuss may only be a problem on Linux. While many of my video files are converted from VHS using a composite to usb converter, some are coming from DV tape. DV Tape stores it's data digitally in Mpeg-2 on a magnetic tape. On Linux, I've been able to transfer these files using FireWire and a command line tool dvgrab. The resultant content is many 1gb .dv files.

While these files can individually be processed by ffmpeg, it is more ideal to join them into a single clip for processing so that you can grab clips that span multiple files and use a single set of timestamps for processing. To adjust ffmpeg for this use, we use another file which tells ffmpeg all the files to concatenate.

Use the following sh to generate a file that ffmpeg can understand:

        for i in *.dv; do echo "file '$i'" >> files.txt; done 

TIP Did you know bash has a shorthand non-posix for loop?

        for i in *.dv; { echo "file '$i' >> files.txt; }

NOTE The space after the { is needed

Now use the resultant files.txt to create a single test clip of the whole lot:

        ffmpeg -f concat -safe 0 -i files.txt \
            -c:v libx264 -c:a aac -crf 35 \
            -preset ultrafast testconcat.mp4

You can now use this file to create timestamps relative to the whole concatenated lot.

Problem 4 - LP vs SP

DV Camera's in the early 2000s started featuring a LP (Long Play) feature which allowed them to drop the sample rate on the audio (and maybe video?) significantly in order to fit more on the tape. This introduces a problem for ffmpeg.

If multiple clips on a single tape switch between LP and SP (Short Play or standard play) then ffmpeg would only see what it encountered when it first probed the file. The result could be video/audio playing either very fast (chipmunk voice) or very slow. The trick here is to slice the .dv file far enough in that ffmpeg's probe matches the clip of audio you begin to transcode. It is not enough to simple specify the start position using -ss. As ffmpeg will probe the start of the file, not the start specified by -ss.

So we come now to the next tool in the unix tool belt dd. We can use dd to copy a file but tell it to skip x bytes. Eg

        dd if=dvgrab-001.dv of=testdv.dv bs=1M skip=100

Using bs=1M and skip=100 will create a new file that will skip roughly 100 megabytes of video at the start. Depending on where you clip begins/ends, you will have to fiddle with the skip option and if you need more granular control, you could reduce the bs (byte size) down into the kilobyte range.

If the resulting output file starts at the clip you are wanting to produce, you could now replace the original dvgrab file from your tracks list file with the newly generated file.

Tags: ffmpeg, dd, cli