Openur.FFMpegCore 6.0.0.57

dotnet add package Openur.FFMpegCore --version 6.0.0.57
                    
NuGet\Install-Package Openur.FFMpegCore -Version 6.0.0.57
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Openur.FFMpegCore" Version="6.0.0.57" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Openur.FFMpegCore" Version="6.0.0.57" />
                    
Directory.Packages.props
<PackageReference Include="Openur.FFMpegCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Openur.FFMpegCore --version 6.0.0.57
                    
#r "nuget: Openur.FFMpegCore, 6.0.0.57"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Openur.FFMpegCore@6.0.0.57
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Openur.FFMpegCore&version=6.0.0.57
                    
Install as a Cake Addin
#tool nuget:?package=Openur.FFMpegCore&version=6.0.0.57
                    
Install as a Cake Tool

FFMpegCore

NuGet Version GitHub issues GitHub stars GitHub codecov CI GitHub code contributors

A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Supports both synchronous and asynchronous calls. It runs the ffmpeg and ffprobe binaries, so they need to be installed or downloaded at runtime; see Binaries.

Upgrading from 5.x? Version 6.0 renames most option methods after the ffmpeg options they emit, splits input from output options, and renames or removes several FFMpeg.* helpers. MIGRATION.md lists every breaking change and its replacement, and the new helpers and builders that came with it.

API

FFProbe

Use FFProbe to analyze media files:

var mediaInfo = await FFProbe.AnalyseAsync(inputPath);

or

var mediaInfo = FFProbe.Analyse(inputPath);
Console.WriteLine($"{mediaInfo.Duration}, {mediaInfo.PrimaryVideoStream?.Width}x{mediaInfo.PrimaryVideoStream?.Height}");

Analyse also takes a Uri or a Stream. The primary stream of each kind is the one marked default, or the first when none is. FFProbe.GetFrames and GetPackets return ffprobe's per-frame and per-packet data.

mediaInfo.Json is ffprobe's own output, for keeping or sending elsewhere, and FFProbe.FromJson(json) turns it back into an analysis without running ffprobe again — so one machine can probe and another work from the result.

A missing input throws FileNotFoundException, as it does when building an ffmpeg command, and a non-zero exit throws FFProbeProcessException, which carries ExitCode and the captured stderr lines in StandardError. It derives from FFMpegException, so a single catch (FFMpegException) covers ffprobe's and ffmpeg's failures alike.

FFMpeg

Build the ffmpeg command with the fluent argument builder. This converts a file to h264/aac at 720p, with the index at the front for web playback:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath, options => options
        .WithVideoCodec(VideoCodec.LibX264)
        .WithConstantRateFactor(21)
        .WithAudioCodec(AudioCodec.Aac)
        .WithVideoFilters(filterOptions => filterOptions
            .Scale(VideoSize.Hd))
        .WithFastStart())
    .ProcessSynchronously();

Scale(VideoSize.Hd) sets the height and lets ffmpeg pick a width that keeps the aspect ratio, rounded to an even number — scale=-2:720. Most video encoders reject odd dimensions for the usual yuv420p pixel format, so when you pass a size yourself, use -2 rather than -1 for the side ffmpeg should compute.

Inputs and outputs can be streams as well as files (see Input piping):

await FFMpegArguments
    .FromPipeInput(new StreamPipeSource(inputStream))
    .OutputToPipe(new StreamPipeSink(outputStream), options => options
        .WithVideoCodec(VideoCodec.LibVpxVp9)
        .ForceFormat(ContainerFormats.WebM))
    .ProcessAsynchronously();

Input options and output options

Options given to an input land before that input's -i, and options given to an output land before the output path — which is what ffmpeg requires, and what decides their meaning. The two sides therefore have their own types, FFMpegInputOptions and FFMpegOutputOptions, each offering only what ffmpeg accepts there: WithVideoDecoder selects a decoder on an input, while WithVideoCodec selects an encoder on an output, and -y or -map are output-only. Options that are valid on both sides, such as -ss and -t, appear on both.

Seeking on the input is fast, because ffmpeg skips ahead before decoding:

