Skip to content

Commit

Permalink
Implement Building Videos From Image Sequences (#187)
Browse files Browse the repository at this point in the history
* Implement Building Videos From Image Sequences

* Don't Rename SetFramerate

* Create IInputBuilder to allow passing list of files to FFmpeg

* Add Second Overload of BuildVideoFromImages

* Remove StartNumber from Second Overload

* Merge
  • Loading branch information
tomaszzmuda committed Aug 5, 2019
1 parent a2fc45c commit cd9f535
Show file tree
Hide file tree
Showing 27 changed files with 404 additions and 51 deletions.
4 changes: 2 additions & 2 deletions Xabe.FFmpeg.Test/AudioStreamTests.cs
Expand Up @@ -20,8 +20,8 @@ public async Task ChangeSpeedTest(int expectedDuration, int expectedAudioDuratio
string outputPath = Path.ChangeExtension(Path.GetTempFileName(), FileExtensions.Mp3);

IConversionResult conversionResult = await Conversion.New()
.AddStream(inputFile.AudioStreams.First().ChangeSpeed(speed))
//.SetPreset(ConversionPreset.UltraFast)
.AddStream(inputFile.AudioStreams.First().ChangeSpeed(speed))
//.SetPreset(ConversionPreset.UltraFast)
.SetOutput(outputPath)
.Start().ConfigureAwait(false);

Expand Down
68 changes: 68 additions & 0 deletions Xabe.FFmpeg.Test/ConversionTests.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -102,6 +103,24 @@ public async Task SetInputAndOutputFormatTest()
Assert.Equal(".avi", resultFile.FileInfo.Extension);
}

[Fact]
public async Task SetOutputPixelFormatTest()
{
string output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + FileExtensions.Mp4);
IMediaInfo info = await MediaInfo.Get(Resources.MkvWithAudio).ConfigureAwait(false);
IVideoStream videoStream = info.VideoStreams.First()?.SetCodec(VideoCodec.Mpeg4);

IConversionResult conversionResult = await Conversion.New()
.AddStream(videoStream)
.SetOutputPixelFormat(PixelFormat.Yuv420P)
.SetOutput(output)
.Start().ConfigureAwait(false);

Assert.True(conversionResult.Success);
IMediaInfo resultFile = conversionResult.MediaInfo.Value;
Assert.Equal("yuv420p", resultFile.VideoStreams.First().PixelFormat);
}

[RunnableInDebugOnly]
public async Task GetScreenCaptureTest()
{
Expand Down Expand Up @@ -316,6 +335,55 @@ public async Task ExtractNthFrameTest()
.Where(x => x.Contains(fileGuid.ToString()) && Path.GetExtension(x) == FileExtensions.Png).Count();

Assert.Equal(1, outputFilesCount);
}

[Fact]
public async Task BuildVideoFromImagesTest()
{
List<string> files = Directory.EnumerateFiles(Resources.Images).ToList();
InputBuilder builder = new InputBuilder();
string preparedFilesDir = string.Empty;
Func<string, string> inputBuilder = builder.PrepareInputFiles(files, out preparedFilesDir);
string output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + FileExtensions.Mp4);

IConversionResult conversionResult = await Conversion.New()
.SetInputFrameRate(1)
.BuildVideoFromImages(1, inputBuilder)
.SetFrameRate(1)
.SetOutputPixelFormat(PixelFormat.Yuv420P)
.SetOutput(output)
.Start().ConfigureAwait(true);

int preparedFilesCount = Directory.EnumerateFiles(preparedFilesDir).ToList().Count;

Assert.True(conversionResult.Success);
IMediaInfo resultFile = conversionResult.MediaInfo.Value;
Assert.Equal(builder.FileList.Count, preparedFilesCount);
Assert.Equal(TimeSpan.FromSeconds(12), resultFile.VideoStreams.First().Duration);
Assert.Equal(1, resultFile.VideoStreams.First().FrameRate);
Assert.Equal("yuv420p", resultFile.VideoStreams.First().PixelFormat);
}

