Introduction
I am creating a library for my video files and want it to be capable of extracting as many information from a file as possible so the user (mainly me) can be as lazy adding new videos as possible. Information also means video snapshots so you can instantly see what video file it is. This article will be about taking snapshots from almost any video file.
Background
I am using C# at work - mainly C# 3.5 CF - and to my shame I do not have much experience with other programming languages. Coming with working on the Compact Framework and different mobile devices comes a resignation, that if you want to do something the Windows OS on a device does not offer natively, you end up improvising a lot. Luckily that skill let me reach my goal for the video library - and it is highly improvised.
I tried to use ActiveX and its COM interface in C#. I managed to grab frames at specified positions after editing the COM interface - I do not exactly remember where but I had to replace a byte parameter with a IntPtr one. The disappointment came when I tried other video formats than my standard test video (AVI DivX MP3), e.g. a MP4 container with a H.264 video and AAC audio codec or a simple FLV video. The MediaDet class could not handle these types although I had the correct codecs installed. I did some research and found out that there seems to be an interface missing in these codecs that is used by ActiveX.
My second approach was to use one of the many FFmpeg wrappers which wrap the FFmpeg DLLs directly into C#. But they did not want to work for me. Some did have some functionality but seeking (one of the most important methods to grab snapshots) did not work without decoding the whole video up to this point which of course took too long.
I played a bit with the ffmpeg comand-line utilities and found that they actually did exactly what I need - just having to use files is a down.
Getting media information
First I want to explain how to use the command-line arguments to grab media information and snapshots.
The ffprobe.exe offers a command-line output of the video properties, using the following arguments:
-hide_banner | Hides the banner at the beginning of the command-line output |
|
-show_format | Outputs general information about the video file |
|
-show_streams | Outputs information about every stream in the video file |
|
-pretty | Formats the output in a MS INI format with [/...] end tags |
|
{file} | The input file - has to be at the end |
So the command-line should look like this:
ffprobe.exe -hide_banner -show_format -show_streams -pretty {video_file}
To read the command-line output with C# a process has to be started with a redirected output. So I wrote this helper method to execute a command and return its output after the process has terminated:
private static string Execute(string exePath, string parameters)
{
string result = String.Empty;
using (Process p = new Process())
{
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = exePath;
p.StartInfo.Arguments = parameters;
p.Start();
p.WaitForExit();
result = p.StandardOutput.ReadToEnd();
}
return result;
}
The output looks like this example:
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'c:\file.mp4':
Metadata:
major_brand : isom
minor_version : 1
compatible_brands: isomavc1
creation_time : 2013-05-05 07:16:05
Duration: 01:06:09.07, start: 0.000000, bitrate: 887 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt470bg), 720x576 [SAR 64:45 DAR 16:9], 706 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default)
Metadata:
creation_time : 2013-05-05 07:16:05
Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 176 kb/s (default)
Metadata:
creation_time : 2013-05-05 07:16:07
[STREAM]
index=0
codec_name=h264
codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
profile=High
codec_type=video
codec_time_base=1/50
codec_tag_string=avc1
codec_tag=0x31637661
width=720
height=576
duration=1:06:08.760000
bit_rate=706.941000 Kbit/s
[/STREAM]
[STREAM]
index=1
codec_name=aac
codec_long_name=AAC (Advanced Audio Coding)
codec_type=audio
codec_time_base=1/48000
codec_tag_string=mp4a
codec_tag=0x6134706d
duration=1:06:09.066667
bit_rate=176.062000 Kbit/s
[/STREAM]
[FORMAT]
filename=c:\file.mp4
nb_streams=2
nb_programs=0
format_name=mov,mp4,m4a,3gp,3g2,mj2
format_long_name=QuickTime / MOV
duration=1:06:09.066667
size=419.768014 Mibyte
bit_rate=887.178000 Kbit/s
[/FORMAT]
So I only have to parse these information. In the attached class this will be done in the constructor.
Taking a snapshot
The most important aspect of the ffmpeg.exe syntax is that the arguments used always apply to the next mentioned file (input or output) - so you first state what options and then the file to use. I am going to use these options:
-hide_banner | Hides the banner at the beginning of the command-line output |
|
-ss {hh:mm:ss.fff} | Jumps to the specified position in the video - if this is defined before the input the input video is seeked, if defined before the output the input is decoded up to the position |
|
-i {file} | Defines the input file |
|
-r {n} | Sets the forced frame rate |
|
-t {n} | Sets the length of frames to output |
|
-f {format} | Sets the forced format to use for input or output - I am using 'image2' to get a JPEG output |
|
{file} | The output file - has to be at the end |
So the command-line called should be something like:
ffmpeg.exe -hide_banner -ss {timespan} -i {video_file} -r 1 -t 1 -f image2 {temp_file}
To supress a command-line console being shown while the snapshot is taken - and depending on the video file and the computer's performance this can take up to a few seconds - I am using the same method as above to execute the command. C# offers a method to directly get a temporary file name so there is almost nothing unordinary here:
public Bitmap GetSnapshot(TimeSpan atPosition, string filename)
{
if (filename.Contains(' '))
filename = "\"" + filename + "\"";
string tmpFileName = Path.GetTempFileName();
if (tmpFileName.Contains(' '))
tmpFileName = "\"" + tmpFileName + "\"";
string cmdParams = String.Format("-hide_banner -ss {0} -i {1} -r 1 -t 1 -f image2 {2}",
atPosition, filename, tmpFileName);
Bitmap result = null;
try
{
Execute(FFMPEG_EXE_PATH, cmdParams);
if (File.Exists(tmpFileName))
{
byte[] fileData = File.ReadAllBytes(tmpFileName);
result = new Bitmap(new MemoryStream(fileData));
File.Delete(tmpFileName);
}
}
catch { }
return result;
}
The tricky part is to load the saved bitmap into C#: If you create the image using new Bitmap(tmpFileName)
, the file is locked until the Bitmap is disposed so the tmpFileName cannot be deleted. So I am reading all bytes first and initialize the Bitmap using a MemoryStream.
Using the code
I wrapped these methods with a few other helper methods into the attached class. You can simply use it by using something like this:
FFmpegMediaInfo info = new FFmpegMediaInfo("C:\file.mp4");
double length = info.Duration.TotalSeconds;
double step = length / 10;
double pos = 0.0;
Dictionary<TimeSpan, Bitmap> snapshots = new Dictionary<TimeSpan,Bitmap>();
while (pos < length)
{
TimeSpan position = TimeSpan.FromSeconds(pos);
Bitmap bmp = info.GetSnapshot(position);
snapshots[position] = bmp;
pos += step;
}
This example opens the file C:\file.mp4 - the video information is automatically loaded in the constructor so the duration is known. Then there is a snapshot taken every tenth of the videos duration and stored in a Dictionary with the TimeStamp as the key.
History
Changes in Version 1.2:
- Added descrition comments to Properties
- Added more comments to code
- Added a try-catch-wrapper around Int32 and Int64 parsing
- Using Split() instead of IndexOf() and Substring() for output line parsing
- Added an example of ffprobe.exe output data