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
  1. let options = FFMpegOptions()  
  2. options.RootDirectory <- "path to your binaries"  
  3. 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. 
  1. async {  
  2.     let! videoInfo = FFProbe.AnalyseAsync fileName |> Async.AwaitTask  
  3.     return JsonSerializer.Serialize videoInfo  
  4. }  
The output may be as rich as below, 
  1. {  
  2.    "Path":"D:\\giphy.mp4",  
  3.    "Extension":".mp4",  
  4.    "Duration":{  
  5.       "Ticks":17200000,  
  6.       "Days":0,  
  7.       "Hours":0,  
  8.       "Milliseconds":720,  
  9.       "Minutes":0,  
  10.       "Seconds":1,  
  11.       "TotalDays":1.990740740740741E-05,  
  12.       "TotalHours":0.00047777777777777776,  
  13.       "TotalMilliseconds":1720,  
  14.       "TotalMinutes":0.028666666666666667,  
  15.       "TotalSeconds":1.72  
  16.    },  
  17.    "Format":{  
  18.       "Duration":{  
  19.          "Ticks":17200000,  
  20.          "Days":0,  
  21.          "Hours":0,  
  22.          "Milliseconds":720,  
  23.          "Minutes":0,  
  24.          "Seconds":1,  
  25.          "TotalDays":1.990740740740741E-05,  
  26.          "TotalHours":0.00047777777777777776,  
  27.          "TotalMilliseconds":1720,  
  28.          "TotalMinutes":0.028666666666666667,  
  29.          "TotalSeconds":1.72  
  30.       },  
  31.       "FormatName":"mov,mp4,m4a,3gp,3g2,mj2",  
  32.       "FormatLongName":"QuickTime / MOV",  
  33.       "StreamCount":1,  
  34.       "ProbeScore":100,  
  35.       "BitRate":458339,  
  36.       "Tags":{  
  37.          "major_brand":"isom",  
  38.          "minor_version":"512",  
  39.          "compatible_brands":"isomiso2avc1mp41",  
  40.          "encoder":"Lavf56.40.101"  
  41.       }  
  42.    },  
  43.    "PrimaryAudioStream":null,  
  44.    "PrimaryVideoStream":{  
  45.       "AvgFrameRate":25,  
  46.       "BitsPerRawSample":8,  
  47.       "DisplayAspectRatio":{  
  48.   
  49.       },  
  50.       "Profile":"Constrained Baseline",  
  51.       "Width":480,  
  52.       "Height":264,  
  53.       "FrameRate":25,  
  54.       "PixelFormat":"yuv420p",  
  55.       "Rotation":0,  
  56.       "Index":0,  
  57.       "CodecName":"h264",  
  58.       "CodecLongName":"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",  
  59.       "BitRate":453744,  
  60.       "Duration":{  
  61.          "Ticks":17200000,  
  62.          "Days":0,  
  63.          "Hours":0,  
  64.          "Milliseconds":720,  
  65.          "Minutes":0,  
  66.          "Seconds":1,  
  67.          "TotalDays":1.990740740740741E-05,  
  68.          "TotalHours":0.00047777777777777776,  
  69.          "TotalMilliseconds":1720,  
  70.          "TotalMinutes":0.028666666666666667,  
  71.          "TotalSeconds":1.72  
  72.       },  
  73.       "Language":"und",  
  74.       "Tags":{  
  75.          "language":"und",  
  76.          "handler_name":"VideoHandler",  
  77.          "vendor_id":"[0][0][0][0]"  
  78.       }  
  79.    },  
  80.    "VideoStreams":[  
  81.       {  
  82.          "AvgFrameRate":25,  
  83.          "BitsPerRawSample":8,  
  84.          "DisplayAspectRatio":{  
  85.   
  86.          },  
  87.          "Profile":"Constrained Baseline",  
  88.          "Width":480,  
  89.          "Height":264,  
  90.          "FrameRate":25,  
  91.          "PixelFormat":"yuv420p",  
  92.          "Rotation":0,  
  93.          "Index":0,  
  94.          "CodecName":"h264",  
  95.          "CodecLongName":"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",  
  96.          "BitRate":453744,  
  97.          "Duration":{  
  98.             "Ticks":17200000,  
  99.             "Days":0,  
  100.             "Hours":0,  
  101.             "Milliseconds":720,  
  102.             "Minutes":0,  
  103.             "Seconds":1,  
  104.             "TotalDays":1.990740740740741E-05,  
  105.             "TotalHours":0.00047777777777777776,  
  106.             "TotalMilliseconds":1720,  
  107.             "TotalMinutes":0.028666666666666667,  
  108.             "TotalSeconds":1.72  
  109.          },  
  110.          "Language":"und",  
  111.          "Tags":{  
  112.             "language":"und",  
  113.             "handler_name":"VideoHandler",  
  114.             "vendor_id":"[0][0][0][0]"  
  115.          }  
  116.       }  
  117.    ],  
  118.    "AudioStreams":[  
  119.   
  120.    ]  
  121. }  
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.
  1. async {  
  2.     let! _ =  
  3.         FFMpegArguments  
  4.             .FromFileInput(fileName)  
  5.             .OutputToFile(outputFileName,  
  6.                 true,  
  7.                 fun options -> options  
  8.                                 .WithVideoCodec(VideoCodec.LibX264)  
  9.                                 .WithAudioCodec(AudioCodec.Aac)  
  10.                                 .WithVariableBitrate(4)  
  11.                                 .Resize(newWidth, newHeight)  
  12.                                 |> ignore)  
  13.             .ProcessAsynchronously() |> Async.AwaitTask  
  14.     ()  
  15. }  
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
  1. let newWidth =  
  2.     if videoInfo.PrimaryVideoStream.Height % 2 = 0 then  
  3.         videoInfo.PrimaryVideoStream.Height  
  4.     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.