FFmpegCore is a convenient wrapper around FFMpeg/FFProbe which allows convenient work with video. In this article, we'll have a look at how one can utilize it for video conversion.
Introduction
Working with multimedia is terra incognita for most developers since it’s something that one rarely encounters while working with usual business applications. So when I was tasked to convert video for the project I’m currently working on, I was expecting to deal with some sort of old poorly maintained C++ library. So FFmpegCore was a pleasant surprise since it enables working with .NET Core which is an area of my expertise.
Examples in this article will be provided in F# which I’m a big fan of but they are pretty straightforward, so it should be no problem in translating them to C#.
Installing FFmpeg
While documentation reads that this core is “A .NET Standard FFMpeg/FFProbe wrapper”, this tells nothing about the fact that Ffmpeg
/fmprobe
should be installed on the machine where the application is running.
Furthermore, I think it is worth clarifying that FFMpeg is a cross-platform command-line tool that allows working with video.
While on Linux, once you do apt install
you’re good to go on Windows, there is a point of interest. Installing FFmpeg
on Windows is a matter of downloading the binaries and putting them in a folder you will but once you run the tool, you may face the error:
system.componentmodel.win32exception: the system cannot find the file specified
which is fixed with the help of static
class FFMpegOptions
:
let options = FFMpegOptions()
options.RootDirectory <- "path to your binaries"
FFMpegOptions.Configure(options)
In order to query information about the video, we use static FFProbe
which has both synchronous and asynchronous API for video analysis. Let’s stick with the async
version and serialize the output to examine the wealth of information that FFProbe
provides us.
async {
let! videoInfo = FFProbe.AnalyseAsync fileName |> Async.AwaitTask
return JsonSerializer.Serialize videoInfo
}
The output may be as rich as below:
{
"Path":"D:\\giphy.mp4",
"Extension":".mp4",
"Duration":{
"Ticks":17200000,
"Days":0,
"Hours":0,
"Milliseconds":720,
"Minutes":0,
"Seconds":1,
"TotalDays":1.990740740740741E-05,
"TotalHours":0.00047777777777777776,
"TotalMilliseconds":1720,
"TotalMinutes":0.028666666666666667,
"TotalSeconds":1.72
},
"Format":{
"Duration":{
"Ticks":17200000,
"Days":0,
"Hours":0,
"Milliseconds":720,
"Minutes":0,
"Seconds":1,
"TotalDays":1.990740740740741E-05,
"TotalHours":0.00047777777777777776,
"TotalMilliseconds":1720,
"TotalMinutes":0.028666666666666667,
"TotalSeconds":1.72
},
"FormatName":"mov,mp4,m4a,3gp,3g2,mj2",
"FormatLongName":"QuickTime / MOV",
"StreamCount":1,
"ProbeScore":100,
"BitRate":458339,
"Tags":{
"major_brand":"isom",
"minor_version":"512",
"compatible_brands":"isomiso2avc1mp41",
"encoder":"Lavf56.40.101"
}
},
"PrimaryAudioStream":null,
"PrimaryVideoStream":{
"AvgFrameRate":25,
"BitsPerRawSample":8,
"DisplayAspectRatio":{
},
"Profile":"Constrained Baseline",
"Width":480,
"Height":264,
"FrameRate":25,
"PixelFormat":"yuv420p",
"Rotation":0,
"Index":0,
"CodecName":"h264",
"CodecLongName":"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"BitRate":453744,
"Duration":{
"Ticks":17200000,
"Days":0,
"Hours":0,
"Milliseconds":720,
"Minutes":0,
"Seconds":1,
"TotalDays":1.990740740740741E-05,
"TotalHours":0.00047777777777777776,
"TotalMilliseconds":1720,
"TotalMinutes":0.028666666666666667,
"TotalSeconds":1.72
},
"Language":"und",
"Tags":{
"language":"und",
"handler_name":"VideoHandler",
"vendor_id":"[0][0][0][0]"
}
},
"VideoStreams":[
{
"AvgFrameRate":25,
"BitsPerRawSample":8,
"DisplayAspectRatio":{
},
"Profile":"Constrained Baseline",
"Width":480,
"Height":264,
"FrameRate":25,
"PixelFormat":"yuv420p",
"Rotation":0,
"Index":0,
"CodecName":"h264",
"CodecLongName":"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"BitRate":453744,
"Duration":{
"Ticks":17200000,
"Days":0,
"Hours":0,
"Milliseconds":720,
"Minutes":0,
"Seconds":1,
"TotalDays":1.990740740740741E-05,
"TotalHours":0.00047777777777777776,
"TotalMilliseconds":1720,
"TotalMinutes":0.028666666666666667,
"TotalSeconds":1.72
},
"Language":"und",
"Tags":{
"language":"und",
"handler_name":"VideoHandler",
"vendor_id":"[0][0][0][0]"
}
}
],
"AudioStreams":[
]
}
Converting Video
In order to convert video, one uses static
FFMpegArguments
class which enables some sort of static
builder pattern. Again, it exhibits both synchronous and asynchronous API and we’ll stick to the latter.
async {
let! _ =
FFMpegArguments
.FromFileInput(fileName)
.OutputToFile(outputFileName,
true,
fun options -> options
.WithVideoCodec(VideoCodec.LibX264)
.WithAudioCodec(AudioCodec.Aac)
.WithVariableBitrate(4)
.Resize(newWidth, newHeight)
|> ignore)
.ProcessAsynchronously() |> Async.AwaitTask
()
}
Upon some circumstances, FFMpeg
may return an error.
"ffmpeg version 2021-01-24-git-1775688292-full_build-www.gyan.dev Copyright (c) 2000-2021
the FFmpeg developers\n built with gcc 10.2.0 (Rev6, Built by MSYS2 project)\n
configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads
--disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2
--enable-gmp --enable-lzma --enable-libsnappy --enable-zlib --enable-libsrt --enable-libssh
--enable-libzmq --enable-avisynth --enable-libbluray --enable-libcaca --enable-sdl2
--enable-libdav1d --enable-libzvbi --enable-librav1e --enable-libsvtav1 --enable-libwebp
--enable-libx264 --enable-libx265 --enable-libxvid --enable-libaom --enable-libopenjpeg
--enable-libvpx --enable-libass --enable-frei0r --enable-libfreetype --enable-libfribidi
--enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm
--enable-cuvid --enable-ffnvcodec --enable-nvdec --enable-nvenc --enable-d3d11va
--enable-dxva2 --enable-libmfx --enable-libglslang --enable-vulkan --enable-opencl
--enable-libcdio --enable-libgme --enable-libmodplug --enable-libopenmpt
--enable-libopencore-amrwb --enable-libmp3lame --enable-libshine --enable-libtheora
--enable-libtwolame --enable-libvo-amrwbenc --enable-libilbc --enable-libgsm
--enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis
--enable-ladspa --enable-libbs2b --enable-libflite --enable-libmysofa --enable-librubberband
--enable-libsoxr --enable-chromaprint\n libavutil
56. 63.101 / 56. 63.101\n libavcodec 58.117.101 / 58.117.101\n libavformat
58. 65.101 / 58. 65.101\n libavdevice 58. 11.103 / 58. 11.103\n libavfilter
7. 96.100 / 7. 96.100\n libswscale 5. 8.100 / 5. 8.100\n libswresample
3. 8.100 / 3. 8.100\n libpostproc 55. 8.100 / 55.
8.100\nInput #0, mov,mp4,m4a,3gp,3g2,mj2, from 'D:\\giphy.mp4':\n Metadata:\n
major_brand : isom\n minor_version : 512\n compatible_brands: isomiso2avc1mp41\n
encoder : Lavf56.40.101\n Duration: 00:00:01.72, start: 0.000000,
bitrate: 458 kb/s\n Stream #0:0(und): Video: h264 (Constrained Baseline)
(avc1 / 0x31637661), yuv420p, 480x264 [SAR 1:1 DAR 20:11], 453 kb/s, 25 fps, 25 tbr,
12800 tbn, 50 tbc (default)\n Metadata:\n handler_name : VideoHandler\n
vendor_id : [0][0][0][0]\nCodec AVOption vbr (Variable bit rate mode)
specified for output file #0 (D:\\kek.mp4) has not been used for any stream.
The most likely reason is either wrong type (e.g. a video option with no video streams)
or that it is a private option of some encoder which was not actually used for any stream.
\nStream mapping:\n Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))\nPress [q] to stop,
[?] for help\n[libx264 @ 000001cd5ac43100] width not divisible by 2
(101x101)\nError initializing output stream 0:0 -- Error while opening encoder for
output stream #0:0 - maybe incorrect parameters such as bit_rate, rate, width or
height\nConversion failed!"
While stacktrace
is rather intimidating, the “width not divisible by 2” suggests that FFMpeg
has a thing for odd width
and height
. I use this simple hack to trick it and force it to convert my video.
let newWidth =
if videoInfo.PrimaryVideoStream.Height % 2 = 0 then
videoInfo.PrimaryVideoStream.Height
else videoInfo.PrimaryVideoStream.Height - 1
videoInfo
here is a result of FFProbe
work couple of paragraphs above.
FFmpegCore
is capable of much more, i.e., capturing screenshots, changing video thumbnail, etc. but I’ll leave it to research for a curious reader.
History
- 23rd February, 2021 - Initial version submitted