Add keywords from video filename

Have you ever wanted to create keywords based on the filename of a video?

With the scripting support in Fast video cataloger, this is actually pretty easy. Here is a sample script to do just that, create keywords from the filename or path of a video.

The provided script takes the currently selected videos and adds keywords and actors based on the name of the video file.

You might want to tweak the script a bit to suit your own naming standard.

The array of separators list the characters that should be treated as keyword separators.

The array ignore_words is a list of common words that should simply be ignored and not added as keywords.

The integer min_length is the number of characters that a word needs to contains to be added as a keyword.

Finally, if a string contains the @ character that sentence will be treated as an actor. First name before the @ and last name after the @. The actors are created and added as cast to the video.

As usual, to run a script in Fast video cataloger you load it into the console window and click the Run button.

Script console and web browser open.

 

#region title_to_keywords


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

/// <summary>
///  Take the title and use it to generate keywords.  
///  If there is a @ in the title we treat that as an actor
///  the text before @ is first name and the text after is last name
///  if an actor with that name already exist we use that one.
/// </summary>
class KeywordsFromTitle
{
    static public void Run(IScripting scripting, string arguments)
    { 
        scripting.GetConsole().Clear();
        var catalog = scripting.GetVideoCatalogService();
        ISelection selection = scripting.GetSelection();
        List<long> selected = selection.GetSelectedVideos();
        foreach (long video in selected)
        {
            // Get the video file entry
            var entry = catalog.GetVideoFileEntry(video);
            scripting.GetConsole().WriteLine(System.Convert.ToString("Processing..." + entry.FilePath));

            char[] separators = { ' ', ',', '.', '-', '[' ,']', '{', '}', '_' };
            string[] ignore_words = { "is", "are", "who", "where" };
            string title = entry.Title;
            string[] keywords = title.Split(separators);
            int min_length = 3;
            foreach (string word in keywords)
            {
                if (word.Length>= min_length)
                {
                    if (!ignore_words.Contains(word))
                    {
                        if (word.Contains("@"))
                        {
                            // Actor
                            string[] names = word.Split('@');
                            string first_name = names[0];
                            string last_name = names[1];

                            scripting.GetConsole().WriteLine( "Actor FirstName:"+ first_name + " LastName:" + last_name );

                            int actor_id = -1;
                            VideoCataloger.RemoteCatalogService.Actor[] current_actors = catalog.GetActors(null, first_name, last_name, true);
                            if (current_actors.Length >= 1)
                            {
                                actor_id = current_actors[0].ID;
                            }
                            else
                            {
                                VideoCataloger.RemoteCatalogService.Actor actor = new VideoCataloger.RemoteCatalogService.Actor();
                                actor.FirstName = first_name;
                                actor.LastName = last_name;
                                actor_id = catalog.AddActorToDB(actor);
                            }

                            if (actor_id != -1)
                                catalog.AddActorToVideo(video, actor_id);
                        }
                        else
                        {
                            // Keywords
                            scripting.GetConsole().WriteLine("Keyword:" + word );
                            scripting.GetVideoCatalogService().TagVideo(video, word);
                        }
                    }
                }
            }
        }

        // refresh the gui to show the changed file paths.
        scripting.GetGUI().Refresh("");
    } 
}
#endregion

The Architecture of Fast Video Cataloger

Introduction

Here is a very high overview of the technology we have used to build Fast Video Cataloger. The goal has always been to provide a video content management system for Windows with great performance.

Main application

The main application is written in C# using Visual Studio and the .NET framework. We use a lot of async/await for threading as wait as background workers and explicit threading. C# is generally pretty efficient, and we have access to most of the windows system using the .Net framework. We have had quite a bit of performance issues with garbage collection and quite a few tricks to work around these performance issues.

User interface

The user interface in Fast video cataloger is done in WPF and a few extra UI components. WPF uses DirectX internally, and DirectX uses graphics hardware for rendering. We have made quite a few optimizations to handle huge video collections with good performance.

Scripting

The scripting interface uses CSScript and we expose the API through some abstract interface classes as well as the direct WCF interfaces. You can extend Fast Video Cataloger with C# either by loading the scripts in the console windows or by creating actions that link to C# scripts.

Encryption

Video encryption uses Aes encryption and we hook the filesystem to be able to work on a stream level.

Web Browser

WPF has a web browser component but that uses internet explorer. The browser window is a hosted Chromium web browser, CEF sharp.

Video player

WPF has a video player component but that one has pretty bad performance and does not support all video formats. The video player in Fast video cataloger is custom for FVC and is written in C++ and uses a DirectShow filter graph to play videos. C# allows us pretty easy access to interface with c components. We build the graph manually to avoid the common issues where a system has badly configured filter priorities (Select the player from preferences). We support a few different renderers that you can select from the preferences.

Video indexer

The video indexer is custom for FVC and is written in C++. We use a custom DirectShow filter graph with a few custom DirectShow filters for the capture. The video indexer subsystem is something we have developed for over 10 years. It contains quite a lot of “special” code to handle errors and all sorts of broken files and codecs.

Video database

The video database engine is SQLite and we have built a custom WCF interface. Locally the WCF server is run as a separate process and the main application communicates through a memory pipe or TCP/IP.

Server

When running the Fast video cataloger server the WCF component is hosted as a Windows service and communicates with the game over TCP.

Conclusion

You can download a trial version of Fast video cataloger

