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
<PackageReference Include="Openur.FFMpegCore" Version="6.0.0.57" />
<PackageVersion Include="Openur.FFMpegCore" Version="6.0.0.57" />
<PackageReference Include="Openur.FFMpegCore" />
paket add Openur.FFMpegCore --version 6.0.0.57
#r "nuget: Openur.FFMpegCore, 6.0.0.57"
#:package Openur.FFMpegCore@6.0.0.57
#addin nuget:?package=Openur.FFMpegCore&version=6.0.0.57
#tool nuget:?package=Openur.FFMpegCore&version=6.0.0.57
FFMpegCore
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,WithAudioFiltersandWithComplexFilterbuild - Codecs: encoder and decoder options, such as libx264's
-presetand-tune - Formats: muxer and demuxer options, such as
-movflagsor the hls muxer's-hls_time - Protocols: the URLs
FromUrlInputandOutputToUrlaccept
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 | Versions 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. |
-
.NETStandard 2.0
- Instances (>= 3.1.0)
- System.Text.Json (>= 9.0.10)
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.
|