Combining video clips

If you have lots of short video clips it might be convenient to combine them to one longer video file. Concatenate video clips with FFmpeg can be done loss-less and it is super easy with the help of Fast video cataloger and some scripting.

This tutorial will show you have to write a script to for example combine a list of video clips from your phone into a longer clip, or perhaps combining short clips you have downloaded from the internet.

Fast video cataloger gives you a good overview of all your videos regardless of how long they are.

Why not just use a video editor

You normally edit videos using a video editing software, like the free Blender.

Tip: There is a little known video editor that comes with Windows 10. To launch it simply right-click a video and select open with “photo”. This editor allows lots of simple video editing operations like trimming a video or adding text.

If you only want to combine lots of small clips to larger ones this is way more complicated than it needs to be. Worse, if you use a normal video editing software it is most likely going to recompress the video to save it out once you are finished editing. This not only takes time, but it will also reduce the quality of the video.

If the videos are the exact same dimension and have the exact same compression you can probably concatenate them without recompression and quality loss.

Using FFMpeg

FFmpeg has this function build-in with its concat command, expplained in detail here.

With c# scripting in Fast video cataloger, we can create a simple script that takes your selected videos and concatenates them. If the selected files are similar we generate the needed FFmpeg command line and temporary files and launches the program. They are are not the same format we give an error message.

Extended video properties

This sample is hopefully doing something useful. But, it also shows how you can get and uses extended properties from videos in Fast video cataloger.

For this example to work we need the extended properties for video format, this is added by default in newer versions of Fast video cataloger if you have the right checkbox set in preferences (the default is on).

Extract meta data

Setting for extracting meta data during the video indexing process.

Extended video properties

Extended video properties are displayed in the video properties window

We write the script so that if the properties are missing we just accept the video and let FFmpeg present the error message instead when there are issues.

The script

I have organized the whole script as a class so you can continue to build on it or bring it into other scripts easy. However, everything is really done in the Concat() member function to make it easy to follow.

Entry function

As always all scripts start in the run function

static public void Run(IScripting scripting, string argument)

Here we just do a simple check that more than one video is selected. Then create the Concat class and call the Concat function on the newly created Merger object.

The concat class constructor simply takes the root script interface and saves it as a member.

The Concat() member function, as said before, is where we do everything.

FFmpeg

The first thing we do is getting the path to the FFmpeg command-line tool. If you have not already downloaded it you can do that from here: https://ffmpeg.org/ and make sure it is installed in c:\\ffmpeg\\bin\\ffmpeg.exe (or update the script to the path where it is installed).

Temporary file for listing the video files.

Next, we need to create a temporary file. The temporary file will be the list of videos to concatenate that will be passed to FFmpeg.

string tmp_file_path = System.IO.Path.GetTempFileName();

Will generate a unique temporary filename in your “tmp” folder. (NOTE that if you have too many files in your tmp folder creating files there will fail. This is actually a pretty common cause for random Windows problems. I.e we need to make sure we delete the file when we are done)

var stream_writer = System.IO.File.CreateText(tmp_file_path);

Will create a text with the temp file name in your temp folder.

Checking if videos are compatible

Next, we will check if the format is similar for all videos selected. This is done by iterating over all videos and getting all extended properties for that video

var extended = catalog.GetVideoFileExtendedProperty((int)video_id);
foreach (var prop in extended )
{
   // here is where we do the checking
}

and the check for video format looks like this. (In a real-world solution you would write a shared check function taking arguments on what to check but for this short example I think it would be harder to follow, so copy-paste it is )

if (prop.Property == "video_Format")
{
  if (video_format == null)
  {
    video_format = prop.Value;
    m_scripting.GetConsole().WriteLine( video_path + " - Video format is "  + video_format );
  }
  else if (video_format != prop.Value)
  {
    string msg = "Aborting. All videos must be in the same video format.\n'";
    msg += video_path + "' is in " + prop.Value + "\n";
    msg += " previous was in " + video_format + "\n";
    m_scripting.GetConsole().WriteLine( msg);
    can_do_pure_concat = false;
  }
}

The time we see the property we check we store it, the next time we compare it to what was stored. If they are not the same we print an error message and flag the that the list of videos is incompatible can_do_pure_concat = false. If you want to extend on this script you could use this flag and generate a different command line for re-encoding the videos.

Picking output video path

If we know the videos are compatible it is time to present a save dialog.


Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
dlg.FileName = "concat" + extension;
dlg.DefaultExt = extension;
dlg.Filter = "All files|*.*";
Nullable result = dlg.ShowDialog();

Since we know the files are of the same format we suggest the file extension to be the same as the first video in the list.

Running FFMpeg

Now we have the list of source files, we know they are compatible and we know where we want the output file. It is time to call Fmpeg


System.Diagnostics.Process process = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.FileName = "cmd.exe";
string cmd_line = " -f concat -safe 0 -i " + tmp_file_path + conversion_parameters + " -c copy \"" + out_file + "\"";
startInfo.Arguments = "/C " + tool_path + " " + cmd_line;
process.StartInfo = startInfo;
process.Start();
process.WaitForExit();

The /C switch to the windows command-line will close the window once the script has run to its end. If you instead use /K the window will remain on screen. That might be a good idea if you want to read error messages from FFMpeg.

Cleanup and playing the new video

Afte the command has run we should have a video. We need to clear up the temp file

File.Delete(tmp_file_path);

and finally, we use the windows shell to show the new video outside of Fast video cataloger.

System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo()
{
  FileName = out_file,
  UseShellExecute = true,
  Verb = "open"
});

Running the script

To run the script load it into the script window in Fast video cataloger. Select the videos you want to concatenate and then click run. The program will check the file and present you with a save dialog to save the merged video and then play it with the default video player.