FFMpegArguments
    .FromFileInput(inputPath, options => options
        .WithStartTime(TimeSpan.FromSeconds(10))
        .WithDuration(TimeSpan.FromSeconds(30)))
    .OutputToFile(outputPath, options => options
        .CopyStreams())
    .ProcessSynchronously();

The options lambda goes straight after the path. File inputs check that the file exists before ffmpeg starts, and file outputs overwrite an existing file; to change either, use the overload with the flag before the lambda, and name it so the call says what it does:

FFMpegArguments
    .FromFileInput(inputPath, verifyExists: false, options => options.ForceFormat("mpegts"))
    .OutputToFile(outputPath, overwrite: false, options => options.CopyStreams())
    .ProcessSynchronously();

FromFileInputs(paths) and AddFileInputs(paths) add one input per path, with the options repeated before each — to join files into one, see Concat. WithLoop() repeats a still image for as long as the output runs, and WithStreamLoop(n) plays any input n more times (-1 for ever).

Looking up options

Each option method's summary names the ffmpeg option it emits, so searching your IDE for -ss finds WithStartTime, and each filter method's summary names its ffmpeg filter (LoudnessNormalizer is loudnorm). That name is also what to look up in ffmpeg's own documentation:

  • ffmpeg: the command-line options, stream specifiers and -map
  • Filters: everything WithVideoFilters, WithAudioFilters and WithComplexFilter build
  • Codecs: encoder and decoder options, such as libx264's -preset and -tune
  • Formats: muxer and demuxer options, such as -movflags or the hls muxer's -hls_time
  • Protocols: the URLs FromUrlInput and OutputToUrl accept

ffmpeg.org documents the current development version. Your own build lists what it accepts: ffmpeg -h filter=scale, ffmpeg -h encoder=libx264, ffmpeg -h muxer=hls.

When the builder has no method for an option, WithCustomArgument("-flag value") on the input or output options passes it straight through, and WithArgument takes your own IArgument. On the filter builders, WithCustomFilter(key, value) does the same for a filter.

Selecting streams

WithMap mirrors ffmpeg's own -map input:specifier — the input index first, then what to take from it. Leaving the stream index out maps every stream of that kind, which is what you want when the input's stream count is not known up front:

FFMpegArguments
    .FromFileInput(inputPath)
    .AddFileInput(audioPath)
    .OutputToFile(outputPath, options => options
        .WithMap(0, StreamType.Video)              // -map 0:v  — every video stream of the first input
        .WithMap(1, StreamType.Audio)              // -map 1:a  — every audio stream of the second
        .WithNegativeMap(0, StreamType.Subtitle)   // -map -0:s — but none of its subtitles
        .CopyStreams())
    .ProcessSynchronously();

WithMap(0) maps everything from the first input, and WithMap(0, StreamType.All, 3) picks a single stream by index — -map 0:3. WithMap(string) and WithNegativeMap(string) select a label a complex-filter chain produced instead.

The codec, bitrate and copy options take a stream index too, for settings that apply to one stream only. Like ffmpeg's -c:a:1, the index counts within the stream type:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath, options => options
        .WithMap(0)
        .CopyStreams(StreamType.Video)       // -c:v copy
        .CopyStreams(StreamType.Audio, 0)    // -c:a:0 copy — keep the first audio track as it is
        .WithAudioCodec(AudioCodec.Aac, 1)   // -c:a:1 aac  — re-encode the second
        .WithAudioBitrate(128, 1))           // -b:a:1 128k
    .ProcessSynchronously();

Encoder options

The options for the encode are named after the ffmpeg options they emit. Where ffmpeg takes one of a set of values, the parameter is a small type listing the common ones — EncoderPreset, EncoderTune, VideoProfile, FpsMode, MovFlags, HardwareAccelerationDevice and others in FFMpegCore.Enums — which also accepts any string, so an encoder's own values pass straight through:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile("output.mp4", options => options
        .WithVideoCodec(VideoCodec.LibX265)
        .WithConstantRateFactor(26)
        .WithSpeedPreset(EncoderPreset.Slow)    // or "p5" for NVENC, "8" for SVT-AV1
        .WithCodecTag("hvc1", StreamType.Video) // -tag:v hvc1 — so Apple's players open HEVC in mp4
        .WithFpsMode(FpsMode.ConstantFrameRate)
        .WithFrameRate("30000/1001")            // exact NTSC, where 29.97 is an approximation
        .WithFastStart())                       // -movflags faststart
    .ProcessSynchronously();

WithMovFlags(MovFlags.FragmentKeyframe + MovFlags.EmptyMoov) writes a fragmented mp4 for streaming. Every WithMovFlags and WithFastStart on an output combines into one -movflags, since ffmpeg keeps only the last.

Metadata tags and dispositions

WithMetadata sets a tag on the output, and WithStreamMetadata sets one on its streams — every stream of a type, or one of them by index. WithDisposition marks streams as the default, forced, and so on:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath, options => options
        .WithMap(0)
        .CopyStreams()
        .WithMetadata("title", "Interview")                                    // -metadata "title=Interview"
        .WithStreamMetadata("language", "dan", StreamType.Audio, 0)            // -metadata:s:a:0 "language=dan"
        .WithDisposition(StreamDisposition.Default + StreamDisposition.Forced,
            StreamType.Subtitle, 0))                                           // -disposition:s:0 default+forced
    .ProcessSynchronously();

Complex filters

WithVideoFilters builds the single -vf chain that one input feeds. When a filter needs more than one input, or you want to route what a filter produces into another, use WithComplexFilter, which builds -filter_complex:

FFMpegArguments
    .FromFileInput(inputPath)
    .AddFileInput(logoPath)
    .OutputToFile(outputPath, options => options
        .WithComplexFilter(graph => graph
            .From(0, StreamType.Video)
            .From(1, StreamType.Video)
            .Overlay(x: "W-w-10", y: "H-h-10")
            .As("v"))
        .WithMap("v"))
    .ProcessSynchronously();

A chain reads its inputs with From — either a stream of an input file, or a label an earlier chain produced — applies filters in order, and closes with As, naming what it produced. As returns the graph, so the next From starts another chain; chains are joined with ;. WithMap(label) then selects a labelled output:

.WithComplexFilter(graph => graph
    .From(0, StreamType.Video).Video(f => f.Scale(640, -2)).As("small")
    .From("small").Video(f => f.HorizontalFlip()).As("flipped"))
.WithMap("flipped")

The chain has methods only for the filters ffmpeg accepts nowhere else — Concat, Overlay and AudioMix, all of which need more than one input and are rejected in -vf; Concat and AudioMix count the chain's inputs unless you pass a count. Every other filter goes in through Video and Audio, which take the same builders as WithVideoFilters and WithAudioFilters, so Scale, Fade and the rest are spelled the same in both places. WithFilter takes a filter argument object directly, and WithCustomFilter(key, value) covers anything the library has no method for. Both are on the -vf and -af builders as well, so a filter without a method joins the built-in ones rather than replacing the whole chain:

.WithVideoFilters(f => f.WithCustomFilter("yadif").Scale(VideoSize.Hd))

Reading the result

ProcessSynchronously() and ProcessAsynchronously() return an FFMpegResult describing the run:

var result = FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath)
    .ProcessSynchronously(throwOnError: false);

if (!result.Success)
{
    Console.Error.WriteLine($"ffmpeg exited with {result.ExitCode}");
    Console.Error.WriteLine(string.Join("\n", result.StandardError));
}

By default (throwOnError: true) a non-zero exit throws FFMpegProcessException, whose Result is the same FFMpegResult, and a cancellation throws OperationCanceledException. Pass false and the result reports what happened instead, through ExitCode, StandardError, Cancelled and Success.

try
{
    FFMpeg.Remux(inputPath, outputPath).ProcessSynchronously();
}
catch (FFMpegProcessException exception)
{
    logger.LogError("ffmpeg exited with {ExitCode}: {Output}", exception.Result.ExitCode, exception.Result.StandardError);
}

Problems found before ffmpeg starts throw the usual .NET exceptions: FileNotFoundException for a missing input, IOException for an existing output when overwrite: false.

A processor can be run again once its previous run has finished, but not twice at the same time: the runs would share its cancellation state, its pipes and its temporary files. To run the same command concurrently, build it from scratch per run. Processors branched from one FFMpegArguments (see Multiple outputs) share its input arguments, so run those one after another too.

Progress and cancellation

NotifyOnProgress reports the timestamp ffmpeg has reached. NotifyOnPercentageProgress reports a percentage from 0 to 100, which needs the output duration — pass it explicitly, or omit it after any FFMpeg.* helper except SaveStream, which reads a live stream with no known length. Both take an Action<T> or an IProgress<T>:

await FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath)
    .NotifyOnProgress(time => Console.WriteLine($"at {time}"))
    .NotifyOnPercentageProgress(percent => Console.WriteLine($"{percent}%"), mediaInfo.Duration)
    .ProcessAsynchronously(cancellationToken: cancellationToken);

Cancelling sends q to ffmpeg, so it finalises the output — an mp4 gets its index written and stays playable — and kills the process if it has not exited within five seconds. ProcessSynchronously and ProcessAsynchronously take the token for a single run. CancellableThrough attaches one to the processor instead, for every run it makes, and is where to choose a different grace period, or take an out Action to cancel by calling it:

var processor = FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath)
    .CancellableThrough(shutdownToken, TimeSpan.FromSeconds(30))
    .CancellableThrough(out var cancel);

Pass TimeSpan.Zero to kill ffmpeg straight away, at the cost of whatever it had not yet written.

Multiple outputs

OutputToMany writes several outputs from one input using ffmpeg's own repeated outputs, which encodes once per output:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToMany(outputs => outputs
        .OutputToFile("sd.mp4", options => options.WithVideoFilters(f => f.Scale(1280, 720)))
        .OutputToFile("hd.mp4", options => options.WithVideoFilters(f => f.Scale(1920, 1080))))
    .ProcessSynchronously();

To make them separate ffmpeg runs instead, call OutputTo* more than once on the same inputs. Each call returns its own processor and leaves the inputs unchanged:

var inputs = FFMpegArguments.FromFileInput(inputPath);
await inputs.OutputToFile("preview.mp4", options => options.WithDuration(TimeSpan.FromSeconds(10))).ProcessAsynchronously();
await inputs.OutputToFile("full.mp4").ProcessAsynchronously();

OutputToTee instead encodes once and fans the result out through ffmpeg's tee muxer, which is cheaper but requires every target to accept the same encoded streams:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToTee(outputs => outputs
        .OutputToFile("recording.mp4")
        .OutputToUrl("rtmp://example.com/live/key", options => options.ForceFormat(ContainerFormats.Flv)),
        options => options.CopyStreams())
    .ProcessSynchronously();

Encoder options go on the tee itself, since there is only one encode. A target takes what the tee muxer lets each target choose — ForceFormat, WithSelect for which of the encoded streams it gets, WithBitstreamFilter, WithMovFlags/WithFastStart, WithOnFail(TeeOnFail.Ignore) to keep the others running when one fails, and WithMuxerOption for anything else its muxer accepts.

Analysing without writing an output

Detection filters such as SilenceDetect, VolumeDetect and BlackDetect report what they find on stderr rather than in an output file. OutputToNull decodes the input through them and throws the result away (-f null -); the findings are in the result's StandardError, or arrive line by line through NotifyOnStandardError:

var result = FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToNull(options => options
        .DisableVideo()
        .WithAudioFilters(filters => filters.SilenceDetect(noise: -50, duration: 1)))
    .ProcessSynchronously();

var silences = result.StandardError.Where(line => line.Contains("silence_start") || line.Contains("silence_end"));

The filters log at ffmpeg's info level, so a quieter LogLevel drops their findings.

Metadata

FFMetadataBuilder builds the ffmetadata document that AddMetadata passes to ffmpeg:

var metadata = new FFMetadataBuilder()
    .WithTitle("Interview")
    .WithArtists("Some Artist")
    .WithChapter("Introduction", TimeSpan.FromMinutes(2))
    .WithChapter("Main topic", TimeSpan.FromMinutes(25));

FFMpegArguments
    .FromFileInput(inputPath)
    .AddMetadata(metadata)
    .OutputToFile(outputPath, options => options.CopyStreams())
    .ProcessSynchronously();

A chapter given a single TimeSpan is a duration, and starts where the previous one ended; the overload taking two starts and ends it explicitly. Chapters are ChapterData, the same type IMediaAnalysis.Chapters returns, so chapters read from one file can be fed straight into another. Call Build() if you want the document text itself.

AddMetadata adds the document as an input and maps its metadata onto every output. An ffmetadata file you already have goes in through AddMetadataFile(path), which is mapped the same way. To take the metadata from another input instead, or drop it, say so on the output — WithMapMetadata(inputFileIndex) and WithoutMetadata() replace the automatic mapping:

FFMpegArguments
    .FromFileInput(videoPath)
    .AddFileInput(audiobookPath)
    .OutputToFile(outputPath, options => options
        .WithMapMetadata(1)   // -map_metadata 1 — keep the audiobook's tags
        .CopyStreams())
    .ProcessSynchronously();

Helper methods

The provided helper methods make it simple to perform common operations. Each one builds the ffmpeg arguments and returns an FFMpegArgumentProcessor, so you choose how to run it — ProcessSynchronously() or await ProcessAsynchronously() — and can attach progress callbacks or cancellation exactly as with FFMpegArguments.

Helpers and ffprobe

Most helpers need to know something about their input before they can build the arguments — its duration, its resolution, whether it has an audio stream — so they run ffprobe while building. That probe is synchronous, which means await FFMpeg.Remux(path, out).ProcessAsynchronously() blocks the calling thread for the probe before it reaches the await.

Every one of those helpers also has an overload taking an IMediaAnalysis instead of an input path, so you can do the probing yourself and keep the whole thing asynchronous. The analysis carries the path it was made from, so it replaces the path rather than accompanying it:

var source = await FFProbe.AnalyseAsync(inputPath, cancellationToken: cancellationToken);

await FFMpeg.Remux(source, "output.mkv")
    .ProcessAsynchronously(cancellationToken: cancellationToken);

For the helpers taking several inputs this also lets the probes run concurrently, where the path overloads probe one after another:

var sources = await Task.WhenAll(parts.Select(part => FFProbe.AnalyseAsync(part, cancellationToken: cancellationToken)));

await FFMpeg.Concat("joined.mp4", sources)
    .ProcessAsynchronously(cancellationToken: cancellationToken);

It is also worth using whenever you have already probed the input to decide what to do — passing the analysis in saves a second probe of the same file. An analysis made from a Stream has no path, so these overloads reject it; use the path overloads there.

Capture a snapshot from a video

// persist the image on the drive
FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1))
    .ProcessSynchronously();

// or asynchronously, with cancellation
await FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1))
    .ProcessAsynchronously(cancellationToken: cancellationToken);

// or process the snapshot in-memory using one of the image extension packages
var bitmap = SystemDrawingImage.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); // FFMpegCore.Extensions.System.Drawing.Common
var skBitmap = SkiaSharpImage.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));   // FFMpegCore.Extensions.SkiaSharp

Capture a GIF from a video

FFMpeg.GifSnapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromSeconds(10))
    .ProcessSynchronously();

// pass -1 for the width or the height to have it follow the aspect ratio
await FFMpeg.GifSnapshot(inputPath, outputPath, new Size(480, -1), TimeSpan.FromSeconds(10))
    .ProcessAsynchronously();

Join video parts into one file

When the parts already share a codec — segments of one recording, say — Concat joins them through ffmpeg's concat demuxer without re-encoding, which is both lossless and far faster:

FFMpeg.Concat(@"..\joined_video.mp4",
    @"..\part1.mp4",
    @"..\part2.mp4",
    @"..\part3.mp4"
).ProcessSynchronously();

Join re-encodes through the concat filter instead, which is what you need when the parts differ. They must still share a resolution:

FFMpeg.Join(@"..\joined_video.mp4",
    @"..\part1.mp4",
    @"..\part2.mkv"
).ProcessSynchronously();

// by default ffmpeg picks the container's encoder at its default quality; pass output options to choose your own
FFMpeg.Join(@"..\joined_video.mp4", parts, options => options
    .WithVideoCodec(VideoCodec.LibX265)
    .WithConstantRateFactor(23)
).ProcessSynchronously();

Overlay a watermark

FFMpeg.Watermark(inputPath, "logo.png", outputPath,
    WatermarkPosition.BottomRight, margin: 20
).ProcessSynchronously();

A PNG with an alpha channel keeps its transparency. The audio is copied across untouched; the video is re-encoded, since the picture is what changes, with ffmpeg's default encoder for the container. Pass output options to choose the encode instead — addArguments: options => options.WithVideoCodec(VideoCodec.LibX264).WithConstantRateFactor(18) — and add CopyStreams(StreamType.Audio) to them if the audio should still be copied. For anything more elaborate — scaling the logo first, fading it in — build the graph yourself with WithComplexFilter.

Add or extract subtitles

// mux the subtitles in as their own stream, so the viewer can switch them off
FFMpeg.AddSubtitles(inputPath, "subs.srt", "output.mkv", language: "eng").ProcessSynchronously();

// mp4 only carries subtitles as mov_text
FFMpeg.AddSubtitles(inputPath, "subs.srt", "output.mp4", "eng", SubtitleCodec.MovText).ProcessSynchronously();

// and back out again
FFMpeg.ExtractSubtitles("output.mkv", "subs.srt").ProcessSynchronously();

ExtractSubtitles takes the first subtitle stream unless you pass a streamIndex — the stream's Index as FFProbe reports it — and throws ArgumentException when the input has no subtitles to extract.

To burn the subtitles into the picture instead, so they cannot be switched off, use the subtitles filter:

FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath, options => options
        .WithVideoFilters(filters => filters
            .BurnSubtitles("subs.srt")))
    .ProcessSynchronously();

Build a contact sheet of thumbnails

// 5x5 frames spread evenly across the whole video
FFMpeg.ThumbnailSheet(inputPath, "sheet.png").ProcessSynchronously();

// or one frame every 10 seconds, 160px wide, height following the aspect ratio
FFMpeg.ThumbnailSheet(inputPath, "sheet.jpg",
    columns: 10, rows: 8,
    interval: TimeSpan.FromSeconds(10),
    tileSize: new Size(160, -1)
).ProcessSynchronously();

This is the sprite sheet a player loads to show previews while scrubbing. Leave interval out and the frames are spread across the input's duration instead.

Change container without re-encoding

FFMpeg.Remux(@"..\input.mp4", @"..\output.mkv").ProcessSynchronously();

Every stream is copied across, so this is lossless and near-instant. The target container has to be able to mux the streams as they are — h264/aac into .mkv or .mov is fine, into .webm is not, and ffmpeg fails the run rather than silently re-encoding.

Cut a section out of a video

FFMpeg.Trim(inputPath,
    outputPath,
    TimeSpan.FromSeconds(0),
    TimeSpan.FromSeconds(30)
).ProcessSynchronously();

Trim copies every stream rather than re-encoding, so the output container has to be able to mux them as they are — cutting an h264/aac mp4 into a .mkv is fine, into a .webm is not, and ffmpeg says so. Without a re-encode the video can only start on a keyframe, so the cut begins at the keyframe at or before startTime and may run a few seconds longer than asked. For a frame-exact cut, re-encode with FFMpegArguments and WithStartTime/WithStopTime on the input.

Join images into a video

FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
    @"..\1.png",
    @"..\2.png",
    @"..\3.png"
).ProcessSynchronously();

// output options replace the default yuv420p, as with Join
FFMpeg.JoinImageSequence(@"..\joined_video.mp4", images, frameRate: 1, options => options
    .WithVideoCodec(VideoCodec.LibX264)
    .WithConstantRateFactor(18)
    .WithPixelFormat("yuv420p")
).ProcessSynchronously();