[Fact]
public async Task BuildVideoFromImagesListTest()
{
List<string> files = Directory.EnumerateFiles(Resources.Images).ToList();
string preparedFilesDir = string.Empty;
string output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + FileExtensions.Mp4);

IConversionResult conversionResult = await Conversion.New()
.SetInputFrameRate(1)
.BuildVideoFromImages(files)
.SetFrameRate(1)
.SetOutputPixelFormat(PixelFormat.Yuv420P)
.SetOutput(output)
.Start().ConfigureAwait(true);

Assert.True(conversionResult.Success);
IMediaInfo resultFile = conversionResult.MediaInfo.Value;
Assert.Equal(TimeSpan.FromSeconds(12), resultFile.VideoStreams.First().Duration);
Assert.Equal(1, resultFile.VideoStreams.First().FrameRate);
Assert.Equal("yuv420p", resultFile.VideoStreams.First().PixelFormat);
}

[Fact]
Expand Down
27 changes: 27 additions & 0 deletions Xabe.FFmpeg.Test/InputBuilderTests.cs
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Xabe.FFmpeg.Test
{
public class InputBuilderTests
{
[Fact]
public async Task PrepareInputFilesTest()
{
List<string> files = Directory.EnumerateFiles(Resources.Images).ToList();
InputBuilder builder = new InputBuilder();
string directory = string.Empty;

Func<string, string> inputBuilder = builder.PrepareInputFiles(files, out directory);
List<string> preparedFiles = Directory.EnumerateFiles(directory).ToList();

Assert.Equal(12, builder.FileList.Count);
Assert.Equal(builder.FileList.Count, preparedFiles.Count);
}
}
}
2 changes: 2 additions & 0 deletions Xabe.FFmpeg.Test/Resources.cs
Expand Up @@ -16,6 +16,8 @@ internal static class Resources
internal static readonly string BunnyMp4 = GetResourceFilePath("bunny.mp4");
internal static readonly string Dll = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Xabe.FFmpeg.Test.dll");

internal static readonly string Images = GetResourceFilePath("Images");

internal static readonly string SubtitleSrt = GetResourceFilePath("sampleSrt.srt");

internal static readonly string FFbinariesInfo = GetResourceFilePath("ffbinaries.json");
Expand Down
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_001.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_002.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_003.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_004.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_005.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_006.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_007.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_008.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_009.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_010.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_011.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Xabe.FFmpeg.Test/Resources/Images/img_012.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions Xabe.FFmpeg.sln.DotSettings
Expand Up @@ -41,7 +41,9 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_WHILE_STMT/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/LINE_FEED_AT_FILE_END/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OLD_ENGINE/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSORHOLDER_ON_SINGLE_LINE/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_EMBEDDED_STATEMENT_ON_SAME_LINE/@EntryValue">NEVER</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">LINE_BREAK</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_CATCH_PARENTHESES/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EXTENDS_COLON/@EntryValue">False</s:Boolean>
Expand Down Expand Up @@ -150,7 +152,12 @@
<s:Int64 x:Key="/Default/Environment/General/WorkspaceHostInitializationTimeout/@EntryValue">2000</s:Int64>
<s:Int64 x:Key="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue">25</s:Int64>
<s:Boolean x:Key="/Default/Environment/MemoryUsageIndicator/IsVisible/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EJavaScript_002ECodeStyle_002ESettingsUpgrade_002EJsParsFormattingSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
Expand Down
77 changes: 63 additions & 14 deletions Xabe.FFmpeg/Conversion/Conversion.cs
Expand Up @@ -38,6 +38,7 @@ public partial class Conversion : IConversion

private ProcessPriorityClass? _priority = null;
private FFmpegWrapper _ffmpeg;
private Func<string, string> _buildInputFileName = null;
private Func<string, string> _buildOutputFileName = null;

/// <inheritdoc />
Expand All @@ -54,11 +55,15 @@ public string Build()
builder.Append(BuildInputFormat());
builder.Append(_inputTime);
builder.Append(BuildParameters(ParameterPosition.PreInput));

if (!_capturing)
{
builder.Append(BuildInputParameters());
builder.Append(BuildInput());

if (_buildInputFileName == null)
_buildInputFileName = (number) => { return BuildInput(); };

builder.Append(_buildInputFileName("_%03d"));
}

builder.Append(BuildOverwriteOutputParameter(_overwriteOutput));
Expand All @@ -69,6 +74,7 @@ public string Build()
builder.Append(BuildMap());
builder.Append(BuildParameters(ParameterPosition.PostInput));
builder.Append(_outputTime);
builder.Append(BuildOutputPixelFormat());
builder.Append(BuildOutputFormat());
builder.Append(_buildOutputFileName("_%03d"));

Expand All @@ -92,7 +98,10 @@ public string Build()
public MediaFormat InputFormat { get; private set; }

/// <inheritdoc />
public MediaFormat OutputFormat { get; private set; }
public MediaFormat OutputFormat { get; private set; }

/// <inheritdoc />
public PixelFormat OutputPixelFormat { get; private set; }

/// <inheritdoc />
public Task<IConversionResult> Start()
Expand Down Expand Up @@ -277,7 +286,7 @@ public IConversion ExtractEveryNthFrame(int frameNo, Func<string, string> buildO
{
_buildOutputFileName = buildOutputFileName;
AddParameter(string.Format("-vf select='not(mod(n\\,{0}))'", frameNo));
AddParameter("-vsync vfr");
AddParameter("-vsync vfr", ParameterPosition.PostInput);

return this;
}
Expand All @@ -287,14 +296,40 @@ public IConversion ExtractNthFrame(int frameNo, Func<string, string> buildOutput
{
_buildOutputFileName = buildOutputFileName;
AddParameter(string.Format("-vf select='eq(n\\,{0})'", frameNo));
AddParameter("-vsync 0");
AddParameter("-vsync 0", ParameterPosition.PostInput);
return this;
}

/// <inheritdoc />
public IConversion BuildVideoFromImages(int startNumber, Func<string, string> buildInputFileName)
{
_buildInputFileName = buildInputFileName;
AddParameter($"-start_number {startNumber}", ParameterPosition.PreInput);
return this;
}

/// <inheritdoc />
public IConversion BuildVideoFromImages(IEnumerable<string> imageFiles)
{
InputBuilder builder = new InputBuilder();
string directory = string.Empty;

_buildInputFileName = builder.PrepareInputFiles(imageFiles.ToList(), out directory);

return this;
}

/// <inheritdoc />
public IConversion SetInputFrameRate(double frameRate)
{
AddParameter($"-framerate {frameRate}", ParameterPosition.PreInput);
return this;
}

/// <inheritdoc />
public IConversion SetFrameRate(double frameRate)
{
AddParameter($"-framerate {frameRate}");
AddParameter($"-framerate {frameRate}", ParameterPosition.PostInput);
return this;
}

Expand All @@ -308,7 +343,7 @@ public IConversion GetScreenCapture(double frameRate)
SetInputFormat(MediaFormat.GdiGrab);
SetFrameRate(frameRate);
AddParameter("-i desktop ", ParameterPosition.PreInput);
AddParameter("-pix_fmt yuv420p");
SetOutputPixelFormat(PixelFormat.Yuv420P);
AddParameter("-preset ultrafast");
return this;
}
Expand All @@ -318,8 +353,8 @@ public IConversion GetScreenCapture(double frameRate)

SetInputFormat(MediaFormat.AVFoundation);
SetFrameRate(frameRate);
AddParameter("-i 1:1 ", ParameterPosition.PreInput);
AddParameter("-pix_fmt yuv420p");
AddParameter("-i 1:1 ", ParameterPosition.PreInput);
SetOutputPixelFormat(PixelFormat.Yuv420P);
AddParameter("-preset ultrafast");
return this;
}
Expand All @@ -329,8 +364,8 @@ public IConversion GetScreenCapture(double frameRate)

