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
}
The output may be as rich a 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.