Here is the full script:

using System.Collections.Generic;
using VideoCataloger;
using System.IO;
using System;

public class ConcatVideos
{
    static public void Run(IScripting scripting, string argument)
    {
        scripting.GetConsole().Clear();
        ISelection selection = scripting.GetSelection();
        List selected = selection.GetSelectedVideos();
        if (selected.Count == 1)
        {
            scripting.GetConsole().WriteLine("Select more than one video");
            return;
        }

        ConcatVideos merger = new ConcatVideos(scripting);
        merger.Concat(null,null);
    }

    IScripting m_scripting = null;

    ConcatVideos(IScripting scripting)
    {
        m_scripting = scripting;
    }

    public string GetFFMPEGPath()
    {
        string tool_path = "c:\\ffmpeg\\bin\\ffmpeg.exe";
        if (!File.Exists(tool_path))
        {
            System.Windows.MessageBox.Show(tool_path, "ffmpeg missing");
        }
        return tool_path;
    }


    private void Concat( string out_file, string conversion_parameters )
    {
        string tool_path = GetFFMPEGPath();
        string tmp_file_path = System.IO.Path.GetTempFileName();

        var stream_writer = System.IO.File.CreateText(tmp_file_path);
        if (stream_writer == null)
        {
            System.Windows.MessageBox.Show(tool_path, "Failed to write temporary text file");
            return;
        }

        ISelection selection = m_scripting.GetSelection();
        IUtilities utilities = m_scripting.GetUtilities();
        var catalog = m_scripting.GetVideoCatalogService();
        string extension = null;
        string video_format = null;
        string video_width = null;
        string video_height = null;
        string audio_format = null;
        List selected = selection.GetSelectedVideos();
        bool can_do_pure_concat = true;
        foreach (long video_id in selected)
        {
            var entry = catalog.GetVideoFileEntry(video_id);
            string video_path = utilities.ConvertToLocalPath(entry.FilePath);

            var extended = catalog.GetVideoFileExtendedProperty((int)video_id);
            foreach (var prop in extended )
            {
                if (prop.Property == "video_Format")
                {
                    if (video_format == null)
                    {
                        video_format = prop.Value;
                        m_scripting.GetConsole().WriteLine( video_path + " - format "  + video_format );
                    }
                    else if (video_format != prop.Value)
                    {
                        string msg = "Aborting. All videos must be in the same video format.\n'";
                        msg += video_path + "' is in " + prop.Value + "\n";
                        msg += " previous was in " + video_format + "\n";
                        m_scripting.GetConsole().WriteLine( msg);
                        can_do_pure_concat = false;
                    }
                }
                else if (prop.Property == "video_Width")
                {
                    if (video_width == null)
                    {
                        video_width = prop.Value;
                        m_scripting.GetConsole().WriteLine(video_path + " - width " + video_width);
                    }
                    else if (video_width != prop.Value)
                    {
                        string msg = "Aborting. All videos must be in the same dimension.\n'";
                        msg += video_path + "' width is " + prop.Value + "\n";
                        msg += " previous was in " + video_width + "\n";
                        m_scripting.GetConsole().WriteLine(msg);
                        can_do_pure_concat = false;
                    }
                }
                if (prop.Property == "video_Height")
                {
                    if (video_height == null)
                    {
                        video_height = prop.Value;
                        m_scripting.GetConsole().WriteLine(video_path + " - height " + video_height);
                    }
                    else if (video_height != prop.Value)
                    {
                        string msg = "Aborting. All videos must be in the same dimension.\n'";
                        msg += video_path + "' height is " + prop.Value + "\n";
                        msg += " previous was in " + video_height + "\n";
                        m_scripting.GetConsole().WriteLine(msg);
                        can_do_pure_concat = false;
                    }
                }
                if (prop.Property == "audio_Format")
                {
                    if (audio_format == null)
                    {
                        audio_format = prop.Value;
                        m_scripting.GetConsole().WriteLine(video_path + " - Audio " + audio_format);
                    }
                    else if (audio_format != prop.Value)
                    {
                        string msg = "Aborting. All videos must be in the same audio format.\n";
                        msg += video_path + "is in " + prop.Value + "\n";
                        msg += " previous was in " + audio_format + "\n";
                        m_scripting.GetConsole().WriteLine(msg);
                        can_do_pure_concat = false;
                    }
                }
            }
            stream_writer.Write("file '" + video_path + "'" );
            stream_writer.WriteLine();

            if (!can_do_pure_concat)
                return;
            if (extension==null)
            {
                int extension_start = video_path.LastIndexOf(".");
                extension = video_path.Substring(extension_start);
            }
        }
        stream_writer.Close();

        if (out_file == null)
        {
            Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
            dlg.FileName = "concat" + extension;
            dlg.DefaultExt = extension;
            dlg.Filter = "All files|*.*";
            Nullable result = dlg.ShowDialog();
            if (result == false)
            {
                return;
            }
            out_file = dlg.FileName;
        }

        System.Diagnostics.Process process = new System.Diagnostics.Process();
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
        startInfo.FileName = "cmd.exe";
        string cmd_line = " -f concat -safe 0 -i " + tmp_file_path + conversion_parameters + " -c copy \"" + out_file + "\"";
        startInfo.Arguments = "/C " + tool_path + " " + cmd_line; // use /K instead of /C to keep the cmd window up
        process.StartInfo = startInfo;

        m_scripting.GetConsole().WriteLine("Running " + startInfo.Arguments);

        process.Start();
        process.WaitForExit();

        File.Delete(tmp_file_path);

        System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo()
        {
            FileName = out_file,
            UseShellExecute = true,
            Verb = "open"
        });
       
    }
}