Removing advertisements from recorded video streams

Hypothetically, let’s say I have some software that records streaming video from the interwebs. For the sake of this exercise I’m also pretending this software is called PlayOn.  Next, I’m imagining that the recorded streams also include advertisements. Continuing into our hypothetical journey I’m guessing I have a media center where I host the recorded media. 

Come on a journey with me and learn how to write a simple ffmpeg wrapper to automate removing advertisements from recorded video streams. **DISCLAIMER: this is for educational purposes only**

Initial Setup

In order for this to work we need a folder where we can dump all of the recorded videos.  While we could certainly leave them in the default folder where PlayOn puts them I prefer to have things in a flat structure when processing them.  

For sake of this article let’s say that my folder structure is going to look like this:

  • h:\PlayLater_Unprocessed – these are the raw video files
  • h:\PlayLater_Processed – this folder holds processed videos
  • h:\PlayLater_Processed\_done – this folder holds the original raw video files after processing

Getting Started

The first thing you need is to get hold of the static binaries for your respective operating system.  For me on Windows those can be located here.  Take note of where you download and unzip those.

Now that we have that out of the way, let’s go ahead and build a new .NET Core console application.  You can do that by running dotnet new console -n MyConsoleApplication.  Me personally, I prefer the GUI because I’m weird like that.

Ok great.  Now we have a console application.  One thing I like about .NET Core is that it comes with built-in support for dependency injection which lends itself quite nicely to unit testing among other things. 

Since this a topic all of it’s own I’m going to gloss over it and pretend we did it together.  If you don’t know how to do it please check out the finished code and/or ask me about it.  I’ll plan on writing another blog post about this in the future.

Application Entry

We waved our magic wand and have the Program.cs all set up now.  The relevant piece here is that we want to get and run the IFileProcessorService now.  This service is going to call into ffprobe and ffmpeg in order to perform all the relevant tasks.  Keep that in mind as we continue.

var provider = services.BuildServiceProvider();

var processingService = provider.GetService<IFileProcessorService>();
processingService.Run();

FFProbe – Find all the datars!

FFProbe is a powerful tool that will gathers information from multimedia and spits it out to something we (or our program) can read.  In our use case we want to get hold of the chapter listing and that’s about it.  

When we execute ffprobe we’re going to pass it the arguments string args = $"-v quiet -print_format json -show_format -show_streams -show_chapters -i \"{filePath}\"";.  For brevity sake we’re not actually make use of the format or streams right now.  Those were included for “debugging” purposes.  The only things we really care about is that it prints it out as JSON and gives us the chapters which is accomplished by -print_format json and -show_chapters.

FFMpeg – Make it so

FFMpeg is where all the real magic happens.  The key here is that we’re going to take the metadata we generated from ffprobe and then split our original file out into segments.  We will then re-stitch the segments back together excluding our advertisement segments.  Trickery!

As with the ffprobe process I’m just passing in the arguments we want rather than making the wrapper highly configurable. 

For splitting the files we’re passing the arguments $" -i \"{inputFilePath}\" -ss {startTime} -t {duration} -c copy {outputPath}".  I hope that the arguments here are mostly self-explanatory based on what I’m passing into them.  The one outlier here is the -c copy.  This instructs it to take both the audio and video stream codecs and copy them.  You can tell it to only do one or the other but for this project I don’t know why you would.

For rejoining our files it is slightly more complicated but can be made easier by generating a listing file first which is the route I opted to take.  I iterate all the segments I have and build a listing in a StringBuilder then I use the trusty old System.IO.File.WriteAllText to output the file.  This is only necessary if our process *actually* detected multiple segments.  If not, we just shortcut the process and simply copy the single segment over to our output folder.

Assuming we have a listing file then we’re passing the arguments $" -f concat -safe 0 -i {listPath} -c copy \"{targetPath}\"" along.  -f is to force the input/output file format.  -safe 0 is linked to the -c copy.  That flag instructs ffmpeg not to ensure my path is safe and is required if I’m using absolute paths (which I am).

Wiring it all together

In my simple use-case I have the IFileProcessorService run against an input folder and iterate all MP4 files it finds.  It then asks ffprobe to generate the metadata for it and extracts all the chapters marked as “video”.  It then iterates each of those chapters and splits them out to our temporary location. 

Next it takes the resulting segments (if many) and stitches them back together and copies it to our desired output folder.  If there is only a single segment it simply copies it to the desired folder.

Finally it takes the original file and copies it to our “done” folder just in case we somehow screwed up and need to try again.

Running a .NET Core console application isn’t as straightforward as you’d think.  Previous iterations of .NET would build the application as an executable; .NET Core does not.  Instead you’ll have to use the command-line and run dotnet MyConsoleApplication -c Release where -c Release is the configuration you are targeting.  You might also consider installing it as a .NET Global Tool as Rick Strahl details on his blog.

Conclusion

We now have a simple application that will automate removing advertisements from recorded video streams.

I’m preempting all the outrage at some of the practices in this application and say right off that it could (and should) be modified quite a bit to be cleaner, better separation of concerns, etc. 

Code for this article can be located at my GitHub.  Feel free to download, modify, etc!  And remember kids, this is only for educational purposes.  I’d never advocate actually doing this.

Credits

Photo by Denise Jans on Unsplash