SetInputFormat(MediaFormat.X11Grab);
SetFrameRate(frameRate);
AddParameter("-i :0.0+0,0 ", ParameterPosition.PreInput);
AddParameter("-pix_fmt yuv420p");
AddParameter("-i :0.0+0,0 ", ParameterPosition.PreInput);
SetOutputPixelFormat(PixelFormat.Yuv420P);
AddParameter("-preset ultrafast");
return this;
}
Expand Down Expand Up @@ -494,6 +529,14 @@ private string BuildOutputFormat()
return string.Empty;
}

private string BuildOutputPixelFormat()
{
if (OutputPixelFormat != null)
return $"-pix_fmt {OutputPixelFormat.ToString()} ";
else
return string.Empty;
}

private bool HasH264Stream()
{
foreach (IStream stream in _streams)
Expand Down Expand Up @@ -548,14 +591,20 @@ public IConversion SetInputFormat(MediaFormat inputFormat)
{
InputFormat = inputFormat;
return this;
}


}

/// <inheritdoc />
public IConversion SetOutputFormat(MediaFormat outputFormat)
{
OutputFormat = outputFormat;
return this;
}

/// <inheritdoc />
public IConversion SetOutputPixelFormat(PixelFormat outputPixelFormat)
{
OutputPixelFormat = outputPixelFormat;
return this;
}
}
}
38 changes: 34 additions & 4 deletions Xabe.FFmpeg/Conversion/IConversion.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -54,11 +55,33 @@ public interface IConversion
IConversion ExtractNthFrame(int frameNo, Func<string, string> buildOutputFileName);

/// <summary>
/// Builds the -framerate option for this conversion
/// Builds a video from a directory containing one or more sequentially named images
/// </summary>
/// <param name="frameRate"></param>
/// <param name="startNumber">The number of the image to start building video from</param>
/// <param name="buildInputFileName"> Delegate Function to build up custom filename when inputting multiple files </param>
/// <returns>IConversion object</returns>
IConversion SetFrameRate(double frameRate);
IConversion BuildVideoFromImages(int startNumber, Func<string, string> buildInputFileName);

/// <summary>
/// Builds a video from a directory containing one or more sequentially named images
/// </summary>
/// <param name="imageFiles"> List of Image Files to Build into a Video</param>
/// <returns>IConversion object</returns>
IConversion BuildVideoFromImages(IEnumerable<string> imageFiles);

/// <summary>
/// Builds the -framerate option for the output of this conversion
/// </summary>
/// <param name="frameRate">The desired framerate of the output</param>
/// <returns>IConversion object</returns>
IConversion SetFrameRate(double frameRate);

/// <summary>
/// Builds the -framerate option for the input of this conversion
/// </summary>
/// <param name="frameRate">the desired framerate of the input</param>
/// <returns>IConversion object</returns>
IConversion SetInputFrameRate(double frameRate);

/// <summary>
/// Seeks in output file to position. (-ss argument)
Expand Down Expand Up @@ -152,7 +175,14 @@ public interface IConversion
/// </summary>
/// <param name="outputFormat">The output format to set</param>
/// <returns>IConversion object</returns>
IConversion SetOutputFormat(MediaFormat outputFormat);
IConversion SetOutputFormat(MediaFormat outputFormat);

/// <summary>
/// Sets the pixel format for the output file using the -pix_fmt option before the output file name
/// </summary>
/// <param name="pixelFormat">The output pixel format to set</param>
/// <returns>IConversion object</returns>
IConversion SetOutputPixelFormat(PixelFormat pixelFormat);

/// <summary>
/// Fires when FFmpeg progress changes
Expand Down

0 comments on commit cd9f535

Please sign in to comment.