How to combine video clips using ffmpeg and scripting

Combining video clips

Merging lots of short video clips into one longer video file can be a great idea. But if you’ve tried to manually merge videos in the past, you may know how difficult this can be. Not any longer!

Concatenating video clips with FFmpeg can be done loss-less, and it is super easy thanks to 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.

But if your plan is to combine lots of small clips into a larger one, this is far more complicated than it needs to be. Worse, if you use normal video editing software, it is most likely going to recompress the video once you have 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.

And it’s not as complicated as it may sound.

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).

Extracting meta data from video

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 will 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 and merge the video.


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”
});

}
}

c# for all the really advanced video searches

Extending the video search

The basic idea I show you here is how to write a script in C# that determines if a video matches your search criteria or not and put the matching videos in a custom Bin.

You will learn by a simple step by step example how to write a script that goes through all videos in you catalog and puts all short videos(the critera we have as an example) in a bin.

After you have this skill you can expand your skill and continue to experiment.

About the native video search

Fast video cataloger has a very powerful, fast and simple search engine. The text below show how easy you can extend it even more with advanced custom video search in C#.

Our simple user interface make searching in Fast video cataloger straight forward for everyone. However, we do not provide a SQL interface for search so there are searches you can simply not express given our user interface. With the scripting interface you can do basically anything.

AI For example… What if you want to search with facial recognition or object detection or the next big thing in AI and video?

You an do all this and more by leveraging the scripting capabilities in Fast video cataloger

step by step breakdown on the Video search script in c#

Run(…) in the code below, is the entry function where the execution of the script starts.

    static public void Run(IScripting scripting, string argument)
    {
        FilterToBin instance = new FilterToBin(scripting);
        instance.SetBinTarget("short videos");
        instance.Filter();
        scripting.GetGUI().Refresh("");
    }

I have encapsulated all functionality in a class called “FilterToBin” that has two functions.

SetBinTarget() let you set the name of the Bin that will be used for search results. Please note that this Bin will be cleared every time you run the search.

Filter() does the actual filtering of videos.

    private void Filter()
    {
        var catalog = m_Scripting.GetVideoCatalogService();

        VideoQuery query = new VideoQuery();
        VideoFileEntry[] videos = catalog.SearchVideos(query);

        long bin_id = CreateBin();

        foreach (VideoFileEntry entry in videos)
        {
            if (IsVideoPassingFilter(entry))
                catalog.AddVideoToBin(entry.ID, bin_id);
        }
    }

The Filter() function first does a search to get all videos. In this example we pass in en empty VideoQuery object. That will give us all videos in the catalog. You can of course leverage the search here and first do a high level search to get a subset of your videos and then apply a more advanced search on that subset.

We then call CreateBin() to ensure we have a bin to put our results in. If we look closer in the FilterToBin class the CreateBin member function has some logic to create a Bin if its not already created and clear it from videos if it already exists.

Finally we iterate over all videos and call the IsVideoPassingFilter() function on each video. Videos that pass the test are put in the result Bin.

    private bool IsVideoPassingFilter( VideoFileEntry entry )
    {
        if ( entry.LengthSeconds < 60)
            return true;
        return false;
    }

If you want to change the filter criteria this is the only function you need to edit. In this example we only check if the video length property is smaller than 60s.

To run this script you copy paste it or type it (or load it) into the script window and then click the Run button.

Go the Bin window and select the "short videos" to see the result (if the bin is not there go into the edit bin to refresh the window, this is needed on older versions of Fast video cataloger).

Edit button in bin window

Edit button in bin window

Full source code in C#

using VideoCataloger;
using VideoCataloger.RemoteCatalogService;

public class FilterToBin
{
    IScripting m_Scripting;
    string m_BinLabel;

    FilterToBin(IScripting scripting)
    {
        m_Scripting = scripting;
    }

    private void SetBinTarget(string bin_label)
    {
        m_BinLabel = bin_label;
    }

    private void Filter()
    {
        var catalog = m_Scripting.GetVideoCatalogService();

        VideoQuery query = new VideoQuery();
        VideoFileEntry[] videos = catalog.SearchVideos(query);

        long bin_id = CreateBin();

        foreach (VideoFileEntry entry in videos)
        {
            if (IsVideoPassingFilter(entry))
                catalog.AddVideoToBin(entry.ID, bin_id);
        }
    }

    private bool IsVideoPassingFilter( VideoFileEntry entry )
    {
        if ( entry.LengthSeconds < 60)
            return true;
        return false;
    }

    private long CreateBin()
    {
        var catalog = m_Scripting.GetVideoCatalogService();
        Bin[] all_bins = catalog.GetAllBins();
        foreach (Bin bin in all_bins)
        {
            if (bin.Label == m_BinLabel)
            {
                VideoFileEntry[] videos = catalog.GetVideosInBin(bin.BinID);
                foreach (VideoFileEntry entry in videos)
                {
                    catalog.RemoveVideoFromBin( bin.BinID, entry.ID );
                }

                return bin.BinID;
            }
        }

        Bin target_bin = catalog.CreateBin(m_BinLabel, -1, 0xffffff);
        return target_bin.BinID;
    }

    static public void Run(IScripting scripting, string argument)
    {
        FilterToBin instance = new FilterToBin(scripting);
        instance.SetBinTarget("short videos");
        instance.Filter();
        scripting.GetGUI().Refresh("");
    }
}