mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 08:34:12 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
3888a07ab2
13 changed files with 248 additions and 3 deletions
|
@ -324,6 +324,27 @@ public void Builder_BuildString_DrawtextFilter_Alt()
|
||||||
str);
|
str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_SubtitleHardBurnFilter()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments
|
||||||
|
.FromFileInput("input.mp4")
|
||||||
|
.OutputToFile("output.mp4", false, opt => opt
|
||||||
|
.WithVideoFilters(filterOptions => filterOptions
|
||||||
|
.HardBurnSubtitle(SubtitleHardBurnOptions
|
||||||
|
.Create(subtitlePath: "sample.srt")
|
||||||
|
.SetCharacterEncoding("UTF-8")
|
||||||
|
.SetOriginalSize(1366,768)
|
||||||
|
.SetSubtitleIndex(0)
|
||||||
|
.WithStyle(StyleOptions.Create()
|
||||||
|
.WithParameter("FontName", "DejaVu Serif")
|
||||||
|
.WithParameter("PrimaryColour", "&HAA00FF00")))))
|
||||||
|
.Arguments;
|
||||||
|
|
||||||
|
Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"",
|
||||||
|
str);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Builder_BuildString_StartNumber()
|
public void Builder_BuildString_StartNumber()
|
||||||
{
|
{
|
||||||
|
|
|
@ -83,6 +83,9 @@
|
||||||
<None Update="Resources\mute.mp4">
|
<None Update="Resources\mute.mp4">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Resources\sample.srt">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -100,5 +100,15 @@ public async Task Probe_Success_FromStream_Async()
|
||||||
var info = await FFProbe.AnalyseAsync(stream);
|
var info = await FFProbe.AnalyseAsync(stream);
|
||||||
Assert.AreEqual(3, info.Duration.Seconds);
|
Assert.AreEqual(3, info.Duration.Seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public async Task Probe_Success_Subtitle_Async()
|
||||||
|
{
|
||||||
|
var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle);
|
||||||
|
Assert.IsNotNull(info.PrimarySubtitleStream);
|
||||||
|
Assert.AreEqual(1, info.SubtitleStreams.Count);
|
||||||
|
Assert.AreEqual(0, info.AudioStreams.Count);
|
||||||
|
Assert.AreEqual(0, info.VideoStreams.Count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,5 +20,6 @@ public static class TestResources
|
||||||
public static readonly string Mp3Audio = "./Resources/audio.mp3";
|
public static readonly string Mp3Audio = "./Resources/audio.mp3";
|
||||||
public static readonly string PngImage = "./Resources/cover.png";
|
public static readonly string PngImage = "./Resources/cover.png";
|
||||||
public static readonly string ImageCollection = "./Resources/images";
|
public static readonly string ImageCollection = "./Resources/images";
|
||||||
|
public static readonly string SrtSubtitle = "./Resources/sample.srt";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
FFMpegCore.Test/Resources/sample.srt
Normal file
12
FFMpegCore.Test/Resources/sample.srt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
1
|
||||||
|
00:00:00,000 --> 00:00:01,500
|
||||||
|
For www.forom.com
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:01,500 --> 00:00:02,500
|
||||||
|
<i>Tonight's the night.</i>
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:03,000 --> 00:00:15,000
|
||||||
|
<i>And it's going to happen
|
||||||
|
again and again --</i>
|
24
FFMpegCore/Extend/KeyValuePairExtensions.cs
Normal file
24
FFMpegCore/Extend/KeyValuePairExtensions.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Extend
|
||||||
|
{
|
||||||
|
internal static class KeyValuePairExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Concat the two members of a <see cref="KeyValuePair{TKey,TValue}" />
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pair">Input object</param>
|
||||||
|
/// <param name="enclose">
|
||||||
|
/// If true encloses the value part between quotes if contains an space character. If false use the
|
||||||
|
/// value unmodified
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The formatted string</returns>
|
||||||
|
public static string FormatArgumentPair(this KeyValuePair<string, string> pair, bool enclose)
|
||||||
|
{
|
||||||
|
var key = pair.Key;
|
||||||
|
var value = enclose ? pair.Value.EncloseIfContainsSpace() : pair.Value;
|
||||||
|
|
||||||
|
return $"{key}={value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
FFMpegCore/Extend/StringExtensions.cs
Normal file
15
FFMpegCore/Extend/StringExtensions.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
namespace FFMpegCore.Extend
|
||||||
|
{
|
||||||
|
internal static class StringExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enclose string between quotes if contains an space character
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The input</param>
|
||||||
|
/// <returns>The enclosed string</returns>
|
||||||
|
public static string EncloseIfContainsSpace(this string input)
|
||||||
|
{
|
||||||
|
return input.Contains(" ") ? $"'{input}'" : input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs
Normal file
132
FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
using FFMpegCore.Extend;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Arguments
|
||||||
|
{
|
||||||
|
public class SubtitleHardBurnArgument : IVideoFilterArgument
|
||||||
|
{
|
||||||
|
private readonly SubtitleHardBurnOptions _subtitleHardBurnOptions;
|
||||||
|
|
||||||
|
public SubtitleHardBurnArgument(SubtitleHardBurnOptions subtitleHardBurnOptions)
|
||||||
|
{
|
||||||
|
_subtitleHardBurnOptions = subtitleHardBurnOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key => "subtitles";
|
||||||
|
|
||||||
|
public string Value => _subtitleHardBurnOptions.TextInternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubtitleHardBurnOptions
|
||||||
|
{
|
||||||
|
private readonly string _subtitle;
|
||||||
|
|
||||||
|
public readonly Dictionary<string, string> Parameters = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="SubtitleHardBurnOptions"/> using a provided subtitle file or a video file
|
||||||
|
/// containing one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subtitlePath"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <remarks>Only support .srt and .ass files, and subrip and ssa subtitle streams</remarks>
|
||||||
|
public static SubtitleHardBurnOptions Create(string subtitlePath)
|
||||||
|
{
|
||||||
|
return new SubtitleHardBurnOptions(subtitlePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubtitleHardBurnOptions(string subtitle)
|
||||||
|
{
|
||||||
|
_subtitle = subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the size of the original video, the video for which the ASS file was composed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width"></param>
|
||||||
|
/// <param name="height"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public SubtitleHardBurnOptions SetOriginalSize(int width, int height)
|
||||||
|
{
|
||||||
|
return WithParameter("original_size", $"{width}x{height}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the size of the original video, the video for which the ASS file was composed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="size"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public SubtitleHardBurnOptions SetOriginalSize(Size size)
|
||||||
|
{
|
||||||
|
return SetOriginalSize(size.Width, size.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set subtitles stream index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Used when the provided subtitle is an stream of a video file (ex. .mkv) with multiple subtitles.
|
||||||
|
/// Represent the index of the subtitle not the stream, them the first subtitle index is 0 and second is 1
|
||||||
|
/// </remarks>
|
||||||
|
public SubtitleHardBurnOptions SetSubtitleIndex(int index)
|
||||||
|
{
|
||||||
|
return WithParameter("stream_index", index.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set subtitles input character encoding. Only useful if not UTF-8
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encode">Charset encoding</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public SubtitleHardBurnOptions SetCharacterEncoding(string encode)
|
||||||
|
{
|
||||||
|
return WithParameter("charenc", encode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override default style or script info parameters of the subtitles
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="styleOptions"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions)
|
||||||
|
{
|
||||||
|
return WithParameter("force_style", styleOptions.TextInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubtitleHardBurnOptions WithParameter(string key, string value)
|
||||||
|
{
|
||||||
|
Parameters.Add(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string TextInternal => string.Join(":", new[] { _subtitle.EncloseIfContainsSpace() }.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StyleOptions
|
||||||
|
{
|
||||||
|
public readonly Dictionary<string, string> Parameters = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public static StyleOptions Create()
|
||||||
|
{
|
||||||
|
return new StyleOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to override default style or script info parameters of the subtitles. It accepts ASS style format
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public StyleOptions WithParameter(string key, string value)
|
||||||
|
{
|
||||||
|
Parameters.Add(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string TextInternal => string.Join(",", Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: false)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ public class VideoFilterOptions
|
||||||
public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition));
|
public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition));
|
||||||
public VideoFilterOptions Mirror(Mirroring mirroring) => WithArgument(new SetMirroringArgument(mirroring));
|
public VideoFilterOptions Mirror(Mirroring mirroring) => WithArgument(new SetMirroringArgument(mirroring));
|
||||||
public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
|
public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
|
||||||
|
public VideoFilterOptions HardBurnSubtitle(SubtitleHardBurnOptions subtitleHardBurnOptions) => WithArgument(new SubtitleHardBurnArgument(subtitleHardBurnOptions));
|
||||||
|
|
||||||
private VideoFilterOptions WithArgument(IVideoFilterArgument argument)
|
private VideoFilterOptions WithArgument(IVideoFilterArgument argument)
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,7 +9,9 @@ public interface IMediaAnalysis
|
||||||
MediaFormat Format { get; }
|
MediaFormat Format { get; }
|
||||||
AudioStream? PrimaryAudioStream { get; }
|
AudioStream? PrimaryAudioStream { get; }
|
||||||
VideoStream? PrimaryVideoStream { get; }
|
VideoStream? PrimaryVideoStream { get; }
|
||||||
|
SubtitleStream? PrimarySubtitleStream { get; }
|
||||||
List<VideoStream> VideoStreams { get; }
|
List<VideoStream> VideoStreams { get; }
|
||||||
List<AudioStream> AudioStreams { get; }
|
List<AudioStream> AudioStreams { get; }
|
||||||
|
List<SubtitleStream> SubtitleStreams { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,9 @@ internal MediaAnalysis(FFProbeAnalysis analysis)
|
||||||
Format = ParseFormat(analysis.Format);
|
Format = ParseFormat(analysis.Format);
|
||||||
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
|
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
|
||||||
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
|
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
|
||||||
|
SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaFormat ParseFormat(Format analysisFormat)
|
private MediaFormat ParseFormat(Format analysisFormat)
|
||||||
{
|
{
|
||||||
return new MediaFormat
|
return new MediaFormat
|
||||||
|
@ -36,12 +37,14 @@ private MediaFormat ParseFormat(Format analysisFormat)
|
||||||
}.Max();
|
}.Max();
|
||||||
|
|
||||||
public MediaFormat Format { get; }
|
public MediaFormat Format { get; }
|
||||||
|
|
||||||
public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault();
|
public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault();
|
||||||
|
|
||||||
public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault();
|
public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault();
|
||||||
|
public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy(stream => stream.Index).FirstOrDefault();
|
||||||
|
|
||||||
public List<VideoStream> VideoStreams { get; }
|
public List<VideoStream> VideoStreams { get; }
|
||||||
public List<AudioStream> AudioStreams { get; }
|
public List<AudioStream> AudioStreams { get; }
|
||||||
|
public List<SubtitleStream> SubtitleStreams { get; }
|
||||||
|
|
||||||
private VideoStream ParseVideoStream(FFProbeStream stream)
|
private VideoStream ParseVideoStream(FFProbeStream stream)
|
||||||
{
|
{
|
||||||
|
@ -84,7 +87,19 @@ private AudioStream ParseAudioStream(FFProbeStream stream)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SubtitleStream ParseSubtitleStream(FFProbeStream stream)
|
||||||
|
{
|
||||||
|
return new SubtitleStream
|
||||||
|
{
|
||||||
|
Index = stream.Index,
|
||||||
|
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
|
||||||
|
CodecName = stream.CodecName,
|
||||||
|
CodecLongName = stream.CodecLongName,
|
||||||
|
Duration = MediaAnalysisUtils.ParseDuration(stream),
|
||||||
|
Language = stream.GetLanguage(),
|
||||||
|
Tags = stream.Tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MediaAnalysisUtils
|
public static class MediaAnalysisUtils
|
||||||
|
|
7
FFMpegCore/FFProbe/SubtitleStream.cs
Normal file
7
FFMpegCore/FFProbe/SubtitleStream.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace FFMpegCore
|
||||||
|
{
|
||||||
|
public class SubtitleStream : MediaStream
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -186,6 +186,8 @@ The default value of an empty string (expecting ffmpeg to be found through PATH)
|
||||||
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
|
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
|
||||||
// or
|
// or
|
||||||
GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
|
GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
|
||||||
|
// on some systems the absolute path may be required, in which case
|
||||||
|
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = Server.MapPath("./bin"), TemporaryFilesFolder = Server.MapPath("/tmp") });
|
||||||
|
|
||||||
// or individual, per-run options
|
// or individual, per-run options
|
||||||
await FFMpegArguments
|
await FFMpegArguments
|
||||||
|
|
Loading…
Reference in a new issue