Converting video with FFmpegCore
Working with multimedia is terra incognita for most of the 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 running the tool you may face the error:
system.componentmodel.win32exception: the system cannot find the 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)
Querying video information
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
}
{
"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
()
}
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.