Remove the audio track of a video

FFMpeg.RemoveAudio(inputPath, outputPath).ProcessSynchronously();

Extract the audio track from a video

// the output extension picks the container, and ffmpeg picks the encoder for it
FFMpeg.ExtractAudio(inputPath, "track.mp3").ProcessSynchronously();
FFMpeg.ExtractAudio(inputPath, "track.flac").ProcessSynchronously();

// or lift the stream out untouched, with no re-encode
FFMpeg.ExtractAudio(inputPath, "track.m4a", AudioCodec.Copy).ProcessSynchronously();

// an encoder with no constant can be named directly
FFMpeg.ExtractAudio(inputPath, "track.opus", "libopus").ProcessSynchronously();

Record a remote stream to a file

await FFMpeg.SaveStream(new Uri("https://example.com/live/stream.m3u8"), "recording.ts")
    .ProcessAsynchronously(cancellationToken: cancellationToken);

Any protocol ffmpeg can open works — http(s), rtmp, rtsp, srt. The streams are copied, not re-encoded. Prefer .ts or .mkv over .mp4 for anything long-running: an mp4 is only finalised when the run ends, so a crash loses the recording, while cancelling finalises it properly.

Add or replace the audio track of a video

FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath).ProcessSynchronously();

// re-encode the new track, for audio the output container cannot carry as it is
FFMpeg.ReplaceAudio(inputPath, "voiceover.wav", outputPath, AudioCodec.Aac).ProcessSynchronously();

The output takes the video of the first file and the audio of the second, whatever audio the video already had. Both are copied unless you pass a codec.

Combine an image with an audio file, for YouTube and similar platforms

FFMpeg.PosterWithAudio(inputImagePath, inputAudioPath, outputPath).ProcessSynchronously();

// or from a bitmap in memory, using one of the image extension packages
using var image = SKBitmap.Decode(inputImagePath);
await image.AddAudio(inputAudioPath, outputPath).ProcessAsynchronously(cancellationToken: cancellationToken);

AddAudio returns the processor like the FFMpeg.* helpers. The bitmap is written to a temporary file when the run starts and deleted when it ends, so keep it alive until then.

The audio is copied, not re-encoded, so the track is not degraded a second time on its way to a platform that will transcode it anyway. Pass a codec if you do need to re-encode, and output options to change the video encode from libx264 at CRF 21 in yuv420p:

FFMpeg.PosterWithAudio(inputImagePath, inputAudioPath, outputPath, AudioCodec.Aac).ProcessSynchronously();

FFMpeg.PosterWithAudio(inputImagePath, inputAudioPath, outputPath, addArguments: options => options
    .WithVideoCodec(VideoCodec.LibX264)
    .WithTune(EncoderTune.StillImage)
    .WithPixelFormat("yuv420p")
).ProcessSynchronously();

Input piping

Input piping feeds ffmpeg from memory instead of a file, so frames can be encoded while they are being generated or received, without writing them to disk first. The data comes from an IPipeSource: StreamPipeSource for a stream, RawVideoPipeSource for raw video frames, and RawAudioPipeSource for raw audio samples. Output piping works the same way in reverse, through an IPipeSink such as StreamPipeSink.

A pipe can't be seeked, so an input format that needs seeking — an mp4 with its index at the end, for one — fails through a pipe where it works from a file.

On Linux and macOS the pipe is a Unix domain socket, which ffmpeg before 8.0 treats as seekable. Demuxers that seek when they can then lose data without an error — a piped WAV is missing its first 64 KiB of audio — so pipe input, and FFProbe.Analyse(Stream), need ffmpeg 8.0 or newer there. With an older ffmpeg, write the input to a file first. Windows uses a named pipe and is not affected.

Working with raw video frames

Generate the frames:

IEnumerable<IVideoFrame> CreateFrames(int count)
{
    for(int i = 0; i < count; i++)
    {
        yield return GetNextFrame(); // generates or receives the next frame
    }
}

Then hand them to a RawVideoPipeSource:

