mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-14 02:04:14 +01:00
Merge pull request #59 from max619/feature/variables_from_ffmpeg
Added Codec, PixelFormat and ContainerFormat classes
This commit is contained in:
commit
22daf6eb27
22 changed files with 746 additions and 133 deletions
|
@ -169,8 +169,8 @@ public void Builder_BuildString_CpuSpeed()
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Builder_BuildString_ForceFormat()
|
public void Builder_BuildString_ForceFormat()
|
||||||
{
|
{
|
||||||
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").ForceFormat(VideoCodec.LibX264).OutputToFile("output.mp4").Arguments;
|
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").ForceFormat(VideoType.Mp4).OutputToFile("output.mp4").Arguments;
|
||||||
Assert.AreEqual("-i \"input.mp4\" -f libx264 \"output.mp4\"", str);
|
Assert.AreEqual("-i \"input.mp4\" -f mp4 \"output.mp4\"", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
@ -278,13 +278,13 @@ public void Builder_BuildString_Threads_2()
|
||||||
public void Builder_BuildString_Codec()
|
public void Builder_BuildString_Codec()
|
||||||
{
|
{
|
||||||
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithVideoCodec(VideoCodec.LibX264).OutputToFile("output.mp4").Arguments;
|
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithVideoCodec(VideoCodec.LibX264).OutputToFile("output.mp4").Arguments;
|
||||||
Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\"", str);
|
Assert.AreEqual("-i \"input.mp4\" -c:v libx264 \"output.mp4\"", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Builder_BuildString_Codec_Override()
|
public void Builder_BuildString_Codec_Override()
|
||||||
{
|
{
|
||||||
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithVideoCodec(VideoCodec.LibX264).OutputToFile("output.mp4", true).Arguments;
|
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p").OutputToFile("output.mp4", true).Arguments;
|
||||||
Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str);
|
Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,5 +305,13 @@ public void Builder_BuildString_Raw()
|
||||||
str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithCustomArgument("-acodec copy").OutputToFile("output.mp4").Arguments;
|
str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithCustomArgument("-acodec copy").OutputToFile("output.mp4").Arguments;
|
||||||
Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str);
|
Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_ForcePixelFormat()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments.FromInputFiles(true, "input.mp4").ForcePixelFormat("yuv444p").OutputToFile("output.mp4").Arguments;
|
||||||
|
Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
44
FFMpegCore.Test/PixelFormatTests.cs
Normal file
44
FFMpegCore.Test/PixelFormatTests.cs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
using FFMpegCore.Exceptions;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Test
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class PixelFormatTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void PixelFormats_Enumerate()
|
||||||
|
{
|
||||||
|
var formats = FFMpeg.GetPixelFormats();
|
||||||
|
Assert.IsTrue(formats.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PixelFormats_TryGetExisting()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(FFMpeg.TryGetPixelFormat("yuv420p", out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PixelFormats_TryGetNotExisting()
|
||||||
|
{
|
||||||
|
Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PixelFormats_GetExisting()
|
||||||
|
{
|
||||||
|
var fmt = FFMpeg.GetPixelFormat("yuv420p");
|
||||||
|
Assert.IsTrue(fmt.Components == 3 && fmt.BitsPerPixel == 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PixelFormats_GetNotExisting()
|
||||||
|
{
|
||||||
|
Assert.ThrowsException<FFMpegException>(() => FFMpeg.GetPixelFormat("yuv420pppUnknown"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,25 +25,25 @@ public static class VideoLibrary
|
||||||
public static readonly FileInfo ImageDirectory = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images");
|
public static readonly FileInfo ImageDirectory = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images");
|
||||||
public static readonly FileInfo ImageJoinOutput = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images{Path.DirectorySeparatorChar}output.mp4");
|
public static readonly FileInfo ImageJoinOutput = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images{Path.DirectorySeparatorChar}output.mp4");
|
||||||
|
|
||||||
public static string OutputLocation(this FileInfo file, VideoType type)
|
public static string OutputLocation(this FileInfo file, ContainerFormat type)
|
||||||
{
|
{
|
||||||
return OutputLocation(file, type, "_converted");
|
return OutputLocation(file, type.Extension, "_converted");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string OutputLocation(this FileInfo file, AudioType type)
|
public static string OutputLocation(this FileInfo file, AudioType type)
|
||||||
{
|
{
|
||||||
return OutputLocation(file, type, "_audio");
|
return OutputLocation(file, type.ToString(), "_audio");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string OutputLocation(this FileInfo file, ImageType type)
|
public static string OutputLocation(this FileInfo file, ImageType type)
|
||||||
{
|
{
|
||||||
return OutputLocation(file, type, "_screenshot");
|
return OutputLocation(file, type.ToString(), "_screenshot");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string OutputLocation(this FileInfo file, Enum type, string keyword)
|
public static string OutputLocation(this FileInfo file, string type, string keyword)
|
||||||
{
|
{
|
||||||
string originalLocation = file.Directory.FullName,
|
string originalLocation = file.Directory.FullName,
|
||||||
outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToString().ToLowerInvariant());
|
outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToLowerInvariant());
|
||||||
|
|
||||||
return $"{originalLocation}{Path.DirectorySeparatorChar}{outputFile}";
|
return $"{originalLocation}{Path.DirectorySeparatorChar}{outputFile}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace FFMpegCore.Test
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class VideoTest : BaseTest
|
public class VideoTest : BaseTest
|
||||||
{
|
{
|
||||||
public bool Convert(VideoType type, bool multithreaded = false, VideoSize size = VideoSize.Original)
|
public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize size = VideoSize.Original)
|
||||||
{
|
{
|
||||||
var output = Input.OutputLocation(type);
|
var output = Input.OutputLocation(type);
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public bool Convert(VideoType type, bool multithreaded = false, VideoSize size =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConvertFromStreamPipe(VideoType type, params IArgument[] inputArguments)
|
private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] inputArguments)
|
||||||
{
|
{
|
||||||
var output = Input.OutputLocation(type);
|
var output = Input.OutputLocation(type);
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ private void ConvertToStreamPipe(params IArgument[] inputArguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Convert(VideoType type, params IArgument[] inputArguments)
|
public void Convert(ContainerFormat type, Action<MediaAnalysis> validationMethod, params IArgument[] inputArguments)
|
||||||
{
|
{
|
||||||
var output = Input.OutputLocation(type);
|
var output = Input.OutputLocation(type);
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ public void Convert(VideoType type, params IArgument[] inputArguments)
|
||||||
|
|
||||||
Assert.IsTrue(File.Exists(output));
|
Assert.IsTrue(File.Exists(output));
|
||||||
Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1);
|
Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1);
|
||||||
|
validationMethod?.Invoke(outputVideo);
|
||||||
if (scaling?.Size == null)
|
if (scaling?.Size == null)
|
||||||
{
|
{
|
||||||
Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
|
Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
|
||||||
|
@ -207,7 +207,12 @@ public void Convert(VideoType type, params IArgument[] inputArguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ConvertFromPipe(VideoType type, PixelFormat fmt, params IArgument[] inputArguments)
|
public void Convert(ContainerFormat type, params IArgument[] inputArguments)
|
||||||
|
{
|
||||||
|
Convert(type, null, inputArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] inputArguments)
|
||||||
{
|
{
|
||||||
var output = Input.OutputLocation(type);
|
var output = Input.OutputLocation(type);
|
||||||
|
|
||||||
|
@ -261,6 +266,13 @@ public void Video_ToMP4()
|
||||||
Convert(VideoType.Mp4);
|
Convert(VideoType.Mp4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Video_ToMP4_YUV444p()
|
||||||
|
{
|
||||||
|
Convert(VideoType.Mp4, (a) => Assert.IsTrue(a.VideoStreams.First().PixelFormat == "yuv444p"),
|
||||||
|
new ForcePixelFormat("yuv444p"));
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Video_ToMP4_Args()
|
public void Video_ToMP4_Args()
|
||||||
{
|
{
|
||||||
|
@ -268,10 +280,10 @@ public void Video_ToMP4_Args()
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(PixelFormat.Format24bppRgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||||
[DataRow(PixelFormat.Format32bppArgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||||
// [DataRow(PixelFormat.Format48bppRgb)]
|
// [DataRow(PixelFormat.Format48bppRgb)]
|
||||||
public void Video_ToMP4_Args_Pipe(PixelFormat pixelFormat)
|
public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
|
||||||
{
|
{
|
||||||
ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264));
|
ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264));
|
||||||
}
|
}
|
||||||
|
@ -342,16 +354,16 @@ public void Video_ToTS_Args()
|
||||||
Convert(VideoType.Ts,
|
Convert(VideoType.Ts,
|
||||||
new CopyArgument(),
|
new CopyArgument(),
|
||||||
new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB),
|
new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB),
|
||||||
new ForceFormatArgument(VideoCodec.MpegTs));
|
new ForceFormatArgument(VideoType.MpegTs));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(PixelFormat.Format24bppRgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||||
[DataRow(PixelFormat.Format32bppArgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||||
// [DataRow(PixelFormat.Format48bppRgb)]
|
// [DataRow(PixelFormat.Format48bppRgb)]
|
||||||
public void Video_ToTS_Args_Pipe(PixelFormat pixelFormat)
|
public void Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
|
||||||
{
|
{
|
||||||
ConvertFromPipe(VideoType.Ts, pixelFormat, new ForceFormatArgument(VideoCodec.MpegTs));
|
ConvertFromPipe(VideoType.Ts, pixelFormat, new ForceFormatArgument(VideoType.Ts));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
@ -367,10 +379,10 @@ public void Video_ToOGV_Resize_Args()
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(PixelFormat.Format24bppRgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||||
[DataRow(PixelFormat.Format32bppArgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||||
// [DataRow(PixelFormat.Format48bppRgb)]
|
// [DataRow(PixelFormat.Format48bppRgb)]
|
||||||
public void Video_ToOGV_Resize_Args_Pipe(PixelFormat pixelFormat)
|
public void Video_ToOGV_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
|
||||||
{
|
{
|
||||||
ConvertFromPipe(VideoType.Ogv, pixelFormat, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora));
|
ConvertFromPipe(VideoType.Ogv, pixelFormat, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora));
|
||||||
}
|
}
|
||||||
|
@ -388,10 +400,10 @@ public void Video_ToMP4_Resize_Args()
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(PixelFormat.Format24bppRgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||||
[DataRow(PixelFormat.Format32bppArgb)]
|
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||||
// [DataRow(PixelFormat.Format48bppRgb)]
|
// [DataRow(PixelFormat.Format48bppRgb)]
|
||||||
public void Video_ToMP4_Resize_Args_Pipe(PixelFormat pixelFormat)
|
public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
|
||||||
{
|
{
|
||||||
ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264));
|
ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264));
|
||||||
}
|
}
|
||||||
|
@ -466,7 +478,7 @@ public void Video_Snapshot_PersistSnapshot()
|
||||||
public void Video_Join()
|
public void Video_Join()
|
||||||
{
|
{
|
||||||
var output = Input.OutputLocation(VideoType.Mp4);
|
var output = Input.OutputLocation(VideoType.Mp4);
|
||||||
var newInput = Input.OutputLocation(VideoType.Mp4, "duplicate");
|
var newInput = Input.OutputLocation(VideoType.Mp4.Name, "duplicate");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var input = FFProbe.Analyse(Input.FullName);
|
var input = FFProbe.Analyse(Input.FullName);
|
||||||
|
@ -613,7 +625,7 @@ public void Video_TranscodeInMemory()
|
||||||
{
|
{
|
||||||
using var resStream = new MemoryStream();
|
using var resStream = new MemoryStream();
|
||||||
var reader = new StreamPipeDataReader(resStream);
|
var reader = new StreamPipeDataReader(resStream);
|
||||||
var writer = new RawVideoPipeDataWriter(BitmapSource.CreateBitmaps(128, PixelFormat.Format24bppRgb, 128, 128));
|
var writer = new RawVideoPipeDataWriter(BitmapSource.CreateBitmaps(128, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 128, 128));
|
||||||
|
|
||||||
FFMpegArguments
|
FFMpegArguments
|
||||||
.FromPipe(writer)
|
.FromPipe(writer)
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace FFMpegCore.Enums
|
|
||||||
{
|
|
||||||
public static class FileExtension
|
|
||||||
{
|
|
||||||
public static string Extension(this VideoType type)
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
VideoType.Mp4 => Mp4,
|
|
||||||
VideoType.Ogv => Ogv,
|
|
||||||
VideoType.Ts => Ts,
|
|
||||||
VideoType.WebM => WebM,
|
|
||||||
_ => throw new Exception("The extension for this video type is not defined.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public static string Extension(this VideoCodec type)
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
VideoCodec.LibX264 => Mp4,
|
|
||||||
VideoCodec.LibVpx => WebM,
|
|
||||||
VideoCodec.LibTheora => Ogv,
|
|
||||||
VideoCodec.MpegTs => Ts,
|
|
||||||
VideoCodec.Png => Png,
|
|
||||||
_ => throw new Exception("The extension for this video type is not defined.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public static readonly string Mp4 = ".mp4";
|
|
||||||
public static readonly string Mp3 = ".mp3";
|
|
||||||
public static readonly string Ts = ".ts";
|
|
||||||
public static readonly string Ogv = ".ogv";
|
|
||||||
public static readonly string Png = ".png";
|
|
||||||
public static readonly string WebM = ".webm";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
namespace FFMpegCore.Enums
|
|
||||||
{
|
|
||||||
public enum VideoType
|
|
||||||
{
|
|
||||||
Mp4,
|
|
||||||
Ogv,
|
|
||||||
Ts,
|
|
||||||
WebM
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
using FFMpegCore.Enums;
|
using FFMpegCore.Enums;
|
||||||
|
using FFMpegCore.Exceptions;
|
||||||
|
|
||||||
namespace FFMpegCore.Arguments
|
namespace FFMpegCore.Arguments
|
||||||
{
|
{
|
||||||
|
@ -7,8 +8,17 @@ namespace FFMpegCore.Arguments
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AudioCodecArgument : IArgument
|
public class AudioCodecArgument : IArgument
|
||||||
{
|
{
|
||||||
public readonly AudioCodec AudioCodec;
|
public readonly string AudioCodec;
|
||||||
public AudioCodecArgument(AudioCodec audioCodec)
|
|
||||||
|
public AudioCodecArgument(Codec audioCodec)
|
||||||
|
{
|
||||||
|
if (audioCodec.Type != CodecType.Audio)
|
||||||
|
throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec");
|
||||||
|
|
||||||
|
AudioCodec = audioCodec.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioCodecArgument(string audioCodec)
|
||||||
{
|
{
|
||||||
AudioCodec = audioCodec;
|
AudioCodec = audioCodec;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@ public ForceFormatArgument(string format)
|
||||||
_format = format;
|
_format = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ForceFormatArgument(VideoCodec value) : this(value.ToString().ToLowerInvariant()) { }
|
public ForceFormatArgument(ContainerFormat format)
|
||||||
|
{
|
||||||
|
_format = format.Name;
|
||||||
|
}
|
||||||
|
|
||||||
public string Text => $"-f {_format}";
|
public string Text => $"-f {_format}";
|
||||||
}
|
}
|
||||||
|
|
17
FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs
Normal file
17
FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using FFMpegCore.Enums;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Arguments
|
||||||
|
{
|
||||||
|
public class ForcePixelFormat : IArgument
|
||||||
|
{
|
||||||
|
public string PixelFormat { get; private set; }
|
||||||
|
public string Text => $"-pix_fmt {PixelFormat}";
|
||||||
|
|
||||||
|
public ForcePixelFormat(string format)
|
||||||
|
{
|
||||||
|
PixelFormat = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForcePixelFormat(PixelFormat format) : this(format.Name) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using FFMpegCore.Enums;
|
using FFMpegCore.Enums;
|
||||||
|
using FFMpegCore.Exceptions;
|
||||||
|
|
||||||
namespace FFMpegCore.Arguments
|
namespace FFMpegCore.Arguments
|
||||||
{
|
{
|
||||||
|
@ -14,8 +15,14 @@ public VideoCodecArgument(string codec)
|
||||||
Codec = codec;
|
Codec = codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VideoCodecArgument(VideoCodec value) : this(value.ToString().ToLowerInvariant()) { }
|
public VideoCodecArgument(Codec value)
|
||||||
|
{
|
||||||
|
if (value.Type != CodecType.Video)
|
||||||
|
throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{value.Name}\" is not a video codec");
|
||||||
|
|
||||||
public string Text => $"-c:v {Codec} -pix_fmt yuv420p";
|
Codec = value.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Text => $"-c:v {Codec}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,154 @@
|
||||||
namespace FFMpegCore.Enums
|
using FFMpegCore.Exceptions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Enums
|
||||||
{
|
{
|
||||||
public enum VideoCodec
|
public enum FeatureStatus
|
||||||
{
|
{
|
||||||
LibX264,
|
Unknown,
|
||||||
LibVpx,
|
NotSupported,
|
||||||
LibTheora,
|
Supported,
|
||||||
Png,
|
|
||||||
MpegTs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AudioCodec
|
public class Codec
|
||||||
{
|
{
|
||||||
Aac,
|
private static readonly Regex _codecsFormatRegex = new Regex(@"([D\.])([E\.])([VASD\.])([I\.])([L\.])([S\.])\s+([a-z0-9_-]+)\s+(.+)");
|
||||||
LibVorbis,
|
private static readonly Regex _decodersEncodersFormatRegex = new Regex(@"([VASD\.])([F\.])([S\.])([X\.])([B\.])([D\.])\s+([a-z0-9_-]+)\s+(.+)");
|
||||||
LibFdk_Aac,
|
|
||||||
Ac3,
|
public class FeatureLevel
|
||||||
Eac3,
|
{
|
||||||
LibMp3Lame
|
public bool IsExperimental { get; internal set; }
|
||||||
|
public FeatureStatus SupportsFrameLevelMultithreading { get; internal set; }
|
||||||
|
public FeatureStatus SupportsSliceLevelMultithreading { get; internal set; }
|
||||||
|
public FeatureStatus SupportsDrawHorizBand { get; internal set; }
|
||||||
|
public FeatureStatus SupportsDirectRendering { get; internal set; }
|
||||||
|
|
||||||
|
internal void Merge(FeatureLevel other)
|
||||||
|
{
|
||||||
|
IsExperimental |= other.IsExperimental;
|
||||||
|
SupportsFrameLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsFrameLevelMultithreading, (int)other.SupportsFrameLevelMultithreading);
|
||||||
|
SupportsSliceLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsSliceLevelMultithreading, (int)other.SupportsSliceLevelMultithreading);
|
||||||
|
SupportsDrawHorizBand = (FeatureStatus)Math.Max((int)SupportsDrawHorizBand, (int)other.SupportsDrawHorizBand);
|
||||||
|
SupportsDirectRendering = (FeatureStatus)Math.Max((int)SupportsDirectRendering, (int)other.SupportsDirectRendering);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Filter
|
public string Name { get; private set; }
|
||||||
|
public CodecType Type { get; private set; }
|
||||||
|
public bool DecodingSupported { get; private set; }
|
||||||
|
public bool EncodingSupported { get; private set; }
|
||||||
|
public bool IsIntraFrameOnly { get; private set; }
|
||||||
|
public bool IsLossy { get; private set; }
|
||||||
|
public bool IsLossless { get; private set; }
|
||||||
|
public string Description { get; private set; } = null!;
|
||||||
|
|
||||||
|
public FeatureLevel EncoderFeatureLevel { get; private set; }
|
||||||
|
public FeatureLevel DecoderFeatureLevel { get; private set; }
|
||||||
|
|
||||||
|
internal Codec(string name, CodecType type)
|
||||||
{
|
{
|
||||||
H264_Mp4ToAnnexB,
|
EncoderFeatureLevel = new FeatureLevel();
|
||||||
Aac_AdtstoAsc
|
DecoderFeatureLevel = new FeatureLevel();
|
||||||
|
Name = name;
|
||||||
|
Type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Channel
|
internal static bool TryParseFromCodecs(string line, out Codec codec)
|
||||||
{
|
{
|
||||||
Audio,
|
var match = _codecsFormatRegex.Match(line);
|
||||||
Video,
|
if (!match.Success)
|
||||||
Both
|
{
|
||||||
|
codec = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = match.Groups[7].Value;
|
||||||
|
var type = match.Groups[3].Value switch
|
||||||
|
{
|
||||||
|
"V" => CodecType.Video,
|
||||||
|
"A" => CodecType.Audio,
|
||||||
|
"D" => CodecType.Data,
|
||||||
|
"S" => CodecType.Subtitle,
|
||||||
|
_ => CodecType.Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
if(type == CodecType.Unknown)
|
||||||
|
{
|
||||||
|
codec = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = new Codec(name, type);
|
||||||
|
|
||||||
|
codec.DecodingSupported = match.Groups[1].Value != ".";
|
||||||
|
codec.EncodingSupported = match.Groups[2].Value != ".";
|
||||||
|
codec.IsIntraFrameOnly = match.Groups[4].Value != ".";
|
||||||
|
codec.IsLossy = match.Groups[5].Value != ".";
|
||||||
|
codec.IsLossless = match.Groups[6].Value != ".";
|
||||||
|
codec.Description = match.Groups[8].Value;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
internal static bool TryParseFromEncodersDecoders(string line, out Codec codec, bool isEncoder)
|
||||||
|
{
|
||||||
|
var match = _decodersEncodersFormatRegex.Match(line);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
codec = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = match.Groups[7].Value;
|
||||||
|
var type = match.Groups[1].Value switch
|
||||||
|
{
|
||||||
|
"V" => CodecType.Video,
|
||||||
|
"A" => CodecType.Audio,
|
||||||
|
"D" => CodecType.Data,
|
||||||
|
"S" => CodecType.Subtitle,
|
||||||
|
_ => CodecType.Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type == CodecType.Unknown)
|
||||||
|
{
|
||||||
|
codec = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = new Codec(name, type);
|
||||||
|
|
||||||
|
var featureLevel = isEncoder ? codec.EncoderFeatureLevel : codec.DecoderFeatureLevel;
|
||||||
|
|
||||||
|
codec.DecodingSupported = !isEncoder;
|
||||||
|
codec.EncodingSupported = isEncoder;
|
||||||
|
featureLevel.SupportsFrameLevelMultithreading = match.Groups[2].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported;
|
||||||
|
featureLevel.SupportsSliceLevelMultithreading = match.Groups[3].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported;
|
||||||
|
featureLevel.IsExperimental = match.Groups[4].Value != ".";
|
||||||
|
featureLevel.SupportsDrawHorizBand = match.Groups[5].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported;
|
||||||
|
featureLevel.SupportsDirectRendering = match.Groups[6].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported;
|
||||||
|
codec.Description = match.Groups[8].Value;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
internal void Merge(Codec other)
|
||||||
|
{
|
||||||
|
if (Name != other.Name)
|
||||||
|
throw new FFMpegException(FFMpegExceptionType.Operation, "different codecs enable to merge");
|
||||||
|
|
||||||
|
Type |= other.Type;
|
||||||
|
DecodingSupported |= other.DecodingSupported;
|
||||||
|
EncodingSupported |= other.EncodingSupported;
|
||||||
|
IsIntraFrameOnly |= other.IsIntraFrameOnly;
|
||||||
|
IsLossy |= other.IsLossy;
|
||||||
|
IsLossless |= other.IsLossless;
|
||||||
|
|
||||||
|
EncoderFeatureLevel.Merge(other.EncoderFeatureLevel);
|
||||||
|
DecoderFeatureLevel.Merge(other.DecoderFeatureLevel);
|
||||||
|
|
||||||
|
if (Description != other.Description)
|
||||||
|
Description += "\r\n" + other.Description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
47
FFMpegCore/FFMpeg/Enums/ContainerFormat.cs
Normal file
47
FFMpegCore/FFMpeg/Enums/ContainerFormat.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Enums
|
||||||
|
{
|
||||||
|
public class ContainerFormat
|
||||||
|
{
|
||||||
|
private static readonly Regex _formatRegex = new Regex(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)");
|
||||||
|
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public bool DemuxingSupported { get; private set; }
|
||||||
|
public bool MuxingSupported { get; private set; }
|
||||||
|
public string Description { get; private set; } = null!;
|
||||||
|
public string Extension
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name))
|
||||||
|
return FFMpegOptions.Options.ExtensionOverrides[Name];
|
||||||
|
return "." + Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ContainerFormat(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryParse(string line, out ContainerFormat fmt)
|
||||||
|
{
|
||||||
|
var match = _formatRegex.Match(line);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
fmt = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt = new ContainerFormat(match.Groups[3].Value);
|
||||||
|
fmt.DemuxingSupported = match.Groups[1].Value == " ";
|
||||||
|
fmt.MuxingSupported = match.Groups[2].Value == " ";
|
||||||
|
fmt.Description = match.Groups[4].Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
FFMpegCore/FFMpeg/Enums/Enums.cs
Normal file
54
FFMpegCore/FFMpeg/Enums/Enums.cs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
namespace FFMpegCore.Enums
|
||||||
|
{
|
||||||
|
public enum CodecType
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Video = 1 << 1,
|
||||||
|
Audio = 1 << 2,
|
||||||
|
Subtitle = 1 << 3,
|
||||||
|
Data = 1 << 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class VideoCodec
|
||||||
|
{
|
||||||
|
public static Codec LibX264 => FFMpeg.GetCodec("libx264");
|
||||||
|
public static Codec LibVpx => FFMpeg.GetCodec("libvpx");
|
||||||
|
public static Codec LibTheora => FFMpeg.GetCodec("libtheora");
|
||||||
|
public static Codec Png => FFMpeg.GetCodec("png");
|
||||||
|
public static Codec MpegTs => FFMpeg.GetCodec("mpegts");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AudioCodec
|
||||||
|
{
|
||||||
|
public static Codec Aac => FFMpeg.GetCodec("aac");
|
||||||
|
public static Codec LibVorbis => FFMpeg.GetCodec("libvorbis");
|
||||||
|
public static Codec LibFdk_Aac => FFMpeg.GetCodec("libfdk_aac");
|
||||||
|
public static Codec Ac3 => FFMpeg.GetCodec("ac3");
|
||||||
|
public static Codec Eac3 => FFMpeg.GetCodec("eac3");
|
||||||
|
public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class VideoType
|
||||||
|
{
|
||||||
|
public static ContainerFormat MpegTs => FFMpeg.GetContinerFormat("mpegts");
|
||||||
|
public static ContainerFormat Ts => FFMpeg.GetContinerFormat("mpegts");
|
||||||
|
public static ContainerFormat Mp4 => FFMpeg.GetContinerFormat("mp4");
|
||||||
|
public static ContainerFormat Mov => FFMpeg.GetContinerFormat("mov");
|
||||||
|
public static ContainerFormat Avi => FFMpeg.GetContinerFormat("avi");
|
||||||
|
public static ContainerFormat Ogv => FFMpeg.GetContinerFormat("ogv");
|
||||||
|
public static ContainerFormat WebM => FFMpeg.GetContinerFormat("webm");
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Filter
|
||||||
|
{
|
||||||
|
H264_Mp4ToAnnexB,
|
||||||
|
Aac_AdtstoAsc
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Channel
|
||||||
|
{
|
||||||
|
Audio,
|
||||||
|
Video,
|
||||||
|
Both
|
||||||
|
}
|
||||||
|
}
|
26
FFMpegCore/FFMpeg/Enums/FileExtension.cs
Normal file
26
FFMpegCore/FFMpeg/Enums/FileExtension.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Enums
|
||||||
|
{
|
||||||
|
public static class FileExtension
|
||||||
|
{
|
||||||
|
public static string Extension(this Codec type)
|
||||||
|
{
|
||||||
|
return type.Name switch
|
||||||
|
{
|
||||||
|
"libx264" => Mp4,
|
||||||
|
"libxvpx" => WebM,
|
||||||
|
"libxtheora" => Ogv,
|
||||||
|
"mpegts" => Ts,
|
||||||
|
"png" => Png,
|
||||||
|
_ => throw new Exception("The extension for this video type is not defined.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static readonly string Mp4 = ".mp4";
|
||||||
|
public static readonly string Mp3 = ".mp3";
|
||||||
|
public static readonly string Ts = ".ts";
|
||||||
|
public static readonly string Ogv = ".ogv";
|
||||||
|
public static readonly string Png = ".png";
|
||||||
|
public static readonly string WebM = ".webm";
|
||||||
|
}
|
||||||
|
}
|
56
FFMpegCore/FFMpeg/Enums/PixelFormat.cs
Normal file
56
FFMpegCore/FFMpeg/Enums/PixelFormat.cs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Enums
|
||||||
|
{
|
||||||
|
public class PixelFormat
|
||||||
|
{
|
||||||
|
private static readonly Regex _formatRegex = new Regex(@"([I\.])([O\.])([H\.])([P\.])([B\.])\s+(\S+)\s+([0-9]+)\s+([0-9]+)");
|
||||||
|
|
||||||
|
public bool InputConversionSupported { get; private set; }
|
||||||
|
public bool OutputConversionSupported { get; private set; }
|
||||||
|
public bool HardwareAccelerationSupported { get; private set; }
|
||||||
|
public bool IsPaletted { get; private set; }
|
||||||
|
public bool IsBitstream { get; private set; }
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public int Components { get; private set; }
|
||||||
|
public int BitsPerPixel { get; private set; }
|
||||||
|
|
||||||
|
public bool CanConvertTo(PixelFormat other)
|
||||||
|
{
|
||||||
|
return InputConversionSupported && other.OutputConversionSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal PixelFormat(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryParse(string line, out PixelFormat fmt)
|
||||||
|
{
|
||||||
|
var match = _formatRegex.Match(line);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
fmt = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt = new PixelFormat(match.Groups[6].Value);
|
||||||
|
fmt.InputConversionSupported = match.Groups[1].Value != ".";
|
||||||
|
fmt.OutputConversionSupported = match.Groups[2].Value != ".";
|
||||||
|
fmt.HardwareAccelerationSupported = match.Groups[3].Value != ".";
|
||||||
|
fmt.IsPaletted = match.Groups[4].Value != ".";
|
||||||
|
fmt.IsBitstream = match.Groups[5].Value != ".";
|
||||||
|
if (!int.TryParse(match.Groups[7].Value, out var nbComponents))
|
||||||
|
return false;
|
||||||
|
fmt.Components = nbComponents;
|
||||||
|
if (!int.TryParse(match.Groups[8].Value, out var bpp))
|
||||||
|
return false;
|
||||||
|
fmt.BitsPerPixel = bpp;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FFMpegCore.Enums;
|
using FFMpegCore.Enums;
|
||||||
|
using FFMpegCore.Exceptions;
|
||||||
using FFMpegCore.Helpers;
|
using FFMpegCore.Helpers;
|
||||||
|
|
||||||
namespace FFMpegCore
|
namespace FFMpegCore
|
||||||
|
@ -90,13 +91,13 @@ public static Bitmap Snapshot(MediaAnalysis source, string output, Size? size =
|
||||||
public static bool Convert(
|
public static bool Convert(
|
||||||
MediaAnalysis source,
|
MediaAnalysis source,
|
||||||
string output,
|
string output,
|
||||||
VideoType type = VideoType.Mp4,
|
ContainerFormat format,
|
||||||
Speed speed = Speed.SuperFast,
|
Speed speed = Speed.SuperFast,
|
||||||
VideoSize size = VideoSize.Original,
|
VideoSize size = VideoSize.Original,
|
||||||
AudioQuality audioQuality = AudioQuality.Normal,
|
AudioQuality audioQuality = AudioQuality.Normal,
|
||||||
bool multithreaded = false)
|
bool multithreaded = false)
|
||||||
{
|
{
|
||||||
FFMpegHelper.ExtensionExceptionCheck(output, type.Extension());
|
FFMpegHelper.ExtensionExceptionCheck(output, format.Extension);
|
||||||
FFMpegHelper.ConversionSizeExceptionCheck(source);
|
FFMpegHelper.ConversionSizeExceptionCheck(source);
|
||||||
|
|
||||||
var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size;
|
var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size;
|
||||||
|
@ -105,9 +106,9 @@ public static bool Convert(
|
||||||
if (outputSize.Width % 2 != 0)
|
if (outputSize.Width % 2 != 0)
|
||||||
outputSize.Width += 1;
|
outputSize.Width += 1;
|
||||||
|
|
||||||
return type switch
|
return format.Name switch
|
||||||
{
|
{
|
||||||
VideoType.Mp4 => FFMpegArguments
|
"mp4" => FFMpegArguments
|
||||||
.FromInputFiles(true, source.Path)
|
.FromInputFiles(true, source.Path)
|
||||||
.UsingMultithreading(multithreaded)
|
.UsingMultithreading(multithreaded)
|
||||||
.WithVideoCodec(VideoCodec.LibX264)
|
.WithVideoCodec(VideoCodec.LibX264)
|
||||||
|
@ -118,7 +119,7 @@ public static bool Convert(
|
||||||
.WithAudioBitrate(audioQuality)
|
.WithAudioBitrate(audioQuality)
|
||||||
.OutputToFile(output)
|
.OutputToFile(output)
|
||||||
.ProcessSynchronously(),
|
.ProcessSynchronously(),
|
||||||
VideoType.Ogv => FFMpegArguments
|
"ogv" => FFMpegArguments
|
||||||
.FromInputFiles(true, source.Path)
|
.FromInputFiles(true, source.Path)
|
||||||
.UsingMultithreading(multithreaded)
|
.UsingMultithreading(multithreaded)
|
||||||
.WithVideoCodec(VideoCodec.LibTheora)
|
.WithVideoCodec(VideoCodec.LibTheora)
|
||||||
|
@ -129,14 +130,14 @@ public static bool Convert(
|
||||||
.WithAudioBitrate(audioQuality)
|
.WithAudioBitrate(audioQuality)
|
||||||
.OutputToFile(output)
|
.OutputToFile(output)
|
||||||
.ProcessSynchronously(),
|
.ProcessSynchronously(),
|
||||||
VideoType.Ts => FFMpegArguments
|
"mpegts" => FFMpegArguments
|
||||||
.FromInputFiles(true, source.Path)
|
.FromInputFiles(true, source.Path)
|
||||||
.CopyChannel()
|
.CopyChannel()
|
||||||
.WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB)
|
.WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB)
|
||||||
.ForceFormat(VideoCodec.MpegTs)
|
.ForceFormat(VideoType.Ts)
|
||||||
.OutputToFile(output)
|
.OutputToFile(output)
|
||||||
.ProcessSynchronously(),
|
.ProcessSynchronously(),
|
||||||
VideoType.WebM => FFMpegArguments
|
"webm" => FFMpegArguments
|
||||||
.FromInputFiles(true, source.Path)
|
.FromInputFiles(true, source.Path)
|
||||||
.UsingMultithreading(multithreaded)
|
.UsingMultithreading(multithreaded)
|
||||||
.WithVideoCodec(VideoCodec.LibVpx)
|
.WithVideoCodec(VideoCodec.LibVpx)
|
||||||
|
@ -147,7 +148,7 @@ public static bool Convert(
|
||||||
.WithAudioBitrate(audioQuality)
|
.WithAudioBitrate(audioQuality)
|
||||||
.OutputToFile(output)
|
.OutputToFile(output)
|
||||||
.ProcessSynchronously(),
|
.ProcessSynchronously(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
_ => throw new ArgumentOutOfRangeException(nameof(format))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,6 +324,179 @@ public static bool ReplaceAudio(string input, string inputAudio, string output,
|
||||||
.ProcessSynchronously();
|
.ProcessSynchronously();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region PixelFormats
|
||||||
|
internal static IReadOnlyList<Enums.PixelFormat> GetPixelFormatsInternal()
|
||||||
|
{
|
||||||
|
FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory);
|
||||||
|
|
||||||
|
var list = new List<Enums.PixelFormat>();
|
||||||
|
using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary, "-pix_fmts");
|
||||||
|
instance.DataReceived += (e, args) =>
|
||||||
|
{
|
||||||
|
if (Enums.PixelFormat.TryParse(args.Data, out var fmt))
|
||||||
|
list.Add(fmt);
|
||||||
|
};
|
||||||
|
|
||||||
|
var exitCode = instance.BlockUntilFinished();
|
||||||
|
if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData));
|
||||||
|
|
||||||
|
return list.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<Enums.PixelFormat> GetPixelFormats()
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
return GetPixelFormatsInternal();
|
||||||
|
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetPixelFormat(string name, out Enums.PixelFormat fmt)
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
{
|
||||||
|
fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||||
|
return fmt != null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return FFMpegCache.PixelFormats.TryGetValue(name, out fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Enums.PixelFormat GetPixelFormat(string name)
|
||||||
|
{
|
||||||
|
if (TryGetPixelFormat(name, out var fmt))
|
||||||
|
return fmt;
|
||||||
|
throw new FFMpegException(FFMpegExceptionType.Operation, $"Pixel format \"{name}\" not supported");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Codecs
|
||||||
|
internal static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string arguments, Func<string, Codec?> parser)
|
||||||
|
{
|
||||||
|
FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory);
|
||||||
|
|
||||||
|
using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary, arguments);
|
||||||
|
instance.DataReceived += (e, args) =>
|
||||||
|
{
|
||||||
|
var codec = parser(args.Data);
|
||||||
|
if(codec != null)
|
||||||
|
if (codecs.TryGetValue(codec.Name, out var parentCodec))
|
||||||
|
parentCodec.Merge(codec);
|
||||||
|
else
|
||||||
|
codecs.Add(codec.Name, codec);
|
||||||
|
};
|
||||||
|
|
||||||
|
var exitCode = instance.BlockUntilFinished();
|
||||||
|
if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Dictionary<string, Codec> GetCodecsInternal()
|
||||||
|
{
|
||||||
|
var res = new Dictionary<string, Codec>();
|
||||||
|
ParsePartOfCodecs(res, "-codecs", (s) =>
|
||||||
|
{
|
||||||
|
if (Codec.TryParseFromCodecs(s, out var codec))
|
||||||
|
return codec;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
ParsePartOfCodecs(res, "-encoders", (s) =>
|
||||||
|
{
|
||||||
|
if (Codec.TryParseFromEncodersDecoders(s, out var codec, true))
|
||||||
|
return codec;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
ParsePartOfCodecs(res, "-decoders", (s) =>
|
||||||
|
{
|
||||||
|
if (Codec.TryParseFromEncodersDecoders(s, out var codec, false))
|
||||||
|
return codec;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<Codec> GetCodecs()
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
return GetCodecsInternal().Values.ToList().AsReadOnly();
|
||||||
|
return FFMpegCache.Codecs.Values.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<Codec> GetCodecs(CodecType type)
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly();
|
||||||
|
return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<Codec> GetVideoCodecs() => GetCodecs(CodecType.Video);
|
||||||
|
public static IReadOnlyList<Codec> GetAudioCodecs() => GetCodecs(CodecType.Audio);
|
||||||
|
public static IReadOnlyList<Codec> GetSubtitleCodecs() => GetCodecs(CodecType.Subtitle);
|
||||||
|
public static IReadOnlyList<Codec> GetDataCodecs() => GetCodecs(CodecType.Data);
|
||||||
|
|
||||||
|
public static bool TryGetCodec(string name, out Codec codec)
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
{
|
||||||
|
codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||||
|
return codec != null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return FFMpegCache.Codecs.TryGetValue(name, out codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec GetCodec(string name)
|
||||||
|
{
|
||||||
|
if (TryGetCodec(name, out var codec) && codec != null)
|
||||||
|
return codec;
|
||||||
|
throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{name}\" not supported");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ContainerFormats
|
||||||
|
internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
|
||||||
|
{
|
||||||
|
FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory);
|
||||||
|
|
||||||
|
var list = new List<ContainerFormat>();
|
||||||
|
using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary, "-formats");
|
||||||
|
instance.DataReceived += (e, args) =>
|
||||||
|
{
|
||||||
|
if (ContainerFormat.TryParse(args.Data, out var fmt))
|
||||||
|
list.Add(fmt);
|
||||||
|
};
|
||||||
|
|
||||||
|
var exitCode = instance.BlockUntilFinished();
|
||||||
|
if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData));
|
||||||
|
|
||||||
|
return list.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<ContainerFormat> GetContainerFormats()
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
return GetContainersFormatsInternal();
|
||||||
|
return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetContainerFormat(string name, out ContainerFormat fmt)
|
||||||
|
{
|
||||||
|
if (!FFMpegOptions.Options.UseCache)
|
||||||
|
{
|
||||||
|
fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||||
|
return fmt != null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return FFMpegCache.ContainerFormats.TryGetValue(name, out fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContainerFormat GetContinerFormat(string name)
|
||||||
|
{
|
||||||
|
if (TryGetContainerFormat(name, out var fmt))
|
||||||
|
return fmt;
|
||||||
|
throw new FFMpegException(FFMpegExceptionType.Operation, $"Container format \"{name}\" not supported");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
private static void Cleanup(IEnumerable<string> pathList)
|
private static void Cleanup(IEnumerable<string> pathList)
|
||||||
{
|
{
|
||||||
foreach (var path in pathList)
|
foreach (var path in pathList)
|
||||||
|
|
|
@ -35,7 +35,8 @@ private FFMpegArguments(IInputArgument inputArgument)
|
||||||
public static FFMpegArguments FromPipe(IPipeDataWriter writer) => new FFMpegArguments(new InputPipeArgument(writer));
|
public static FFMpegArguments FromPipe(IPipeDataWriter writer) => new FFMpegArguments(new InputPipeArgument(writer));
|
||||||
|
|
||||||
|
|
||||||
public FFMpegArguments WithAudioCodec(AudioCodec audioCodec) => WithArgument(new AudioCodecArgument(audioCodec));
|
public FFMpegArguments WithAudioCodec(Codec audioCodec) => WithArgument(new AudioCodecArgument(audioCodec));
|
||||||
|
public FFMpegArguments WithAudioCodec(string audioCodec) => WithArgument(new AudioCodecArgument(audioCodec));
|
||||||
public FFMpegArguments WithAudioBitrate(AudioQuality audioQuality) => WithArgument(new AudioBitrateArgument(audioQuality));
|
public FFMpegArguments WithAudioBitrate(AudioQuality audioQuality) => WithArgument(new AudioBitrateArgument(audioQuality));
|
||||||
public FFMpegArguments WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate));
|
public FFMpegArguments WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate));
|
||||||
public FFMpegArguments WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate));
|
public FFMpegArguments WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate));
|
||||||
|
@ -61,7 +62,7 @@ private FFMpegArguments(IInputArgument inputArgument)
|
||||||
public FFMpegArguments UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread));
|
public FFMpegArguments UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread));
|
||||||
public FFMpegArguments UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads));
|
public FFMpegArguments UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads));
|
||||||
|
|
||||||
public FFMpegArguments WithVideoCodec(VideoCodec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
|
public FFMpegArguments WithVideoCodec(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
|
||||||
public FFMpegArguments WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
|
public FFMpegArguments WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
|
||||||
public FFMpegArguments WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate));
|
public FFMpegArguments WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate));
|
||||||
public FFMpegArguments WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
|
public FFMpegArguments WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
|
||||||
|
@ -77,8 +78,11 @@ private FFMpegArguments(IInputArgument inputArgument)
|
||||||
public FFMpegArguments OverwriteExisting() => WithArgument(new OverwriteArgument());
|
public FFMpegArguments OverwriteExisting() => WithArgument(new OverwriteArgument());
|
||||||
public FFMpegArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithArgument(new VerbosityLevelArgument(verbosityLevel));
|
public FFMpegArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithArgument(new VerbosityLevelArgument(verbosityLevel));
|
||||||
|
|
||||||
public FFMpegArguments ForceFormat(VideoCodec videoCodec) => WithArgument(new ForceFormatArgument(videoCodec));
|
public FFMpegArguments ForceFormat(ContainerFormat format) => WithArgument(new ForceFormatArgument(format));
|
||||||
public FFMpegArguments ForceFormat(string videoCodec) => WithArgument(new ForceFormatArgument(videoCodec));
|
public FFMpegArguments ForceFormat(string format) => WithArgument(new ForceFormatArgument(format));
|
||||||
|
public FFMpegArguments ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
|
||||||
|
public FFMpegArguments ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
|
||||||
|
|
||||||
public FFMpegArguments DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
|
public FFMpegArguments DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
|
||||||
|
|
||||||
public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = false) => ToProcessor(new OutputArgument(file, overwrite));
|
public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = false) => ToProcessor(new OutputArgument(file, overwrite));
|
||||||
|
|
54
FFMpegCore/FFMpeg/FFMpegCache.cs
Normal file
54
FFMpegCore/FFMpeg/FFMpegCache.cs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
using FFMpegCore.Enums;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore
|
||||||
|
{
|
||||||
|
static class FFMpegCache
|
||||||
|
{
|
||||||
|
private static readonly object _syncObject = new object();
|
||||||
|
private static Dictionary<string, PixelFormat>? _pixelFormats;
|
||||||
|
private static Dictionary<string, Codec>? _codecs;
|
||||||
|
private static Dictionary<string, ContainerFormat>? _containers;
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, PixelFormat> PixelFormats
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_pixelFormats == null) //First check not thread safe
|
||||||
|
lock (_syncObject)
|
||||||
|
if (_pixelFormats == null)//Second check thread safe
|
||||||
|
_pixelFormats = FFMpeg.GetPixelFormatsInternal().ToDictionary(x => x.Name);
|
||||||
|
|
||||||
|
return _pixelFormats;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public static IReadOnlyDictionary<string, Codec> Codecs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_codecs == null) //First check not thread safe
|
||||||
|
lock (_syncObject)
|
||||||
|
if (_codecs == null)//Second check thread safe
|
||||||
|
_codecs = FFMpeg.GetCodecsInternal();
|
||||||
|
|
||||||
|
return _codecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public static IReadOnlyDictionary<string, ContainerFormat> ContainerFormats
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_containers == null) //First check not thread safe
|
||||||
|
lock (_syncObject)
|
||||||
|
if (_containers == null)//Second check thread safe
|
||||||
|
_containers = FFMpeg.GetContainersFormatsInternal().ToDictionary(x => x.Name);
|
||||||
|
|
||||||
|
return _containers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
@ -9,6 +10,10 @@ public class FFMpegOptions
|
||||||
{
|
{
|
||||||
private static readonly string ConfigFile = Path.Combine(".", "ffmpeg.config.json");
|
private static readonly string ConfigFile = Path.Combine(".", "ffmpeg.config.json");
|
||||||
private static readonly string DefaultRoot = Path.Combine(".", "FFMPEG", "bin");
|
private static readonly string DefaultRoot = Path.Combine(".", "FFMPEG", "bin");
|
||||||
|
private static readonly Dictionary<string, string> DefaultExtensionsOverrides = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "mpegts", ".ts" },
|
||||||
|
};
|
||||||
|
|
||||||
public static FFMpegOptions Options { get; private set; } = new FFMpegOptions();
|
public static FFMpegOptions Options { get; private set; } = new FFMpegOptions();
|
||||||
|
|
||||||
|
@ -25,7 +30,11 @@ public static void Configure(FFMpegOptions options)
|
||||||
static FFMpegOptions()
|
static FFMpegOptions()
|
||||||
{
|
{
|
||||||
if (File.Exists(ConfigFile))
|
if (File.Exists(ConfigFile))
|
||||||
|
{
|
||||||
Options = JsonSerializer.Deserialize<FFMpegOptions>(File.ReadAllText(ConfigFile));
|
Options = JsonSerializer.Deserialize<FFMpegOptions>(File.ReadAllText(ConfigFile));
|
||||||
|
foreach (var kv in DefaultExtensionsOverrides)
|
||||||
|
if (!Options.ExtensionOverrides.ContainsKey(kv.Key)) Options.ExtensionOverrides.Add(kv.Key, kv.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RootDirectory { get; set; } = DefaultRoot;
|
public string RootDirectory { get; set; } = DefaultRoot;
|
||||||
|
@ -34,6 +43,10 @@ static FFMpegOptions()
|
||||||
|
|
||||||
public string FFProbeBinary => FFBinary("FFProbe");
|
public string FFProbeBinary => FFBinary("FFProbe");
|
||||||
|
|
||||||
|
public Dictionary<string, string> ExtensionOverrides { get; private set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public bool UseCache { get; set; } = true;
|
||||||
|
|
||||||
private static string FFBinary(string name)
|
private static string FFBinary(string name)
|
||||||
{
|
{
|
||||||
var ffName = name.ToLowerInvariant();
|
var ffName = name.ToLowerInvariant();
|
||||||
|
|
|
@ -32,4 +32,8 @@
|
||||||
<PackageReference Include="System.Text.Json" Version="4.7.1" />
|
<PackageReference Include="System.Text.Json" Version="4.7.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="FFMpeg\Models\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using FFMpegCore.Enums;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace FFMpegCore
|
namespace FFMpegCore
|
||||||
{
|
{
|
||||||
|
@ -9,5 +10,7 @@ public class MediaStream
|
||||||
public string CodecLongName { get; internal set; } = null!;
|
public string CodecLongName { get; internal set; } = null!;
|
||||||
public int BitRate { get; internal set; }
|
public int BitRate { get; internal set; }
|
||||||
public TimeSpan Duration { get; internal set; }
|
public TimeSpan Duration { get; internal set; }
|
||||||
|
|
||||||
|
public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
namespace FFMpegCore
|
using FFMpegCore.Enums;
|
||||||
|
|
||||||
|
namespace FFMpegCore
|
||||||
{
|
{
|
||||||
public class VideoStream : MediaStream
|
public class VideoStream : MediaStream
|
||||||
{
|
{
|
||||||
|
@ -10,5 +12,7 @@ public class VideoStream : MediaStream
|
||||||
public int Height { get; internal set; }
|
public int Height { get; internal set; }
|
||||||
public double FrameRate { get; internal set; }
|
public double FrameRate { get; internal set; }
|
||||||
public string PixelFormat { get; internal set; } = null!;
|
public string PixelFormat { get; internal set; } = null!;
|
||||||
|
|
||||||
|
public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue