Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / video

Converting Video with FFmpegCore

5.00/5 (3 votes)
23 Feb 2021CPOL2 min read 10.6K  
A simple snippet of how you can convert video in .NET Core
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:

F#
system.componentmodel.win32exception: the system cannot find the file specified

which is fixed with the help of static class FFMpegOptions:

F#
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.

F#
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.

F#
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.

F#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)