var videoFramesSource = new RawVideoPipeSource(CreateFrames(64))
{
    FrameRate = 30 // the frames' rate
};
await FFMpegArguments
    .FromPipeInput(videoFramesSource)
    .OutputToFile(outputPath, options => options
        .WithVideoCodec(VideoCodec.LibVpx))
    .ProcessAsynchronously();

The image extension packages provide SystemDrawingVideoFrame and SkiaSharpVideoFrame, which adapt a System.Drawing.Bitmap or an SKBitmap to IVideoFrame.

Working with raw audio samples

RawAudioPipeSource takes the samples' rate and channel count, since nothing in raw samples says what they are; the sample format defaults to signed 16-bit little-endian (s16le):

var audioSource = new RawAudioPipeSource(samples, sampleRate: 48000, channels: 2);
await FFMpegArguments
    .FromPipeInput(audioSource)
    .OutputToFile("output.m4a", options => options
        .WithAudioCodec(AudioCodec.Aac))
    .ProcessAsynchronously();

Binaries

Runtime Auto Installation

The FFMpegCore.Extensions.Downloader package can install ffmpeg and ffprobe at runtime into the configured BinaryFolder. Set one first — with none, ffmpeg is looked up on PATH, so the download would never be used, and DownloadBinariesAsync throws. The folder is created if it does not exist yet:

GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
await FFMpegDownloader.DownloadBinariesAsync();

This feature uses the api from ffbinaries.

Manual Installation

If you prefer to manually download them, visit ffbinaries or the official ffmpeg download page.

Windows (using choco)

command: choco install ffmpeg -y

location: C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin

macOS

command: brew install ffmpeg

location: /opt/homebrew/bin (Apple Silicon) or /usr/local/bin (Intel)

Ubuntu

command: sudo apt-get install -y ffmpeg

location: /usr/bin

Distribution packages are often older than ffmpeg 8.0 (Ubuntu 24.04 ships 6.1), which matters for piped input on Linux.

Path Configuration

In code

By default BinaryFolder is empty and ffmpeg is found through PATH. Set it, globally or per run, through FFOptions:

// setting global options
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });

// or
GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");

// or individual, per-run options
await FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath)
    .ProcessAsynchronously(ffOptions: new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });

// the FFMpeg.* helpers take the same options, covering the ffprobe call they make before the run
await FFMpeg.RemoveAudio(inputPath, outputPath, ffOptions: new FFOptions { BinaryFolder = "./bin" })
    .ProcessAsynchronously();

// or combined, setting global defaults and adapting per-run options
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "./globalTmp", WorkingDirectory = "./" });

await FFMpegArguments
    .FromFileInput(inputPath)
    .OutputToFile(outputPath)
    .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir")
    .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder")
    .ProcessAsynchronously();

In ffmpeg.config.json

An ffmpeg.config.json in the working directory sets the global options instead. It is read once, on first use:

{
  "BinaryFolder": "./bin",
  "TemporaryFilesFolder": "/tmp"
}

Supporting both 32 and 64 bit processes

To ship binaries for both architectures, put each set in an x64 or x86 folder inside BinaryFolder. The library tries {BinaryFolder}/{x64|x86}/ffmpeg for the architecture of the running process first, then {BinaryFolder}/ffmpeg, and the same for ffprobe (.exe is appended on Windows).

Compatibility

CI runs the test suite against ffmpeg 8.1. Older versions might not support every option the library offers, and piped input on Linux and macOS needs 8.0 or newer.

Code contributors

<a href="https://github.com/rosenbjerg/ffmpegcore/graphs/contributors"> <img src="https://contrib.rocks/image?repo=rosenbjerg/ffmpegcore" /> </a>

License

Released under the MIT license, which carries the copyright notice.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on Openur.FFMpegCore:

Repository Stars
Sonarr/Sonarr
Smart PVR for newsgroup and bittorrent users.
Version Downloads Last Updated
6.0.0.57 0 11/7/2026
5.5.0.50 236 10/24/2026
5.5.0.45 6,189 9/20/2026
5.4.0.37 24,513 5/27/2026
5.4.0.31 36,696 12/3/2025