Skip to content

Latest commit

 

History

552 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

// ... (rest of README.md content remains unchanged) ## IntegrationExample The `IntegrationExample` class demonstrates how to integrate the FFmpeg .NET Wrapper into ASP.NET Core applications. It shows dependency injection setup, service registration, and usage patterns in controllers and services. This example serves as a starting point for building web applications that leverage FFmpeg capabilities. Here is an example usage of the `IntegrationExample` class with its public members: ```csharp using FFmpegDotnetWrapper.Configuration; using FFmpegDotnetWrapper.Models; using FFmpegDotnetWrapper.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; // Create and configure the web host var builder = WebApplication.CreateBuilder(args); // Configure services ConfigureServices(builder.Services); // Build the application var app = builder.Build(); // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseAuthorization(); // Map minimal API endpoints for video processing MapEndpoints(app); app.Run("http://localhost:5000"); // Configure services including FFmpeg wrapper in the DI container private static void ConfigureServices(IServiceCollection services) { // Add controllers (if using MVC) services.AddControllers(); // Add logging services.AddLogging(builder => { builder.AddConsole(); builder.AddDebug(); }); // Configure FFmpeg wrapper with application-specific settings services.AddFFmpegWrapper(options => { // Set appropriate timeout for web operations (10 minutes) options.DefaultTimeout = TimeSpan.FromMinutes(10); // Enable detailed logging for debugging options.EnableDetailedLogging = false; // Configure operation caching for better performance options.EnableOperationCaching = true; options.MaxCachedOperations = 1000; }); // Register additional services services.AddSingleton(); services.AddSingleton(); } // Video trimming example using the service var videoProcessingService = new VideoProcessingService(ffmpegService, logger); var result = await videoProcessingService.ConvertForWebOptimizationAsync( inputFile: @"/videos/input.mp4", outputFile: @"/videos/web-optimized.mp4" ); if (result.IsSuccess) { Console.WriteLine($"Web optimization completed: {result.OutputMedia?.FileSize} bytes"); } // Create thumbnail example var thumbnailResult = await videoProcessingService.CreateThumbnailAsync( inputFile: @"/videos/input.mp4", outputFile: @"/thumbnails/preview.jpg", timestamp: TimeSpan.FromSeconds(30) ); if (thumbnailResult.IsSuccess) { Console.WriteLine("Thumbnail created successfully"); } // Media analysis example var mediaAnalysisService = new MediaAnalysisService(ffmpegService, logger); var mediaInfo = await mediaAnalysisService.GetMediaInfoAsync(@"/videos/input.mp4"); Console.WriteLine($"Duration: {mediaInfo.Duration.TotalSeconds}s"); Console.WriteLine($"Resolution: {mediaInfo.Width}x{mediaInfo.Height}"); Console.WriteLine($"Video codec: {mediaInfo.VideoCodec}"); ``` ## ICacheService The `ICacheService` interface provides an in-memory caching mechanism for storing frequently accessed data like media metadata and operation results. It supports time-based expiration, automatic cleanup of expired entries, and size-based eviction using an LRU (Least Recently Used) policy. This helps reduce unnecessary file system access and expensive FFmpeg probing operations. Here is an example usage of the `ICacheService` interface with its public members: ```csharp using FFmpegDotnetWrapper.Caching; using Microsoft.Extensions.Logging; // Create a cache service instance with default settings (max 1000 entries, 1 hour expiration) var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); var cacheService = new CacheService(loggerFactory.CreateLogger()); // Store a value in the cache with default expiration (1 hour) cacheService.Set("media-info-123", mediaMetadata); // Store a value with custom expiration (5 minutes) cacheService.Set("thumbnail-urls", thumbnailUrls, TimeSpan.FromMinutes(5)); // Retrieve a cached value var cachedMediaInfo = cacheService.Get("media-info-123"); if (cachedMediaInfo != null) { Console.WriteLine($"Retrieved cached media info: {cachedMediaInfo.Duration}"); } // Check if a key exists by attempting to retrieve it var thumbnailUrls = cacheService.Get>("thumbnail-urls"); if (thumbnailUrls != null) { Console.WriteLine($"Found {thumbnailUrls.Count} cached thumbnails"); } // Remove a specific cache entry var wasRemoved = cacheService.Remove("media-info-123"); Console.WriteLine($"Entry removed: {wasRemoved}"); // Get current cache statistics var stats = cacheService.GetStats(); Console.WriteLine($"Cache stats - Count: {stats.Count}, MaxSize: {stats.MaxSize}, Utilization: {stats.Utilization:F2}%"); // Clear the entire cache (useful during application shutdown or cache invalidation) cacheService.Clear(); // The Count property allows checking cache size without retrieving statistics var currentCount = cacheService.Count; Console.WriteLine($"Current cache entries: {currentCount}"); ``` ## StreamingPipelineMetrics The `StreamingPipelineMetrics` class provides real-time monitoring and analytics for streaming pipeline performance, tracking segment production, bitrate switches, pipeline completion/failure states, and comprehensive profile-based metrics breakdown. It enables detailed performance analysis and debugging of FFmpeg transcoding workflows by collecting and exporting operational data. Here is an example usage of the `StreamingPipelineMetrics` class with its public members: ```csharp using FFmpegDotnetWrapper.Monitoring; // Create a metrics tracker for a specific profile var metrics = new StreamingPipelineMetrics("hls-transcode-1080p"); // Record segment production events metrics.RecordSegmentProduced("segment_001.ts", TimeSpan.FromSeconds(2.45)); metrics.RecordSegmentProduced("segment_002.ts", TimeSpan.FromSeconds(2.51)); // Record bitrate switches during encoding metrics.RecordBitrateSwitch("1080p", 5000); // kbps metrics.RecordBitrateSwitch("720p", 3000); // kbps metrics.RecordBitrateSwitch("480p", 1500); // kbps // Record pipeline completion metrics.RecordPipelineCompleted(TimeSpan.FromSeconds(125.3)); // Get detailed profile breakdown var profileMetrics = metrics.GetProfileBreakdown(); foreach (var profile in profileMetrics) { Console.WriteLine($"Profile {profile.Key}: {profile.Value.SegmentsProduced} segments, " + $"Avg duration: {profile.Value.AverageSegmentDuration.TotalSeconds:F2}s"); } // Generate summary report var summary = metrics.GetSummaryReport(); Console.WriteLine(summary); // Export metrics to CSV for external analysis var csvData = metrics.ExportProfilesAsCsv(); File.WriteAllText("pipeline_metrics.csv", csvData); // Reset metrics for a new encoding session metrics.Reset(); // Create a new metrics instance with a different profile var backupMetrics = new StreamingPipelineMetrics("backup-stream-720p"); ``` ## OperationStats The `OperationStats` class provides detailed statistics and performance metrics for FFmpeg operations, tracking success rates, execution times, data throughput, and failure analysis. It supports aggregating statistics across multiple operations and generating comprehensive performance reports for monitoring and optimization purposes. Here is an example usage of the `OperationStats` class with its public members: ```csharp using FFmpegDotnetWrapper.Monitoring; using FFmpegDotnetWrapper.Models; // Create operation statistics tracker for a specific operation type var stats = new OperationStats(OperationType.Transcode); // Record successful operations stats.RecordSuccess(TimeSpan.FromSeconds(45.2), 157286400); // 150MB processed stats.RecordSuccess(TimeSpan.FromSeconds(38.7), 125829120); // 120MB processed stats.RecordSuccess(TimeSpan.FromSeconds(52.1), 214958080); // 205MB processed // Record failed operations stats.RecordFailure(TimeSpan.FromSeconds(12.5)); stats.RecordFailure(TimeSpan.FromSeconds(8.3)); // Get current statistics Console.WriteLine($"Total attempts: {stats.TotalAttempts}"); Console.WriteLine($"Successful operations: {stats.SuccessfulOperations}"); Console.WriteLine($"Failed operations: {stats.FailedOperations}"); Console.WriteLine($"Total bytes processed: {stats.TotalBytesProcessed} ({stats.TotalBytesProcessed / (1024.0 * 1024.0):F2} MB)"); Console.WriteLine($"Total execution time: {stats.TotalExecutionTime}"); Console.WriteLine($"Average execution time: {stats.TotalExecutionTime / stats.TotalAttempts:F2}s"); Console.WriteLine($"Minimum execution time: {stats.MinimumExecutionTime}"); Console.WriteLine($"Maximum execution time: {stats.MaximumExecutionTime}"); Console.WriteLine($"Last updated: {stats.LastUpdated}"); // Get aggregate statistics from multiple operation instances var allStats = new List { stats, new OperationStats(OperationType.Watermark) }; var aggregateStats = OperationStats.GetAggregateStatistics(allStats); Console.WriteLine($"Aggregate successful operations: {aggregateStats.SuccessfulOperations}"); // Generate performance report var report = stats.GetPerformanceReport(); Console.WriteLine(report); // Export statistics as CSV var csv = stats.ExportAsCSV(); File.WriteAllText("operation_stats.csv", csv); // Reset statistics for a new batch of operations stats.Reset(); // Get statistics for a specific operation type var transcodeStats = OperationStats.GetStatistics(OperationType.Transcode); var watermarkStats = OperationStats.GetStatistics(OperationType.Watermark); // Get all stored statistics var allOperationStats = OperationStats.GetAllStatistics(); foreach (var operationStat in allOperationStats) { Console.WriteLine($"{operationStat.Type}: {operationStat.SuccessfulOperations} successes, " + $"{operationStat.FailedOperations} failures"); } ``` ## ProgressReport The `ProgressReport` class provides detailed progress tracking information for FFmpeg operations, including completion percentage, items processed, timing metrics, throughput rates, and status messages. It is used by the `ProgressTracker` to report operation status during video processing workflows. Here is an example usage of the `ProgressReport` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System; // Simulate a video processing operation with 1000 frames var progressTracker = new ProgressTracker(totalItems: 1000); // Simulate processing 250 frames (25% complete) for (int i = 0; i < 250; i++) { progressTracker.ReportItemProgress($"Processing frame {i + 1}"); } // Get the current progress report var progressReport = progressTracker.GetProgressReport(); Console.WriteLine($"Progress: {progressReport.ProgressPercentage:F1}%"); Console.WriteLine($"Items completed: {progressReport.ItemsCompleted}/{progressReport.TotalItems}"); Console.WriteLine($"Status: {progressReport.StatusMessage}"); Console.WriteLine($"Elapsed time: {progressReport.ElapsedTime}"); Console.WriteLine($"ETA: {progressReport.EstimatedTimeRemaining}"); Console.WriteLine($"Throughput: {progressReport.ThroughputItemsPerSecond:F2} items/sec"); Console.WriteLine($"Throughput: {progressReport.ThroughputBytesPerSecond / 1024 / 1024:F2} MB/sec"); // Reset for a new operation progressTracker.Reset(totalItems: 500); // Alternative: track by bytes processed var byteProgressTracker = new ProgressTracker(totalBytes: 1000000000); // 1GB byteProgressTracker.ReportBytesProgress(250000000, "Processing 250MB"); // 250MB var byteProgressReport = byteProgressTracker.GetProgressReport(); Console.WriteLine($"Bytes processed: {byteProgressReport.ThroughputBytesPerSecond / 1024 / 1024:F2} MB/sec"); ``` ## FileUtilities The `FileUtilities` class provides a collection of static methods for safe file system operations, including path validation, file accessibility checks, temporary file management, and format compatibility verification. It includes comprehensive security checks to prevent directory traversal attacks and handles edge cases like locked files, invalid paths, and permission issues gracefully. Here is an example usage of the `FileUtilities` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System; // Validate file paths before processing string inputPath = @"/home/user/videos/input.mp4"; string outputPath = @"/home/user/videos/output/processed.mp4"; if (FileUtilities.IsValidInputFile(inputPath)) { Console.WriteLine($"Input file exists and is accessible: {inputPath}"); Console.WriteLine($"File size: {FileUtilities.GetHumanReadableFileSize(FileUtilities.GetFileSize(inputPath))}"); Console.WriteLine($"File extension: {FileUtilities.GetFileExtension(inputPath)}"); } // Validate output path and create directory if needed if (FileUtilities.IsValidOutputPath(outputPath)) { Console.WriteLine($"Output path is valid and writable: {outputPath}"); } // Generate a safe temporary file for intermediate processing string tempFile = FileUtilities.GetTempFilePath(".tmp"); Console.WriteLine($"Temporary file created: {tempFile}"); // Sanitize filenames for user-provided input string userFileName = "My Video #1!@#.mp4"; string safeFileName = FileUtilities.SanitizeFileName(userFileName); Console.WriteLine($"Original: {userFileName}"); Console.WriteLine($"Sanitized: {safeFileName}"); // Output: My_Video__1____.mp4 // Check if two files have compatible formats for merging string file1 = @"/videos/video1.mp4"; string file2 = @"/videos/video2.mp4"; bool formatsCompatible = FileUtilities.AreFormatsCompatible(file1, file2); Console.WriteLine($"Formats compatible: {formatsCompatible}"); // Output: True // Safely delete files that might be locked bool deleted = FileUtilities.SafeDeleteFile(@"/tmp/old-temp-file.tmp"); Console.WriteLine($"File deleted successfully: {deleted}"); // Get human-readable file sizes for logging long fileSize = 157286400; // 150MB Console.WriteLine($"File size: {FileUtilities.GetHumanReadableFileSize(fileSize)}"); // Output: 150 MB ``` ## MediaProbeService The `MediaProbeService` class probes media files using `ffprobe` and returns a strongly-typed result containing information about its duration, bitrate, and streams. It provides an efficient way to analyze media file properties without manual parsing of command-line output. Here is an example usage of the `MediaProbeService` class with its public members: ```csharp using FFmpegDotnetWrapper.Services; using System; // Initialize the service var probeService = new MediaProbeService(); // Probe a media file var probeResult = probeService.Probe(@"/videos/input.mp4"); Console.WriteLine($"Duration: {probeResult.Duration}"); Console.WriteLine($"Bitrate: {probeResult.Bitrate} bps"); // Iterate through media streams foreach (var stream in probeResult.Streams) { Console.WriteLine($"Stream: {stream.Codec}, " + $"Resolution: {stream.Width}x{stream.Height}, " + $"Channels: {stream.Channels}"); } ``` ## ApplicationStartupJsonExtensions The `ApplicationStartupJsonExtensions` class provides JSON serialization and deserialization extensions for the `ApplicationStartup` configuration record. It enables converting application startup configurations to and from JSON strings, making it easy to save and load configuration settings for the FFmpeg wrapper library. Here is an example usage of the `ApplicationStartupJsonExtensions` class with its public members: ```csharp using FFmpegDotnetWrapper.Configuration; using System; // Create an ApplicationStartup configuration with custom settings var startupConfig = new ApplicationStartup { FFmpegPath = @"/usr/bin/ffmpeg", FFprobePath = @"/usr/bin/ffprobe", EnableHardwareAcceleration = true, EncodingPreset = "fast", VerboseLogging = true, AllowConcurrentOperations = true, MaxConcurrentOperations = 4, OperationTimeoutSeconds = 1200, MaxFileSizeBytes = 100L * 1024 * 1024 * 1024, // 100GB KeepTemporaryFiles = false, TemporaryDirectory = @"/tmp/ffmpeg", SupportedFormats = ["mp4", "mkv", "webm"], RetryAttempts = 3, RetryDelayMs = 2000 }; // Serialize the configuration to JSON string jsonConfig = startupConfig.ToJson(indented: true); Console.WriteLine("Serialized configuration:"); Console.WriteLine(jsonConfig); // Deserialize from JSON back to an ApplicationStartup instance string jsonInput = @"{ \"ffmpegPath\": \"/usr/local/bin/ffmpeg\", \"ffprobePath\": \"/usr/local/bin/ffprobe\", \"enableHardwareAcceleration\": true, \"encodingPreset\": \"medium\", \"verboseLogging\": false, \"allowConcurrentOperations\": true, \"maxConcurrentOperations\": 2, \"operationTimeoutSeconds\": 900, \"maxFileSizeBytes\": 53687091200, \"keepTemporaryFiles\": false, \"temporaryDirectory\": \"/var/tmp/ffmpeg\", \"supportedFormats\": [\"mp4\", \"mkv\", \"webm\"], \"retryAttempts\": 2, \"retryDelayMs\": 1500 }"; var deserializedConfig = ApplicationStartupJsonExtensions.FromJson(jsonInput); if (deserializedConfig != null) { Console.WriteLine($"\nDeserialized configuration:"); Console.WriteLine($"FFmpeg Path: {deserializedConfig.FFmpegPath}"); Console.WriteLine($"FFprobe Path: {deserializedConfig.FFprobePath}"); Console.WriteLine($"Hardware Acceleration: {deserializedConfig.EnableHardwareAcceleration}"); Console.WriteLine($"Encoding Preset: {deserializedConfig.EncodingPreset}"); Console.WriteLine($"Verbose Logging: {deserializedConfig.VerboseLogging}"); Console.WriteLine($"Max Concurrent Operations: {deserializedConfig.MaxConcurrentOperations}"); Console.WriteLine($"Operation Timeout: {deserializedConfig.OperationTimeoutSeconds}s"); Console.WriteLine($"Supported Formats: {string.Join(", ", deserializedConfig.SupportedFormats)}"); } // Use TryFromJson for safe deserialization with error handling if (ApplicationStartupJsonExtensions.TryFromJson(jsonInput, out var safeConfig)) { Console.WriteLine("\nSafe deserialization succeeded!"); } else { Console.WriteLine("Safe deserialization failed - invalid JSON"); } ``` ## ProcessUtilities The `ProcessUtilities` class provides static methods for executing external processes with comprehensive output capture, timeout management, and error handling. It's designed for running command-line tools like FFmpeg and FFprobe safely, with support for both synchronous and asynchronous execution, progress tracking, and argument escaping to prevent command injection. Here is an example usage of the `ProcessUtilities` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System; using System.Threading.Tasks; // Check if FFmpeg is available in the system PATH bool ffmpegAvailable = ProcessUtilities.IsExecutableAvailable("ffmpeg"); Console.WriteLine($"FFmpeg available: {ffmpegAvailable}"); // Execute FFmpeg synchronously to get media information var mediaInfoResult = ProcessUtilities.ExecuteProcess( "ffmpeg", "-i input.mp4 -hide_banner -f null -", timeout: TimeSpan.FromSeconds(30) ); Console.WriteLine($"Exit code: {mediaInfoResult.ExitCode}"); Console.WriteLine($"Execution time: {mediaInfoResult.ExecutionTime}"); Console.WriteLine($"Timed out: {mediaInfoResult.TimedOut}"); if (mediaInfoResult.Success) { // Process succeeded Console.WriteLine("FFmpeg executed successfully"); if (!string.IsNullOrEmpty(mediaInfoResult.StandardOutput)) { // Parse output for media info Console.WriteLine(mediaInfoResult.StandardOutput); } } else { // Process failed or timed out Console.WriteLine("FFmpeg failed:"); Console.WriteLine(mediaInfoResult.StandardError); } // Execute FFmpeg asynchronously with cancellation support var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); var asyncResult = await ProcessUtilities.ExecuteProcessAsync( "ffmpeg", "-i input.mp4 -c:v libx264 -preset fast -b:v 5000k output.mp4", workingDirectory: "/output", timeout: TimeSpan.FromMinutes(10), cancellationToken: cts.Token ); Console.WriteLine($"Async execution completed in: {asyncResult.ExecutionTime}"); Console.WriteLine($"Frames processed: {asyncResult.StandardOutput.Split('\n').Count(line => line.Contains("frame="))}"); // Extract progress percentage from FFmpeg output (requires estimated total frames) string ffmpegOutput = asyncResult.StandardOutput; long estimatedFrames = 1000; // Would typically be calculated from input file double progressPercentage = ProcessUtilities.ExtractProgressPercentage(ffmpegOutput, estimatedFrames); Console.WriteLine($"Progress: {progressPercentage:F1}%"); // Safely escape command-line arguments to prevent injection string inputFile = "/path with spaces/input.mp4"; string outputFile = "/output/result.mp4"; string escapedCommand = $"-i {ProcessUtilities.EscapeArgument(inputFile)} -o {ProcessUtilities.EscapeArgument(outputFile)}"; Console.WriteLine($"Safe command: {escapedCommand}"); ``` ## ExtensionMethods The `ExtensionMethods` class provides a collection of extension methods that enhance standard .NET types with additional functionality. These methods improve code readability, reduce boilerplate, and provide convenient utilities for string manipulation, collection operations, time formatting, and file path handling throughout the FFmpeg wrapper library. Here is an example usage of the `ExtensionMethods` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System.Text; using System.Linq; // String manipulation extensions var message = new StringBuilder(); message.AppendArgument("-i"); message.AppendArgument("/input/video.mp4"); message.AppendArguments("-c:v", "libx264", "-preset", "fast"); Console.WriteLine(message.ToString()); // Output: -i /input/video.mp4 -c:v libx264 -preset fast var text = "Hello"; Console.WriteLine(text.Repeat(3)); // Output: HelloHelloHello var items = new[] { "apple", "banana", "cherry" }; Console.WriteLine(items.Join(", ")); // Output: apple, banana, cherry Console.WriteLine(items.Join(x => x.ToUpper(), " | ")); // Output: APPLE | BANANA | CHERRY // String validation extensions var emptyString = ""; Console.WriteLine(emptyString.IsNullOrWhiteSpace()); // Output: True Console.WriteLine(emptyString.HasValue()); // Output: False // Collection extensions var numbers = new[] { 1, 2, 3, 4, 5, 6 }; var batches = numbers.Batch(2); foreach (var batch in batches) { Console.WriteLine(string.Join(", ", batch)); } // Output: // 1, 2 // 3, 4 // 5, 6 // Time and duration extensions var duration = TimeSpan.FromSeconds(95.5); Console.WriteLine(duration.FormatAsTime()); // Output: 01:35 Console.WriteLine(duration.ToSeconds()); // Output: 95.5 Console.WriteLine(duration.ToMilliseconds()); // Output: 95500 var timeString = "00:02:30"; var parsedSeconds = timeString.TryParseTime(); Console.WriteLine(parsedSeconds); // Output: 150 // File path extensions var filePath = @"/home/user/videos/movie.mp4"; Console.WriteLine(filePath.GetFileName()); // Output: movie.mp4 Console.WriteLine(filePath.GetDirectoryPath()); // Output: /home/user/videos Console.WriteLine(filePath.GetFileExtension()); // Output: mp4 // Size and bitrate formatting Console.WriteLine(1024L.FormatAsSize()); // Output: 1.00 KB Console.WriteLine(5000.FormatAsBitrate()); // Output: 5000 kbps ``` ## ValidationUtilities The `ValidationUtilities` class provides static methods for validating FFmpeg-related parameters such as bitrates, codecs, output formats, resolutions, frame rates, and various media settings. It includes validation for video files, time parsing/formatting, quality settings, watermark positioning, and aspect ratios, ensuring that all parameters passed to FFmpeg operations are valid and compatible. Here is an example usage of the `ValidationUtilities` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System; // Validate video file paths and formats string inputFile = @"/home/user/videos/input.mp4"; string outputFile = @"/home/user/videos/output/processed.mp4"; if (ValidationUtilities.IsValidVideo(inputFile)) { Console.WriteLine($"Valid video file: {inputFile}"); Console.WriteLine($"Video duration: {ValidationUtilities.ParseTimeToSeconds(ValidationUtilities.GetVideoDuration(inputFile))} seconds"); } // Validate FFmpeg codecs string videoCodec = "libx264"; string audioCodec = "aac"; if (ValidationUtilities.IsValidCodec(videoCodec)) { Console.WriteLine($"Valid video codec: {videoCodec}"); } if (ValidationUtilities.IsValidCodec(audioCodec)) { Console.WriteLine($"Valid audio codec: {audioCodec}"); } // Validate output format string outputFormat = "mp4"; if (ValidationUtilities.IsValidOutputFormat(outputFormat)) { Console.WriteLine($"Valid output format: {outputFormat}"); } // Validate resolution int width = 1920; int height = 1080; if (ValidationUtilities.IsValidResolution(width, height)) { Console.WriteLine($"Valid resolution: {ValidationUtilities.FormatResolution(width, height)}"); } // Validate frame rate if (ValidationUtilities.IsValidFrameRate(30.0)) { Console.WriteLine("Valid frame rate: 30 fps"); } // Validate bitrate if (ValidationUtilities.IsValidBitrate(5000)) { Console.WriteLine("Valid bitrate: 5000 kbps"); } // Validate quality setting if (ValidationUtilities.IsValidQualitySetting(23)) { Console.WriteLine("Valid quality setting: 23"); } // Validate aspect ratio if (ValidationUtilities.IsValidAspectRatio(16, 9)) { Console.WriteLine("Valid aspect ratio: 16:9"); } // Validate and parse time strings string timeString = "00:02:30"; double? seconds = ValidationUtilities.ParseTimeToSeconds(timeString); if (seconds.HasValue) { Console.WriteLine($"Parsed time: {timeString} -> {seconds.Value} seconds"); Console.WriteLine($"Formatted back: {ValidationUtilities.FormatSecondsToTime(seconds.Value)}"); } // Validate trim times string startTime = "00:00:10"; string endTime = "00:01:45"; if (ValidationUtilities.ValidateTrimTimes(startTime, endTime)) { Console.WriteLine($"Valid trim range: {startTime} to {endTime}"); } // Get supported codecs and formats Console.WriteLine("Supported video codecs:"); foreach (var codec in ValidationUtilities.GetSupportedCodecs()) { Console.WriteLine($" - {codec}"); } Console.WriteLine("\nSupported output formats:"); foreach (var format in ValidationUtilities.GetSupportedFormats()) { Console.WriteLine($" - {format}"); } // Validate watermark settings if (ValidationUtilities.IsValidWatermarkPosition(10, 10)) { Console.WriteLine("Valid watermark position: (10, 10)"); } if (ValidationUtilities.IsValidWatermarkScale(0.5)) { Console.WriteLine("Valid watermark scale: 0.5"); } if (ValidationUtilities.IsValidOpacity(0.75)) { Console.WriteLine("Valid opacity: 0.75"); } ``` ## CliOutputFormatterValidation The `CliOutputFormatterValidation` class provides validation helpers for CLI output formatting operations. It validates constructor parameters and ensures formatting methods receive valid inputs, preventing runtime errors when configuring console output formatting for FFmpeg operations. Here is an example usage of the `CliOutputFormatterValidation` class with its public members: ```csharp using FFmpegDotnetWrapper.Cli; using System; // Validate console width for formatting operations var consoleWidthValidation = CliOutputFormatterValidation.ValidateConsoleWidth(120); if (consoleWidthValidation.Count == 0) { Console.WriteLine("Console width 120 is valid"); } else { foreach (var problem in consoleWidthValidation) { Console.WriteLine($"Validation error: {problem}"); } } // Validate percentage values for progress bars (0-100 range) var percentageValidation = CliOutputFormatterValidation.ValidatePercentage(75.5); if (percentageValidation.Count == 0) { Console.WriteLine("Percentage 75.5% is valid"); } // Validate width parameters var widthValidation = CliOutputFormatterValidation.ValidateWidth(80); if (widthValidation.Count == 0) { Console.WriteLine("Width 80 is valid"); } // Validate use colors flag (always valid - returns empty list) var colorsValidation = CliOutputFormatterValidation.ValidateUseColors(true); if (colorsValidation.Count == 0) { Console.WriteLine("Use colors flag is valid"); } // Validate string lists to ensure no null or empty entries var stringList = new[] { "item1", "item2", "item3" }; var stringListValidation = CliOutputFormatterValidation.ValidateStringList(stringList, "items"); if (stringListValidation.Count == 0) { Console.WriteLine("String list is valid"); } // Validate message strings var messageValidation = CliOutputFormatterValidation.ValidateMessage("Processing complete"); if (messageValidation.Count == 0) { Console.WriteLine("Message is valid"); } // Example: Using validation in a real formatting scenario public class ConsoleOutputFormatter { private readonly int _consoleWidth; private readonly bool _useColors; public ConsoleOutputFormatter(int consoleWidth = 80, bool useColors = true) { var widthProblems = CliOutputFormatterValidation.ValidateConsoleWidth(consoleWidth); var colorsProblems = CliOutputFormatterValidation.ValidateUseColors(useColors); if (widthProblems.Count > 0) { throw new ArgumentOutOfRangeException(nameof(consoleWidth), string.Join(" ", widthProblems)); } _consoleWidth = consoleWidth; _useColors = useColors; } public void WriteProgress(double percentage, string message) { var percentageProblems = CliOutputFormatterValidation.ValidatePercentage(percentage); var messageProblems = CliOutputFormatterValidation.ValidateMessage(message); if (percentageProblems.Count > 0) { throw new ArgumentOutOfRangeException(nameof(percentage), string.Join(" ", percentageProblems)); } if (messageProblems.Count > 0) { throw new ArgumentException(string.Join(" ", messageProblems)); } // Format progress bar based on validated values Console.WriteLine($"[{percentage:F1}%] {message}"); } } // Usage example var formatter = new ConsoleOutputFormatter(consoleWidth: 100, useColors: true); formatter.WriteProgress(45.2, "Processing video file..."); ``` ## ConcatenationBuilderTests The `ConcatenationBuilderTests` class provides unit tests for the `ConcatenationBuilder` class, verifying that video concatenation operations work correctly with various configurations including segment management, transitions, trimming, and error handling scenarios. Here is an example usage of the `ConcatenationBuilderTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Builders; using FFmpegDotnetWrapper.Models; using Xunit; // Test basic segment addition and ordering var builder = new ConcatenationBuilder(); builder.Add("video1.mp4"); builder.Add("video2.mp4"); builder.Add("video3.mp4"); // Verify segments were added in correct order Assert.Equal(3, builder.Segments.Count); Assert.Equal("video1.mp4", builder.Segments[0].Path); Assert.Equal("video2.mp4", builder.Segments[1].Path); Assert.Equal("video3.mp4", builder.Segments[2].Path); // Test segment insertion at specific position builder.Insert(1, "video1_5.mp4"); Assert.Equal(4, builder.Segments.Count); Assert.Equal("video1_5.mp4", builder.Segments[1].Path); // Test segment removal builder.Remove("video2.mp4"); Assert.Equal(3, builder.Segments.Count); Assert.DoesNotContain(s => s.Path == "video2.mp4", builder.Segments); // Test adding segments with trim parameters builder.Add("long_video.mp4", trimStart: TimeSpan.FromSeconds(10), trimEnd: TimeSpan.FromSeconds(60)); Assert.Single(builder.Segments.Where(s => s.TrimStart.HasValue && s.TrimEnd.HasValue)); // Test adding transition between segments builder.WithTransition(TimeSpan.FromSeconds(2.5)); Assert.Equal(TimeSpan.FromSeconds(2.5), builder.Transition.Duration); // Test fluent API chaining var settings = new ConcatenationSettings { OutputPath = "/output/merged.mp4", VideoCodec = "libx264", AudioCodec = "aac" }; var result = builder .WithTransition(TimeSpan.FromSeconds(1.5)) .WithReencode() .Build(settings); Assert.Equal("/output/merged.mp4", result.OutputPath); Assert.Equal("libx264", result.VideoCodec); Assert.Equal("aac", result.AudioCodec); Assert.True(result.TranscodeOnMerge); // Test reset functionality builder.Reset(); Assert.Empty(builder.Segments); Assert.Null(builder.Transition); ``` ## ValidationUtilitiesTests The `ValidationUtilitiesTests` class provides unit tests for the `ValidationUtilities` class, verifying that validation methods work correctly with various FFmpeg parameters including bitrates, codecs, output formats, resolutions, time parsing, and trim validation scenarios. Here is an example usage of the `ValidationUtilitiesTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using Xunit; // Test bitrate validation [Fact] public void IsValidBitrate_WithinRange_ReturnsTrue() { Assert.True(ValidationUtilities.IsValidBitrate(5000)); Assert.True(ValidationUtilities.IsValidBitrate(1000)); Assert.True(ValidationUtilities.IsValidBitrate(100000)); } [Fact] public void IsValidBitrate_OutsideRange_ReturnsFalse() { Assert.False(ValidationUtilities.IsValidBitrate(50)); Assert.False(ValidationUtilities.IsValidBitrate(100001)); Assert.False(ValidationUtilities.IsValidBitrate(-1)); } // Test codec validation [Fact] public void IsValidCodec_SupportedCodec_ReturnsTrue() { Assert.True(ValidationUtilities.IsValidCodec("libx264")); Assert.True(ValidationUtilities.IsValidCodec("aac")); Assert.True(ValidationUtilities.IsValidCodec("h264")); } [Fact] public void IsValidCodec_UnsupportedOrEmpty_ReturnsFalse() { Assert.False(ValidationUtilities.IsValidCodec("")); Assert.False(ValidationUtilities.IsValidCodec("invalid-codec")); Assert.False(ValidationUtilities.IsValidCodec(null)); } // Test output format validation [Fact] public void IsValidOutputFormat_SupportedFormat_ReturnsTrue() { Assert.True(ValidationUtilities.IsValidOutputFormat("mp4")); Assert.True(ValidationUtilities.IsValidOutputFormat("mov")); Assert.True(ValidationUtilities.IsValidOutputFormat("mkv")); } [Fact] public void IsValidOutputFormat_UnrecognizedFormat_ReturnsFalse() { Assert.False(ValidationUtilities.IsValidOutputFormat("invalid-format")); Assert.False(ValidationUtilities.IsValidOutputFormat("")); } // Test time parsing and formatting [Fact] public void ParseTimeToSeconds_HhMmSsFormat_ReturnsCorrectSeconds() { Assert.Equal(90, ValidationUtilities.ParseTimeToSeconds("00:01:30")); Assert.Equal(3661, ValidationUtilities.ParseTimeToSeconds("01:01:01")); Assert.Equal(0, ValidationUtilities.ParseTimeToSeconds("00:00:00")); } [Fact] public void ParseTimeToSeconds_PureSecondsString_ReturnsValue() { Assert.Equal(125, ValidationUtilities.ParseTimeToSeconds("125")); Assert.Equal(0, ValidationUtilities.ParseTimeToSeconds("0")); } [Fact] public void ParseTimeToSeconds_InvalidOrEmpty_ReturnsNull() { Assert.Null(ValidationUtilities.ParseTimeToSeconds("")); Assert.Null(ValidationUtilities.ParseTimeToSeconds("invalid")); Assert.Null(ValidationUtilities.ParseTimeToSeconds("1:2:3")); } [Fact] public void FormatSecondsToTime_VariousValues_ReturnsHhMmSs() { Assert.Equal("00:01:30", ValidationUtilities.FormatSecondsToTime(90)); Assert.Equal("01:01:01", ValidationUtilities.FormatSecondsToTime(3661)); Assert.Equal("00:00:00", ValidationUtilities.FormatSecondsToTime(0)); } [Fact] public void FormatSecondsToTime_NegativeSeconds_ClampsToZero() { Assert.Equal("00:00:00", ValidationUtilities.FormatSecondsToTime(-1)); Assert.Equal("00:00:00", ValidationUtilities.FormatSecondsToTime(-100)); } // Test resolution validation [Fact] public void IsValidResolution_ValidFormat_ReturnsTrue() { Assert.True(ValidationUtilities.IsValidResolution(1920, 1080)); Assert.True(ValidationUtilities.IsValidResolution(1280, 720)); Assert.True(ValidationUtilities.IsValidResolution(640, 480)); } [Fact] public void IsValidResolution_InvalidFormat_ReturnsFalse() { Assert.False(ValidationUtilities.IsValidResolution(0, 1080)); Assert.False(ValidationUtilities.IsValidResolution(1920, 0)); Assert.False(ValidationUtilities.IsValidResolution(-1, -1)); } // Test trim time validation [Fact] public void ValidateTrimTimes_StartBeforeEnd_ReturnsTrue() { Assert.True(ValidationUtilities.ValidateTrimTimes("00:00:10", "00:01:45")); Assert.True(ValidationUtilities.ValidateTrimTimes("00:00:00", "00:00:01")); } [Fact] public void ValidateTrimTimes_StartGreaterThanEnd_ReturnsFalse() { Assert.False(ValidationUtilities.ValidateTrimTimes("00:01:45", "00:00:10")); Assert.False(ValidationUtilities.ValidateTrimTimes("00:00:10", "00:00:05")); } [Fact] public void ValidateTrimTimes_NegativeStart_ReturnsFalse() { Assert.False(ValidationUtilities.ValidateTrimTimes("-00:00:10", "00:01:45")); Assert.False(ValidationUtilities.ValidateTrimTimes("-10", "00:01:45")); } [Fact] public void ValidateTrimTimes_WithDurationOnly_ReturnsTrue() { Assert.True(ValidationUtilities.ValidateTrimTimes("00:00:10", null, "00:01:35")); Assert.True(ValidationUtilities.ValidateTrimTimes("00:00:00", null, "00:05:00")); } [Fact] public void ValidateTrimTimes_NoEndOrDuration_ReturnsFalse() { Assert.False(ValidationUtilities.ValidateTrimTimes("00:00:10", null, null)); Assert.False(ValidationUtilities.ValidateTrimTimes("00:00:00", null, null)); } // Test watermark scale validation [Fact] public void IsValidWatermarkScale_ValidRange_ReturnsTrue() { Assert.True(ValidationUtilities.IsValidWatermarkScale(0.1)); Assert.True(ValidationUtilities.IsValidWatermarkScale(0.5)); Assert.True(ValidationUtilities.IsValidWatermarkScale(0.9)); } [Fact] public void IsValidWatermarkScale_OutsideRange_ReturnsFalse() { Assert.False(ValidationUtilities.IsValidWatermarkScale(0.0)); Assert.False(ValidationUtilities.IsValidWatermarkScale(1.0)); Assert.False(ValidationUtilities.IsValidWatermarkScale(1.1)); Assert.False(ValidationUtilities.IsValidWatermarkScale(-0.1)); } ``` ## TranscodeServiceTests The `TranscodeServiceTests` class provides unit tests for the `TranscodeService` class, verifying that video transcoding operations work correctly with various configurations including web transcoding presets, bitrate adjustments, resolution changes, and error handling scenarios. Here is an example usage of the `TranscodeServiceTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Services; using FFmpegDotnetWrapper.Models; using Xunit; // Test web transcoding with standard HLS settings var transcodeService = new TranscodeService(); var webResult = await transcodeService.TranscodeToWebAsync( inputPath: "/input/video.mp4", outputDirectory: "/output/hls", preset: WebPreset.Hls_720p ); Assert.True(webResult.Success); // Test bitrate adjustment with validation var bitrateResult = await transcodeService.TranscodeWithBitrateAsync( inputPath: "/input/video.mp4", outputPath: "/output/bitrate.mp4", bitrateKbps: 3000, videoCodec: "libx264" ); Assert.True(bitrateResult.Success); // Test video resizing with validation var resizeResult = await transcodeService.ResizeVideoAsync( inputPath: "/input/video.mp4", outputPath: "/output/resized.mp4", width: 1280, height: 720, keepAspectRatio: true ); Assert.True(resizeResult.Success); // Test audio extraction var audioResult = await transcodeService.ExtractAudioAsync( inputPath: "/input/video.mp4", outputPath: "/output/audio.mp3" ); Assert.True(audioResult.Success); // Test error handling for invalid bitrate await Assert.ThrowsAsync(() => transcodeService.TranscodeWithBitrateAsync( inputPath: "/input/video.mp4", outputPath: "/output/invalid.mp4", bitrateKbps: 50, videoCodec: "libx264" ) ); // Test error handling for zero dimensions await Assert.ThrowsAsync(() => transcodeService.ResizeVideoAsync( inputPath: "/input/video.mp4", outputPath: "/output/invalid.mp4", width: 0, height: 0 ) ); // Test error handling for non-video input in audio extraction await Assert.ThrowsAsync(() => transcodeService.ExtractAudioAsync( inputPath: "/input/audio.mp3", outputPath: "/output/audio.mp3" ) ); ``` ## ThumbnailSettingsTests The `ThumbnailSettingsTests` class provides unit tests for the `ThumbnailSettings` class, verifying that thumbnail extraction configurations work correctly with various settings including count, quality, dimensions, timestamps, and validation scenarios. Here is an example usage of the `ThumbnailSettingsTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FluentAssertions; using Xunit; // Create a new ThumbnailSettings instance with default values var settings = new ThumbnailSettings(); // Verify default values settings.Count.Should().Be(1); settings.Format.Should().Be(ThumbnailFormat.Jpeg); settings.Times.Should().BeEmpty(); settings.Width.Should().BeNull(); settings.Height.Should().BeNull(); settings.JpegQuality.Should().Be(2); // Set valid count value var settingsWithCount = new ThumbnailSettings { Count = 10 }; settingsWithCount.Count.Should().Be(10); // Set valid JPEG quality value var settingsWithQuality = new ThumbnailSettings { JpegQuality = 15 }; settingsWithQuality.JpegQuality.Should().Be(15); // Add explicit timestamps for thumbnail extraction var settingsWithTimestamps = new ThumbnailSettings(); settingsWithTimestamps.Times.Add(TimeSpan.FromSeconds(10)); settingsWithTimestamps.Times.Add(TimeSpan.FromSeconds(30)); settingsWithTimestamps.Times.Add(TimeSpan.FromSeconds(60)); // Set specific dimensions for thumbnails var settingsWithDimensions = new ThumbnailSettings { Width = 640, Height = 480 }; settingsWithDimensions.Width.Should().Be(640); settingsWithDimensions.Height.Should().Be(480); // Set auto width (negative value) with specific height var settingsWithAutoWidth = new ThumbnailSettings { Width = -1, Height = 720 }; // Clone settings to create an independent copy var originalSettings = new ThumbnailSettings { Count = 5, Format = ThumbnailFormat.Png, Width = 640, Height = 360 }; originalSettings.Times.Add(TimeSpan.FromSeconds(10)); var clonedSettings = originalSettings.Clone(); // Verify clone has same values clonedSettings.Count.Should().Be(5); clonedSettings.Format.Should().Be(ThumbnailFormat.Png); clonedSettings.Width.Should().Be(640); clonedSettings.Times.Should().HaveCount(1); // Mutations on clone should not affect original clonedSettings.Times.Add(TimeSpan.FromSeconds(20)); originalSettings.Times.Should().HaveCount(1); ``` ## FFmpegServiceBenchmarksExtensions The `FFmpegServiceBenchmarksExtensions` class provides extension methods for the `FFmpegServiceBenchmarks` class that enhance benchmarking capabilities with batch operations, validation utilities, statistics analysis, and performance comparison tools. It's designed to facilitate automated benchmark execution, validation, and performance regression detection in CI/CD pipelines. Here is an example usage of the `FFmpegServiceBenchmarksExtensions` class with its public members: ```csharp using BenchmarkDotNet.Running; using FFmpegDotnetWrapper.Benchmarks; using FFmpegDotnetWrapper.Models; // Create a benchmarks instance var benchmarks = BenchmarkRunner.Run(); // Create a batch of benchmark runs for comparison testing var batchResults = await benchmarks.CreateBenchmarkBatch( inputPath: @"/videos/sample.mp4", outputDirectory: @"/benchmarks/output", videoCodec: VideoCodec.H265, audioCodec: AudioCodec.AAC, iterations: 5 ); foreach (var result in batchResults) { Console.WriteLine($"Iteration {result.Iteration}: {result.VideoCodec} -> {result.OutputPath}"); } // Validate that all benchmark methods can execute successfully var isValid = await benchmarks.ValidateAllBenchmarksAsync(); Console.WriteLine($"All benchmarks valid: {isValid}"); // Get performance statistics for a specific benchmark var stats = benchmarks.GetBenchmarkStatistics(nameof(FFmpegServiceBenchmarks.Transcode_H264_to_H265_MP4)); Console.WriteLine($"Mean execution time: {stats["MeanMs"]} ms"); Console.WriteLine($"Memory allocated: {stats["AllocatedBytes"]} bytes"); // Compare current performance against a baseline var baselineStats = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["MeanMs"] = 1200.0, ["AllocatedBytes"] = 15000000L }; var comparison = stats.CompareBenchmarks(baselineStats); foreach (var metric in comparison.Metrics) { Console.WriteLine($"{metric.MetricName}: {metric.PercentageChange:+#.##%;-#.##%;0.00}% " + $"{(metric.IsRegression ? "(REGRESSION)" : metric.IsImprovement ? "(IMPROVEMENT)" : "")}"); } ``` ## FFmpegEventExtensions The `FFmpegEventExtensions` class provides extension methods for `FFmpegEvent` and its derived types (`OperationStartedEvent`, `OperationCompletedEvent`, `OperationFailedEvent`, `ProgressReportedEvent`). These methods offer convenient utilities for filtering events by type, extracting operation metadata (input/output files, progress, duration, error details), and formatting events for logging or display purposes. Here is an example usage of the `FFmpegEventExtensions` class with its public members: ```csharp using FFmpegDotnetWrapper.Events; using FFmpegDotnetWrapper.Models; // Create sample events for demonstration var startedEvent = new OperationStartedEvent( operationType: "Transcode", inputFile: @"/videos/input.mp4", outputFile: @"/videos/output.mp4", correlationId: "transcode-001" ); var progressEvent = new ProgressReportedEvent( operationType: "Transcode", progressPercentage: 45.5, elapsedTime: TimeSpan.FromSeconds(125.3), correlationId: "transcode-001" ); var completedEvent = new OperationCompletedEvent( operationType: "Transcode", inputFile: @"/videos/input.mp4", outputFile: @"/videos/output.mp4", duration: TimeSpan.FromSeconds(245.8), outputFileSize: 157286400, // 150MB correlationId: "transcode-001" ); var failedEvent = new OperationFailedEvent( operationType: "Transcode", inputFile: @"/videos/input.mp4", outputFile: @"/videos/output.mp4", errorMessage: "Input file not found", errorCode: "FILE_NOT_FOUND", correlationId: "transcode-002" ); // Use extension methods to extract information Console.WriteLine($"Started event is success: {startedEvent.IsSuccess()}"); Console.WriteLine($"Progress event is failure: {progressEvent.IsFailure()}"); Console.WriteLine($"Operation type: {progressEvent.GetOperationType()}"); Console.WriteLine($"Input file: {progressEvent.GetInputFile()}"); Console.WriteLine($"Output file: {completedEvent.GetOutputFile()}"); Console.WriteLine($"Error message: {failedEvent.GetErrorMessage()}"); Console.WriteLine($"Progress percentage: {progressEvent.GetProgressPercentage()}% "); Console.WriteLine($"Duration: {completedEvent.GetDuration()?.TotalSeconds}s"); Console.WriteLine($"Output file size: {completedEvent.GetOutputFileSize() / (1024.0 * 1024.0):F2} MB"); Console.WriteLine($"Error code: {failedEvent.GetErrorCode()}"); // Check correlation IDs Console.WriteLine($"Progress event has correlation ID 'transcode-001': {progressEvent.HasCorrelationId("transcode-001")}"); Console.WriteLine($"Failed event has correlation ID 'transcode-001': {failedEvent.HasCorrelationId("transcode-001")}"); // Format events for logging Console.WriteLine($"\nStarted event log:\n{startedEvent.ToLogString()}"); Console.WriteLine($"\nProgress event log:\n{progressEvent.ToLogString()}"); Console.WriteLine($"\nCompleted event log:\n{completedEvent.ToLogString()}"); Console.WriteLine($"\nFailed event log:\n{failedEvent.ToLogString()}"); // Get metadata string Console.WriteLine($"\nMetadata: {startedEvent.GetMetadataString()}"); ``` ## MediaRepositoryValidation The `MediaRepositoryValidation` class provides validation helpers for `MediaRepository` instances and collections of `MediaFile` objects. It includes methods for validating repository integrity, checking file paths, validating media file properties, and ensuring dates and numeric values are within acceptable ranges. This validation layer helps prevent invalid data from being persisted in the media repository. Here is an example usage of the `MediaRepositoryValidation` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FFmpegDotnetWrapper.Repository; // Create a media repository with some media files var mediaRepository = new MediaRepository(); // Add valid media files mediaRepository.Add(new MediaFile("/videos/video1.mp4") { Name = "Introduction Video", FileSize = 157286400, // 150MB Duration = TimeSpan.FromMinutes(2), Width = 1920, Height = 1080, FrameRate = 30.0, Bitrate = 5000000, CreatedAt = DateTime.UtcNow.AddDays(-1) }); mediaRepository.Add(new MediaFile("/videos/tutorial.mp4") { Name = "Tutorial Video", FileSize = 262144000, // 250MB Duration = TimeSpan.FromMinutes(5), Width = 1280, Height = 720, FrameRate = 60.0, Bitrate = 8000000, CreatedAt = DateTime.UtcNow.AddDays(-2) }); // Validate the entire repository var validationProblems = MediaRepositoryValidation.Validate(mediaRepository); if (validationProblems.Count > 0) { Console.WriteLine("Repository validation failed:"); foreach (var problem in validationProblems) { Console.WriteLine($"- {problem}"); } } else { Console.WriteLine("Repository is valid!"); } // Check if repository is valid without getting detailed problems bool isValid = MediaRepositoryValidation.IsValid(mediaRepository); Console.WriteLine($"Repository valid: {isValid}"); // Validate repository and throw if invalid try { MediaRepositoryValidation.EnsureValid(mediaRepository); Console.WriteLine("Repository passed validation checks"); } catch (ArgumentException ex) { Console.WriteLine($"Repository validation failed: {ex.Message}"); } // Validate individual media files var fileValidationProblems = MediaRepositoryValidation.ValidateMediaFiles(mediaRepository); Console.WriteLine($"Validated {mediaRepository.Count} media files"); // Validate specific properties bool isValidId = MediaRepositoryValidation.IsValidId("video-001"); bool isValidPath = MediaRepositoryValidation.IsValidFilePath("/videos/video1.mp4"); bool isValidDate = MediaRepositoryValidation.IsValidDate(DateTime.UtcNow.AddDays(-1), nameof(mediaRepository)); bool isValidPositiveNumber = MediaRepositoryValidation.IsValidPositiveNumber(157286400, nameof(MediaFile.FileSize)); Console.WriteLine($"ID valid: {isValidId}"); Console.WriteLine($"Path valid: {isValidPath}"); Console.WriteLine($"Date valid: {isValidDate}"); Console.WriteLine($"Positive number valid: {isValidPositiveNumber}"); ``` ## FileUtilitiesTests The `FileUtilitiesTests` class provides unit tests for the `FileUtilities` class, verifying that file path validation, file operations, and utility methods work correctly. It includes tests for validating absolute and relative paths, handling edge cases like null/empty strings, directory traversal attempts, and environment variable expansion, as well as testing file existence checks and extension extraction. Here is an example usage of the `FileUtilitiesTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using FluentAssertions; using Xunit; // Test that absolute file paths are considered valid var absolutePath = Path.GetFullPath(@"/home/user/videos/input.mp4"); Assert.True(FileUtilities.IsValidFilePath(absolutePath)); // Test that relative paths are rejected for security Assert.False(FileUtilities.IsValidFilePath("relative/path/file.mp4")); Assert.False(FileUtilities.IsValidFilePath("../file.mp4")); // Test that null and empty strings are handled gracefully Assert.False(FileUtilities.IsValidFilePath(null)); Assert.False(FileUtilities.IsValidFilePath(string.Empty)); Assert.False(FileUtilities.IsValidFilePath(" ")); // Test that environment variables and tilde expansion are rejected Assert.False(FileUtilities.IsValidFilePath("$HOME/file.mp4")); Assert.False(FileUtilities.IsValidFilePath("~/file.mp4")); // Test input file validation - must exist and be accessible var testFile = @"/home/user/videos/input.mp4"; Assert.True(FileUtilities.IsValidInputFile(testFile)); Assert.False(FileUtilities.IsValidInputFile("/nonexistent/file.mp4")); Assert.False(FileUtilities.IsValidInputFile("relative/path/file.mp4")); // Test output path validation - must be absolute and in writable directory var outputPath = @"/home/user/videos/output/processed.mp4"; Assert.True(FileUtilities.IsValidOutputPath(outputPath)); // Test output path with directory creation var newOutputPath = @"/home/user/videos/newdir/output.mp4"; Assert.True(FileUtilities.IsValidOutputPath(newOutputPath, createDirectoryIfNeeded: true)); Assert.False(FileUtilities.IsValidOutputPath(newOutputPath, createDirectoryIfNeeded: false)); // Test file extension extraction var extension = FileUtilities.GetFileExtension(@"/home/user/videos/input.mp4"); Assert.Equal("mp4", extension); var mkvExtension = FileUtilities.GetFileExtension(@"/home/user/videos/input.mkv"); Assert.Equal("mkv", mkvExtension); // Test file size utilities var fileSize = FileUtilities.GetFileSize(@"/home/user/videos/input.mp4"); Assert.Greater(fileSize, 0); var humanReadable = FileUtilities.GetHumanReadableFileSize(1572864); // 1.5 MB Assert.Equal("1.5 MB", humanReadable); // Test file operations var tempFile = FileUtilities.GetTempFilePath(".tmp"); Assert.True(File.Exists(Path.GetDirectoryName(tempFile))); Assert.Equal(".tmp", Path.GetExtension(tempFile)); // Test file sanitization var unsafeFileName = "video\0copy.mp4"; var safeFileName = FileUtilities.SanitizeFileName(unsafeFileName); Assert.DoesNotContain("\0", safeFileName); Assert.Equal(".mp4", Path.GetExtension(safeFileName)); // Test format compatibility Assert.True(FileUtilities.AreFormatsCompatible(@"/video1.mp4", @"/video2.mp4")); Assert.False(FileUtilities.AreFormatsCompatible(@"/video.mp4", @"/video.mkv")); ``` ## SubtitleSettingsTests The `SubtitleSettingsTests` class provides unit tests for the `SubtitleSettings` class, verifying that subtitle configuration validation works correctly with various settings including file paths, character encoding, font properties, language specification, and validation scenarios. Here is an example usage of the `SubtitleSettingsTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FFmpegDotnetWrapper.Exceptions; using FluentAssertions; // Create a new SubtitleSettings instance with default values var settings = new SubtitleSettings(); // Verify default values settings.HardEmbed.Should().BeFalse(); settings.CharEncoding.Should().Be("UTF-8"); settings.FontName.Should().Be("Arial"); settings.FontSize.Should().Be(24); settings.SubtitleStreamIndex.Should().Be(0); settings.Language.Should().BeNull(); // Set subtitle file path (must exist and have supported extension like .srt or .ass) var subtitlePath = @"/subtitles/english.srt"; settings.SubtitlePath = subtitlePath; settings.SubtitlePath.Should().Be(Path.GetFullPath(subtitlePath)); // Configure subtitle embedding settings var settingsWithEmbedding = new SubtitleSettings { SubtitlePath = subtitlePath, HardEmbed = true, // Embed subtitles directly into video stream CharEncoding = "UTF-8", // Character encoding for subtitle file FontName = "Arial", // Font family to use FontSize = 24, // Font size in pixels Language = "en", // Language code SubtitleStreamIndex = 0 // Stream index for embedded subtitles }; // Validate settings before use (throws if invalid) settingsWithEmbedding.Validate(); // No exception thrown for valid settings // Test validation with invalid font size (too small) var invalidSettings = new SubtitleSettings { SubtitlePath = subtitlePath, FontSize = 5 // Below minimum of 8 }; var act = () => invalidSettings.Validate(); act.Should().Throw() .WithMessage("*FontSize*"); // Test validation with non-existent file var nonexistentSettings = new SubtitleSettings(); var fileAct = () => nonexistentSettings.SubtitlePath = @"/nonexistent/subtitles.srt"; fileAct.Should().Throw() .WithMessage("*does not exist*"); // Clone settings to create an independent copy var originalSettings = new SubtitleSettings { SubtitlePath = subtitlePath, HardEmbed = true, FontSize = 30, Language = "fr" }; var clonedSettings = originalSettings.Clone(); // Verify clone has same values clonedSettings.SubtitlePath.Should().Be(originalSettings.SubtitlePath); clonedSettings.HardEmbed.Should().Be(originalSettings.HardEmbed); clonedSettings.FontSize.Should().Be(originalSettings.FontSize); clonedSettings.Language.Should().Be(originalSettings.Language); // Mutations on clone should not affect original clonedSettings.FontSize = 20; originalSettings.FontSize.Should().Be(30); ``` ## TranscodeSettingsTests The `TranscodeSettingsTests` class provides unit tests for the `TranscodeSettings` class, verifying that transcoding configuration validation works correctly with various settings including video/audio bitrates, frame rates, dimensions, codecs, containers, and validation scenarios. Here is an example usage of the `TranscodeSettingsTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FFmpegDotnetWrapper.Constants; using FFmpegDotnetWrapper.Exceptions; using FluentAssertions; // Create a new TranscodeSettings instance with default values var settings = new TranscodeSettings(); // Verify default values settings.VideoCodec.Should().Be(VideoCodec.H264); settings.AudioCodec.Should().Be(AudioCodec.AAC); settings.Container.Should().Be(ContainerFormat.MP4); settings.VideoBitrate.Should().Be(FFmpegConstants.DefaultBitrate); settings.AudioBitrate.Should().Be(FFmpegConstants.DefaultAudioBitrate); settings.FrameRate.Should().Be(FFmpegConstants.DefaultFrameRate); settings.Quality.Should().Be(QualityPreset.Medium); settings.EnableAutoScale.Should().BeTrue(); settings.PreserveAspectRatio.Should().BeTrue(); settings.TwoPass.Should().BeFalse(); settings.HardwareAcceleration.Should().Be(HwAccel.None); // Configure transcoding settings for H.264 to MP4 var h264Settings = new TranscodeSettings { VideoCodec = VideoCodec.H264, AudioCodec = AudioCodec.AAC, Container = ContainerFormat.MP4, VideoBitrate = 5000, // 5000 kbps AudioBitrate = 192, // 192 kbps FrameRate = 30, Width = 1920, Height = 1080, Quality = QualityPreset.High, EnableAutoScale = true, PreserveAspectRatio = true, TwoPass = false, HardwareAcceleration = HwAccel.NVENC, CustomFFmpegArgs = "-movflags +faststart" }; // Validate settings before use (throws if invalid) h264Settings.Validate(); // No exception thrown for valid settings // Test validation with invalid video bitrate (too low) var invalidBitrateSettings = new TranscodeSettings { VideoBitrate = 50 }; // Below minimum var bitrateAct = () => invalidBitrateSettings.Validate(); bitrateAct.Should().Throw() .WithMessage("*bitrate*"); // Test validation with incompatible codec/container combination var invalidCodecSettings = new TranscodeSettings { VideoCodec = VideoCodec.H264, Container = ContainerFormat.WebM // H.264 not supported in WebM }; var codecAct = () => invalidCodecSettings.Validate(); codecAct.Should().Throw() .WithMessage("*not supported*"); // Test validation with invalid dimensions (zero width) var invalidDimensionsSettings = new TranscodeSettings { Width = 0 }; var dimensionsAct = () => invalidDimensionsSettings.Validate(); dimensionsAct.Should().Throw() .WithMessage("*too small*"); // Clone settings to create an independent copy var originalSettings = new TranscodeSettings { VideoCodec = VideoCodec.VP9, VideoBitrate = 8000, Width = 1280, TwoPass = true }; var clonedSettings = originalSettings.Clone(); // Verify clone has same values clonedSettings.VideoCodec.Should().Be(VideoCodec.VP9); clonedSettings.VideoBitrate.Should().Be(8000); clonedSettings.Width.Should().Be(1280); clonedSettings.TwoPass.Should().BeTrue(); // Mutations on clone should not affect original clonedSettings.VideoBitrate = 6000; originalSettings.VideoBitrate.Should().Be(8000); ``` ## MediaFileTests The `MediaFileTests` class provides unit tests for the `MediaFile` class, verifying that media file properties, constructors, and validation methods work correctly. It includes tests for file path validation, file properties extraction, video validation, metadata storage, and unique identifier generation. Here is an example usage of the `MediaFileTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FluentAssertions; using Xunit; // Create a new MediaFile instance using the default constructor var mediaFile = new MediaFile(); // Verify default values mediaFile.Id.Should().NotBeEmpty(); mediaFile.Name.Should().BeEmpty(); mediaFile.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); mediaFile.Metadata.Should().BeEmpty(); // Create a MediaFile instance from a real file path var mediaFileFromPath = new MediaFile(@"/home/user/videos/sample.mp4"); // Verify file properties are correctly extracted mediaFileFromPath.FilePath.Should().NotBeEmpty(); mediaFileFromPath.Name.Should().Be("sample"); mediaFileFromPath.Extension.Should().Be(".mp4"); mediaFileFromPath.FileSize.Should().BeGreaterThan(0); // Set and validate video metadata mediaFileFromPath.VideoCodec = "h264"; mediaFileFromPath.AudioCodec = "aac"; mediaFileFromPath.FrameRate = 30.0; mediaFileFromPath.Bitrate = 5000000; mediaFileFromPath.Duration = TimeSpan.FromSeconds(125); mediaFileFromPath.Width = 1920; mediaFileFromPath.Height = 1080; // Validate the media file as a valid video var act = () => mediaFileFromPath.ValidateAsVideo(); act.Should().NotThrow(); // Store arbitrary metadata for additional properties mediaFileFromPath.Metadata["encoder"] = "libx264"; mediaFileFromPath.Metadata["profile"] = "High"; mediaFileFromPath.Metadata["created-by"] = "FFmpeg .NET Wrapper"; // Set descriptive properties mediaFileFromPath.Description = "Sample video for testing transcoding operations"; mediaFileFromPath.ModifiedAt = DateTime.UtcNow; // Verify unique ID generation var anotherMediaFile = new MediaFile(@"/home/user/videos/another.mp4"); anotherMediaFile.Id.Should().NotBe(mediaFileFromPath.Id); // Verify FilePath normalizes to absolute path mediaFileFromPath.FilePath.Should().Be(Path.GetFullPath(@"/home/user/videos/sample.mp4")); ``` ## FFmpegOperationTests The `FFmpegOperationTests` class provides unit tests for FFmpeg operations including command line building, conversion results, and service mocking. It verifies that FFmpeg operations can be constructed with input files and arguments, cloned independently, and that conversion results can be marked as successful or failed with appropriate metrics and error messages. Here is an example usage of the `FFmpegOperationTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FFmpegDotnetWrapper.Services; using FluentAssertions; // Create an FFmpeg operation for transcoding var operation = new FFmpegOperation { Name = "Video transcoding", OutputFile = "/output/transcoded.mp4", Type = FFmpegOperationType.Transcode }; // Add input files to the operation operation.AddInputFile("/input/video1.mp4"); operation.AddInputFile("/input/video2.mp4"); // Add additional FFmpeg arguments operation.AddArguments("-c:v", "libx264"); operation.AddArguments("-crf", "23"); operation.AddArguments("-preset", "fast"); // Build the complete FFmpeg command line var commandLine = operation.BuildCommandLine(); Console.WriteLine(commandLine); /* Output: ffmpeg -i "/input/video1.mp4" -i "/input/video2.mp4" -c:v libx264 -crf 23 -preset fast "/output/transcoded.mp4" */ // Clone the operation to create an independent copy var clonedOperation = operation.Clone(); clonedOperation.AddInputFile("/input/video3.mp4"); // Verify original operation is unchanged Console.WriteLine($"Original inputs: {operation.InputFiles.Count}"); // Output: Original inputs: 2 Console.WriteLine($"Cloned inputs: {clonedOperation.InputFiles.Count}"); // Output: Cloned inputs: 3 // Create a conversion result and mark it as successful var result = new ConversionResult(); result.MarkAsSuccess("/output/result.mp4"); // Set metrics on the result result.SetMetric("bitrate", 5000); result.SetMetric("fps", 30); // Retrieve metrics var bitrate = result.GetMetric("bitrate"); var fps = result.GetMetric("fps"); Console.WriteLine($"Bitrate: {bitrate} kbps, FPS: {fps}"); // Output: Bitrate: 5000 kbps, FPS: 30 // Mark a result as failed with an error message var failedResult = new ConversionResult(); failedResult.MarkAsFailed("FFmpeg exited with code 1: invalid codec"); // Generate a summary for logging var summary = failedResult.GenerateSummary(); Console.WriteLine(summary); /* Output: [Failed] FFmpeg exited with code 1: invalid codec */ // Calculate size reduction percentage (returns null if not successful) var sizeReduction = result.GetSizeReductionPercentage(10_000_000); // 10MB input Console.WriteLine($"Size reduction: {sizeReduction}%"); // Output depends on actual file sizes ``` ## FormattingUtilities The `FormattingUtilities` class provides a collection of static formatting methods for consistent string representation of FFmpeg-related data types. It handles time formatting, byte size formatting, bitrate formatting, resolution formatting, and various string sanitization utilities used throughout the library for logging, CLI output, and API responses. Here is an example usage of the `FormattingUtilities` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System; // Format durations for logging and progress reporting var duration = TimeSpan.FromSeconds(3725); // 1 hour, 2 minutes, 5 seconds Console.WriteLine(FormattingUtilities.FormatDuration(duration)); // Output: 01:02:05 var shortDuration = TimeSpan.FromSeconds(95); // Less than 1 hour Console.WriteLine(FormattingUtilities.FormatDuration(shortDuration)); // Output: 00:01:35 // Format byte sizes for file size display Console.WriteLine(FormattingUtilities.FormatBytes(1024)); // Output: 1 KB Console.WriteLine(FormattingUtilities.FormatBytes(1572864)); // Output: 1.5 MB Console.WriteLine(FormattingUtilities.FormatBytes(2147483648)); // Output: 2 GB // Format bitrates for encoding settings Console.WriteLine(FormattingUtilities.FormatBitrate(5000)); // Output: 5000 Kbps Console.WriteLine(FormattingUtilities.FormatBitrate(3000000)); // Output: 3 Mbps Console.WriteLine(FormattingUtilities.FormatBitrate(2500000000)); // Output: 2.5 Gbps // Format FFmpeg commands for logging (automatically masks file paths) var ffmpegCommand = FormattingUtilities.FormatFFmpegCommand( "ffmpeg", "-i /home/user/input.mp4 -c:v libx264 -preset fast -b:v 5000k /output/output.mp4" ); Console.WriteLine(ffmpegCommand); /* Output: ffmpeg \ -i \ -c:v libx264 \ -preset fast \ -b:v 5000k \ */ // Parse FFmpeg progress output for display var progressOutput = @"frame= 123: fps= 29.98 q=28.0 size= 123kB time=00:00:04.12 bitrate= 243kbits/s speed=1.21x"'; Console.WriteLine(FormattingUtilities.ExtractProgressSummary(progressOutput)); // Output: Frame: 123 | Speed: 1.21x | FPS: 29.98 | Bitrate: 243kbits/s // Format progress time display (elapsed / estimated) var elapsed = TimeSpan.FromSeconds(125); var estimated = TimeSpan.FromSeconds(500); Console.WriteLine(FormattingUtilities.FormatProgressTime(elapsed, estimated)); // Output: 00:02:05 / 00:08:20 // Calculate and format ETA var progressPercentage = 25.0; // 25% complete Console.WriteLine(FormattingUtilities.FormatETA(elapsed, progressPercentage)); // Output: ~00:06:15 remaining // Format timestamps for logging Console.WriteLine(FormattingUtilities.FormatTimestamp(DateTime.Now)); // Output: 2026-07-16 14:30:45.123 // Format resolution for video metadata Console.WriteLine(FormattingUtilities.FormatResolution(1920, 1080)); // Output: 1920x1080 Console.WriteLine(FormattingUtilities.FormatResolution(1280, 720)); // Output: 1280x720 // Format percentages for progress display Console.WriteLine(FormattingUtilities.FormatPercentage(25.5)); // Output: 25.5% Console.WriteLine(FormattingUtilities.FormatPercentage(99.99)); // Output: 100.0% // Truncate long strings for display var longPath = @"/home/user/videos/very/long/path/with/many/nested/directories/file-name-that-is-quite-long.mp4"; Console.WriteLine(FormattingUtilities.TruncateString(longPath, 60)); // Output: /home/user/videos/very/long/path/with/many/n...mp4 // Sanitize strings for safe display var unsafeString = "HelloWorld\tLine1\nLine2"; Console.WriteLine(FormattingUtilities.SanitizeForDisplay(unsafeString)); // Output: HelloWorld Line1 Line2 // Convert kebab-case to Title Case Console.WriteLine(FormattingUtilities.TitleCase("output-format")); // Output: Output Format Console.WriteLine(FormattingUtilities.TitleCase("input_file-path")); // Output: Input File Path ``` ## MediaFileTests The `MediaFileTests` class provides unit tests for the `MediaFile` class, verifying that media file properties, constructors, and validation methods work correctly. It includes tests for file path validation, file properties extraction, video validation, metadata storage, and unique identifier generation. Here is an example usage of the `MediaFileTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FluentAssertions; using Xunit; // Create a new MediaFile instance using the default constructor var mediaFile = new MediaFile(); // Verify default values mediaFile.Id.Should().NotBeEmpty(); mediaFile.Name.Should().BeEmpty(); mediaFile.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); mediaFile.Metadata.Should().BeEmpty(); // Create a MediaFile instance from a real file path var mediaFileFromPath = new MediaFile(@"/home/user/videos/sample.mp4"); // Verify file properties are correctly extracted mediaFileFromPath.FilePath.Should().NotBeEmpty(); mediaFileFromPath.Name.Should().Be("sample"); mediaFileFromPath.Extension.Should().Be(".mp4"); mediaFileFromPath.FileSize.Should().BeGreaterThan(0); // Set and validate video metadata mediaFileFromPath.VideoCodec = "h264"; mediaFileFromPath.AudioCodec = "aac"; mediaFileFromPath.FrameRate = 30.0; mediaFileFromPath.Bitrate = 5000000; mediaFileFromPath.Duration = TimeSpan.FromSeconds(125); mediaFileFromPath.Width = 1920; mediaFileFromPath.Height = 1080; // Validate the media file as a valid video var act = () => mediaFileFromPath.ValidateAsVideo(); act.Should().NotThrow(); // Store arbitrary metadata for additional properties mediaFileFromPath.Metadata["encoder"] = "libx264"; mediaFileFromPath.Metadata["profile"] = "High"; mediaFileFromPath.Metadata["created-by"] = "FFmpeg .NET Wrapper"; // Set descriptive properties mediaFileFromPath.Description = "Sample video for testing transcoding operations"; mediaFileFromPath.ModifiedAt = DateTime.UtcNow; // Verify unique ID generation var anotherMediaFile = new MediaFile(@"/home/user/videos/another.mp4"); anotherMediaFile.Id.Should().NotBe(mediaFileFromPath.Id); // Verify FilePath normalizes to absolute path mediaFileFromPath.FilePath.Should().Be(Path.GetFullPath(@"/home/user/videos/sample.mp4")); ``` ## FFmpegServiceIntegrationTests The `FFmpegServiceIntegrationTests` class provides integration tests for the `FFmpegService` class, verifying end-to-end video processing workflows including transcoding, trimming, merging, watermarking, and batch operations. These tests ensure that the FFmpeg wrapper integrates correctly with the actual FFmpeg binary and produces expected results with various configurations and edge cases. Here is an example usage of the `FFmpegServiceIntegrationTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Services; using FFmpegDotnetWrapper.Models; // Create an FFmpeg service instance var ffmpegService = new FFmpegService(); // Test basic transcoding workflow var transcodeResult = await ffmpegService.TranscodeAsync( inputPath: "/input/source.mp4", outputPath: "/output/transcoded.webm", settings: new TranscodeSettings { VideoCodec = VideoCodec.VP9, AudioCodec = AudioCodec.Vorbis, Container = ContainerFormat.WebM, VideoBitrate = 2500, Quality = QualityPreset.Medium } ); Assert.True(transcodeResult.Success); // Test hardware acceleration with NVENC var hwResult = await ffmpegService.TranscodeAsync( inputPath: "/input/source.mp4", outputPath: "/output/hw-accelerated.mp4", settings: new TranscodeSettings { VideoCodec = VideoCodec.H264, HardwareAcceleration = HwAccel.NVENC, VideoBitrate = 5000, Quality = QualityPreset.High } ); Assert.True(hwResult.Success); // Test audio normalization workflow var normalizeResult = await ffmpegService.TranscodeAsync( inputPath: "/input/source.mp4", outputPath: "/output/normalized.mp4", settings: new TranscodeSettings { VideoCodec = VideoCodec.H264, AudioNormalization = true, AudioBitrate = 192, VideoBitrate = 3000 } ); Assert.True(normalizeResult.Success); // Test trimming workflow var trimResult = await ffmpegService.TrimAsync( inputPath: "/input/source.mp4", outputPath: "/output/trimmed.mp4", startTime: TimeSpan.FromSeconds(10), duration: TimeSpan.FromSeconds(30) ); Assert.True(trimResult.Success); // Test trimming to preserve only audio var audioOnlyResult = await ffmpegService.TrimAsync( inputPath: "/input/source.mp4", outputPath: "/output/audio-only.mp3", startTime: TimeSpan.FromSeconds(0), duration: TimeSpan.FromSeconds(60), videoStreamIndex: -1 // Exclude video stream ); Assert.True(audioOnlyResult.Success); // Test merging multiple videos var mergeResult = await ffmpegService.MergeAsync( inputPaths: new List { "/input/video1.mp4", "/input/video2.mp4" }, outputPath: "/output/merged.mp4", transitionDuration: TimeSpan.FromSeconds(2) ); Assert.True(mergeResult.Success); // Test watermarking workflow var watermarkResult = await ffmpegService.WatermarkAsync( inputPath: "/input/source.mp4", outputPath: "/output/watermarked.mp4", watermarkPath: "/watermark.png", positionX: 10, positionY: 10, opacity: 0.3, scale: 0.2 ); Assert.True(watermarkResult.Success); // Test batch processing of multiple files var batchResult = await ffmpegService.ProcessBatchAsync( operations: new List { new BatchOperation { InputPath = "/input/video1.mp4", OutputPath = "/output/processed1.mp4", OperationType = BatchOperationType.Transcode, Settings = new TranscodeSettings { VideoCodec = VideoCodec.H264 } }, new BatchOperation { InputPath = "/input/video2.mp4", OutputPath = "/output/processed2.mp4", OperationType = BatchOperationType.Transcode, Settings = new TranscodeSettings { VideoCodec = VideoCodec.H264 } } }, parallel: true ); Assert.True(batchResult.All(r => r.Success)); // Test error handling with invalid input var invalidResult = await ffmpegService.TranscodeAsync( inputPath: "/input/nonexistent.mp4", outputPath: "/output/output.mp4", settings: new TranscodeSettings { VideoCodec = VideoCodec.H264 } ); Assert.False(invalidResult.Success); Assert.Contains("not found", invalidResult.ErrorMessage); // Test cancellation support var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); var cancelResult = await ffmpegService.TranscodeAsync( inputPath: "/input/large-video.mp4", outputPath: "/output/cancelled.mp4", settings: new TranscodeSettings { VideoCodec = VideoCodec.H264 }, cancellationToken: cts.Token ); Assert.True(cancelResult.TimedOut || !cancelResult.Success); // Test various codec and container combinations var combinations = new List<(VideoCodec, AudioCodec, ContainerFormat)> { (VideoCodec.H264, AudioCodec.AAC, ContainerFormat.MP4), (VideoCodec.VP9, AudioCodec.Vorbis, ContainerFormat.WebM), (VideoCodec.H265, AudioCodec.AAC, ContainerFormat.MP4), (VideoCodec.MPEG4, AudioCodec.MP3, ContainerFormat.AVI) }; foreach (var (videoCodec, audioCodec, container) in combinations) { var comboResult = await ffmpegService.TranscodeAsync( inputPath: "/input/source.mp4", outputPath: $@"/output/combo-{videoCodec}-{audioCodec}-{container}", settings: new TranscodeSettings { VideoCodec = videoCodec, AudioCodec = audioCodec, Container = container, VideoBitrate = 3000 } ); Assert.True(comboResult.Success); } // Test different quality presets var presets = new[] { QualityPreset.Low, QualityPreset.Medium, QualityPreset.High, QualityPreset.VeryHigh }; foreach (var preset in presets) { var presetResult = await ffmpegService.TranscodeAsync( inputPath: "/input/source.mp4", outputPath: $@"/output/preset-{preset}", settings: new TranscodeSettings { VideoCodec = VideoCodec.H264, Quality = preset, VideoBitrate = 4000 } ); Assert.True(presetResult.Success); } // Verify FFmpeg availability var availability = ffmpegService.IsFFmpegAvailable(); Assert.True(availability); ``` ## FormattingUtilitiesTests The `FormattingUtilitiesTests` class provides unit tests for the `FormattingUtilities` class, verifying that formatting methods work correctly with various FFmpeg-related data types. It includes tests for duration formatting, byte size formatting, bitrate formatting, resolution formatting, string truncation, title casing, percentage formatting, ETA calculation, and string sanitization scenarios. Here is an example usage of the `FormattingUtilitiesTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using Xunit; // Test duration formatting for various time spans [Fact] public void FormatDuration_LessThanOneMinute_ReturnsZeroHoursAndMinutes() { var duration = TimeSpan.FromSeconds(45); var result = FormattingUtilities.FormatDuration(duration); Assert.Equal("00:00:45", result); } [Fact] public void FormatDuration_BetweenOneAndSixtyMinutes_ReturnsZeroHours() { var duration = TimeSpan.FromMinutes(35); var result = FormattingUtilities.FormatDuration(duration); Assert.Equal("00:35:00", result); } [Fact] public void FormatDuration_MoreThanOneHour_IncludesHours() { var duration = TimeSpan.FromHours(2) + TimeSpan.FromMinutes(15) + TimeSpan.FromSeconds(30); var result = FormattingUtilities.FormatDuration(duration); Assert.Equal("02:15:30", result); } // Test byte size formatting [Fact] public void FormatBytes_LessThanOneKilobyte_ReturnsByteSuffix() { var result = FormattingUtilities.FormatBytes(512); Assert.Equal("512 B", result); } [Fact] public void FormatBytes_ExactMegabyte_ReturnsMbSuffix() { var result = FormattingUtilities.FormatBytes(1048576); Assert.Equal("1.00 MB", result); } [Fact] public void FormatBytes_LargeGigabyteValue_ReturnsGbSuffix() { var result = FormattingUtilities.FormatBytes(5368709120); // 5 GB Assert.Equal("5.00 GB", result); } // Test bitrate formatting [Fact] public void FormatBitrate_BelowOneThousand_ReturnsKbps() { var result = FormattingUtilities.FormatBitrate(500); Assert.Equal("500 Kbps", result); } [Fact] public void FormatBitrate_Thousands_ReturnsMbps() { var result = FormattingUtilities.FormatBitrate(3000); Assert.Equal("3 Mbps", result); } [Fact] public void FormatBitrate_Millions_ReturnsGbps() { var result = FormattingUtilities.FormatBitrate(2500000); Assert.Equal("2.5 Gbps", result); } // Test string truncation [Fact] public void TruncateString_BelowMaxLength_ReturnsUnchanged() { var result = FormattingUtilities.TruncateString("short text", 20); Assert.Equal("short text", result); } [Fact] public void TruncateString_ExceedsMaxLength_AppendsEllipsis() { var longText = "This is a very long string that definitely exceeds the maximum length"; var result = FormattingUtilities.TruncateString(longText, 30); Assert.Equal("This is a very long string th...", result); } [Fact] public void TruncateString_NullOrEmpty_ReturnsEmptyString() { var result1 = FormattingUtilities.TruncateString(null, 20); var result2 = FormattingUtilities.TruncateString(string.Empty, 20); Assert.Equal(string.Empty, result1); Assert.Equal(string.Empty, result2); } // Test title case conversion [Fact] public void TitleCase_KebabOrSnakeCase_ReturnsTitleCase() { var result1 = FormattingUtilities.TitleCase("output-format"); var result2 = FormattingUtilities.TitleCase("input_file-path"); Assert.Equal("Output Format", result1); Assert.Equal("Input File Path", result2); } // Test percentage formatting [Fact] public void FormatPercentage_VariousValues_ReturnsOneDecimalPlace() { var result1 = FormattingUtilities.FormatPercentage(25.5); var result2 = FormattingUtilities.FormatPercentage(99.99); var result3 = FormattingUtilities.FormatPercentage(0); Assert.Equal("25.5%", result1); Assert.Equal("100.0%", result2); Assert.Equal("0.0%", result3); } // Test ETA formatting [Fact] public void FormatETA_ZeroProgress_ReturnsCalculatingMessage() { var result = FormattingUtilities.FormatETA(TimeSpan.Zero, 0); Assert.Equal("Calculating...", result); } [Fact] public void FormatETA_HalfwayThrough_ReturnsRemainingTimeEstimate() { var elapsed = TimeSpan.FromSeconds(125); var result = FormattingUtilities.FormatETA(elapsed, 50.0); Assert.Contains("remaining", result); } // Test string sanitization [Fact] public void SanitizeForDisplay_StringWithControlChars_RemovesThem() { var unsafeString = "HelloWorld\tLine1\nLine2"; var result = FormattingUtilities.SanitizeForDisplay(unsafeString); Assert.DoesNotContain("", result); Assert.Contains("Line1", result); Assert.Contains("Line2", result); } [Fact] public void SanitizeForDisplay_StringWithNewline_PreservesNewline() { var result = FormattingUtilities.SanitizeForDisplay("Line1\nLine2"); Assert.Contains("Line1", result); Assert.Contains("Line2", result); } // Test resolution formatting [Fact] public void FormatResolution_StandardHd_ReturnsWidthXHeight() { var result = FormattingUtilities.FormatResolution(1920, 1080); Assert.Equal("1920x1080", result); } ``` ## ProgressTrackerTests The `ProgressTrackerTests` class provides unit tests for the `ProgressTracker` class, verifying that progress tracking functionality works correctly for tracking items, bytes, percentages, and durations during FFmpeg operations. It tests constructor initialization, progress reporting methods, percentage calculations, clamping behavior, reset functionality, and formatted progress output. Here is an example usage of the `ProgressTrackerTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Tests.Utilities; using FFmpegDotnetWrapper.Utilities; using FluentAssertions; using Xunit; // Test constructor with total items [Fact] public void Constructor_WithTotalItems_InitializesCorrectly() { // Arrange & Act var tracker = new ProgressTracker(totalItems: 100); var report = tracker.GetProgressReport(); // Assert report.TotalItems.Should().Be(100); report.ItemsCompleted.Should().Be(0); report.ProgressPercentage.Should().Be(0); report.ElapsedTime.TotalMilliseconds.Should().BeGreaterThan(0); } // Test constructor with total bytes [Fact] public void Constructor_WithTotalBytes_InitializesCorrectly() { // Arrange & Act var tracker = new ProgressTracker(totalBytes: 1024 * 1024); var report = tracker.GetProgressReport(); // Assert report.TotalItems.Should().Be(0); report.ItemsCompleted.Should().Be(0); report.ProgressPercentage.Should().Be(0); } // Test reporting item progress increments items completed [Fact] public void ReportItemProgress_IncrementsItemsCompleted() { // Arrange var tracker = new ProgressTracker(totalItems: 100); // Act tracker.ReportItemProgress(); tracker.ReportItemProgress(); tracker.ReportItemProgress(); var report = tracker.GetProgressReport(); // Assert report.ItemsCompleted.Should().Be(3); report.ProgressPercentage.Should().BeApproximately(3.0, 0.001); } // Test reporting item progress with status message sets status message [Fact] public void ReportItemProgress_WithStatusMessage_SetsStatusMessage() { // Arrange var tracker = new ProgressTracker(totalItems: 100); // Act tracker.ReportItemProgress("Processing frame 42"); var report = tracker.GetProgressReport(); // Assert report.ItemsCompleted.Should().Be(1); report.StatusMessage.Should().Be("Processing frame 42"); } // Test reporting item progress multiple times calculates correct percentage [Fact] public void ReportItemProgress_MultipleTimes_CalculatesCorrectPercentage() { // Arrange var tracker = new ProgressTracker(totalItems: 50); // Act - report 10 items for (int i = 0; i < 10; i++) { tracker.ReportItemProgress(); } var report = tracker.GetProgressReport(); // Assert report.ItemsCompleted.Should().Be(10); report.ProgressPercentage.Should().BeApproximately(20.0, 0.001); } // Test reporting bytes progress updates bytes processed [Fact] public void ReportBytesProgress_UpdatesBytesProcessed() { // Arrange var tracker = new ProgressTracker(totalBytes: 1024 * 1024); // Act tracker.ReportBytesProgress(512 * 1024, "Downloaded 512KB"); tracker.ReportBytesProgress(768 * 1024, "Downloaded 768KB"); var report = tracker.GetProgressReport(); // Assert report.ThroughputBytesPerSecond.Should().BeGreaterThan(0); report.StatusMessage.Should().Be("Downloaded 768KB"); } // Test reporting percentage progress sets correct percentage [Fact] public void ReportPercentageProgress_SetsCorrectPercentage() { // Arrange var tracker = new ProgressTracker(totalItems: 200); // Act tracker.ReportPercentageProgress(25.5, "25% complete"); var report = tracker.GetProgressReport(); // Assert report.ItemsCompleted.Should().Be(51); // 25.5% of 200 = 51 report.ProgressPercentage.Should().BeApproximately(25.5, 0.001); report.StatusMessage.Should().Be("25% complete"); } // Test reporting percentage progress clamps to zero [Fact] public void ReportPercentageProgress_ClampsToZero() { // Arrange var tracker = new ProgressTracker(totalItems: 100); // Act tracker.ReportPercentageProgress(-10, "Negative percentage"); var report = tracker.GetProgressReport(); // Assert report.ProgressPercentage.Should().Be(0); } // Test reporting percentage progress clamps to hundred [Fact] public void ReportPercentageProgress_ClampsToHundred() { // Arrange var tracker = new ProgressTracker(totalItems: 100); // Act tracker.ReportPercentageProgress(150, "Over 100%"); var report = tracker.GetProgressReport(); // Assert report.ProgressPercentage.Should().Be(100); } // Test reporting duration progress calculates correct percentage [Fact] public void ReportDurationProgress_CalculatesCorrectPercentage() { // Arrange var tracker = new ProgressTracker(); var totalDuration = TimeSpan.FromSeconds(100); var processedDuration = TimeSpan.FromSeconds(30); // Act tracker.ReportDurationProgress(processedDuration, totalDuration, "Processing video"); var report = tracker.GetProgressReport(); // Assert report.ProgressPercentage.Should().BeApproximately(30.0, 0.001); report.StatusMessage.Should().Be("Processing video"); } // Test reporting duration progress with zero total duration returns zero [Fact] public void ReportDurationProgress_WithZeroTotalDuration_ReturnsZero() { // Arrange var tracker = new ProgressTracker(); var totalDuration = TimeSpan.Zero; var processedDuration = TimeSpan.FromSeconds(10); // Act tracker.ReportDurationProgress(processedDuration, totalDuration); var report = tracker.GetProgressReport(); // Assert report.ProgressPercentage.Should().Be(0); } // Test get progress report returns all fields populated [Fact] public void GetProgressReport_ReturnsAllFieldsPopulated() { // Arrange var tracker = new ProgressTracker(totalItems: 100); tracker.ReportItemProgress("Processing..."); // Act var report = tracker.GetProgressReport(); // Assert report.ProgressPercentage.Should().BeGreaterThan(0); report.ItemsCompleted.Should().Be(1); report.TotalItems.Should().Be(100); report.ElapsedTime.TotalMilliseconds.Should().BeGreaterThan(0); report.StatusMessage.Should().Be("Processing..."); report.ThroughputItemsPerSecond.Should().BeGreaterThanOrEqualTo(0); } // Test percent complete property returns correct value [Fact] public void PercentComplete_Property_ReturnsCorrectValue() { // Arrange var tracker = new ProgressTracker(totalItems: 50); // Act - report 25 items for (int i = 0; i < 25; i++) { tracker.ReportItemProgress(); } // Assert tracker.PercentComplete.Should().BeApproximately(50.0, 0.001); } // Test percent complete property clamps to zero [Fact] public void PercentComplete_Property_ClampsToZero() { // Arrange var tracker = new ProgressTracker(totalItems: 100); tracker.ReportPercentageProgress(-50); // Assert tracker.PercentComplete.Should().Be(0); } // Test percent complete property clamps to hundred [Fact] public void PercentComplete_Property_ClampsToHundred() { // Arrange var tracker = new ProgressTracker(totalItems: 100); tracker.ReportPercentageProgress(150); // Assert tracker.PercentComplete.Should().Be(100); } // Test reset clears all state [Fact] public void Reset_ClearsAllState() { // Arrange var tracker = new ProgressTracker(totalItems: 100); tracker.ReportItemProgress("First"); tracker.ReportPercentageProgress(50); // Verify initial state - after 50% progress, 50 items should be completed tracker.GetProgressReport().ItemsCompleted.Should().Be(50); tracker.GetProgressReport().ProgressPercentage.Should().BeApproximately(50.0, 0.001); // Act tracker.Reset(totalItems: 200); var report = tracker.GetProgressReport(); // Assert report.ItemsCompleted.Should().Be(0); report.TotalItems.Should().Be(200); report.ProgressPercentage.Should().Be(0); report.StatusMessage.Should().BeEmpty(); } // Test reset resets stopwatch [Fact] public void Reset_ResetsStopwatch() { // Arrange var tracker = new ProgressTracker(totalItems: 100); tracker.ReportItemProgress(); System.Threading.Thread.Sleep(10); // Ensure some time passes // Act tracker.Reset(); var report1 = tracker.GetProgressReport(); System.Threading.Thread.Sleep(10); var report2 = tracker.GetProgressReport(); // Assert - elapsed time should be less after reset report2.ElapsedTime.Should().BeLessThan(report1.ElapsedTime); } // Test get formatted progress returns expected format [Fact] public void GetFormattedProgress_ReturnsExpectedFormat() { // Arrange var tracker = new ProgressTracker(totalItems: 100); tracker.ReportItemProgress("Processing frames"); // Act var formatted = tracker.GetFormattedProgress(); // Assert formatted.Should().NotBeNullOrEmpty(); formatted.Should().Contain("%"); formatted.Should().Contain("1/100"); formatted.Should().Contain("Processing frames"); } // Test get formatted progress without total items shows only percentage [Fact] public void GetFormattedProgress_WithoutTotalItems_ShowsOnlyPercentage() { // Arrange var tracker = new ProgressTracker(); tracker.ReportPercentageProgress(45.5); // Act var formatted = tracker.GetFormattedProgress(); // Assert formatted.Should().Contain("45.5%"); formatted.Should().NotContain("/"); // No items count } ``` using FFmpegDotnetWrapper.Api.Controllers; using FFmpegDotnetWrapper.Api.DTOs; using FFmpegDotnetWrapper.Models; // Create a new instance of the FFmpegController var ffmpegController = new FFmpegController(new FFmpegService(), new Logger()); // Transcode a video file to a different format or codec var transcodeRequest = new TranscodeRequest { InputPath = "/input/video.mp4", OutputPath = "/output/video.mp4", OutputFormat = "mp4", Codec = "libx264", Bitrate = 5000, Quality = 20 }; var transcodeResponse = await ffmpegController.TranscodeAsync(transcodeRequest); Console.WriteLine($"Transcode result: {transcodeResponse.Success}, StatusCode: {transcodeResponse.StatusCode}, Message: {transcodeResponse.Message}"); // Trim a video file to a specified duration or timeframe var trimRequest = new TrimRequest { InputPath = "/input/video.mp4", OutputPath = "/output/trimmed-video.mp4", StartTime = "00:00:10", Duration = "00:01:00" }; var trimResponse = await ffmpegController.TrimAsync(trimRequest); Console.WriteLine($"Trim result: {trimResponse.Success}, StatusCode: {trimResponse.StatusCode}, Message: {trimResponse.Message}"); // Merge multiple video files into a single output file var mergeRequest = new MergeRequest { InputPaths = new List { "/input/video1.mp4", "/input/video2.mp4" }, OutputPath = "/output/merged-video.mp4", MaintainAspectRatio = true }; var mergeResponse = await ffmpegController.MergeAsync(mergeRequest); Console.WriteLine($"Merge result: {mergeResponse.Success}, StatusCode: {mergeResponse.StatusCode}, Message: {mergeResponse.Message}"); // Add a watermark overlay to a video file var watermarkRequest = new WatermarkRequest { InputPath = "/input/video.mp4", OutputPath = "/output/watermarked-video.mp4", WatermarkPath = "/watermark.png", PositionX = 10, PositionY = 10, Opacity = 0.5, Scale = 0.5 }; var watermarkResponse = await ffmpegController.WatermarkAsync(watermarkRequest); Console.WriteLine($"Watermark result: {watermarkResponse.Success}, StatusCode: {watermarkResponse.StatusCode}, Message: {watermarkResponse.Message}"); // Get media info for a file var mediaInfoResponse = ffmpegController.GetMediaInfoAsync("/input/video.mp4"); Console.WriteLine($"Media info: {mediaInfoResponse.Success}, StatusCode: {mediaInfoResponse.StatusCode}, Message: {mediaInfoResponse.Message}"); // Embed subtitles into a video file var subtitleRequest = new SubtitleRequest { InputPath = "/input/video.mp4", OutputPath = "/output/subtitled-video.mp4", SubtitlePath = "/subtitle.srt", HardEmbed = true, Language = "en", FontName = "Arial", FontSize = 24 }; var subtitleResponse = await ffmpegController.EmbedSubtitlesAsync(subtitleRequest); Console.WriteLine($"Subtitle embedding result: {subtitleResponse.Success}, StatusCode: {subtitleResponse.StatusCode}, Message: {subtitleResponse.Message}"); // Extract thumbnails from a video file var thumbnailRequest = new ThumbnailRequest { InputPath = "/input/video.mp4", OutputPattern = "/thumbnails/{index}.jpg", Count = 10, Width = 640, Height = 480, Format = "jpg" }; var thumbnailResponse = await ffmpegController.ExtractThumbnailsAsync(thumbnailRequest); Console.WriteLine($"Thumbnail extraction result: {thumbnailResponse.Success}, StatusCode: {thumbnailResponse.StatusCode}, Message: {thumbnailResponse.Message}"); ``` ``` ## ProcessExecutionExceptionValidationTests The `ProcessExecutionExceptionValidationTests` class provides unit tests for the `ProcessExecutionException` class, verifying that its validation methods work correctly with various exit codes, error outputs, and messages. Here is an example usage of the `ProcessExecutionException` class with its public members: ```csharp using FFmpegDotnetWrapper.Exceptions; // Happy path: valid exception with no exit code or error output var happyPathEx = new ProcessExecutionException("Test message"); var happyPathProblems = happyPathEx.Validate(); // happyPathProblems is empty // ExitCode set with error output: valid combination var exitCodeWithErrorEx = new ProcessExecutionException("Test message") { ExitCode = 0, ErrorOutput = "Some error" }; var exitCodeWithErrorProblems = exitCodeWithErrorEx.Validate(); // exitCodeWithErrorProblems is empty // Negative exit code: returns validation problem var negativeExitCodeEx = new ProcessExecutionException("Test message") { ExitCode = -1, ErrorOutput = "Error" }; var negativeExitCodeProblems = negativeExitCodeEx.Validate(); // Contains: "ExitCode must be a non-negative integer when set." // ExitCode set without error output: returns validation problem var exitCodeWithoutErrorEx = new ProcessExecutionException("Test message") { ExitCode = 1, ErrorOutput = null }; var exitCodeWithoutErrorProblems = exitCodeWithoutErrorEx.Validate(); // Contains: "ErrorOutput must be provided when ExitCode is set." // Whitespace-only message: returns validation problem var whitespaceMessageEx = new ProcessExecutionException(" "); var whitespaceMessageProblems = whitespaceMessageEx.Validate(); // Contains: "Message cannot be null, empty, or whitespace." // IsValid returns true for valid exception var validEx = new ProcessExecutionException("Valid message"); bool isValid = validEx.IsValid(); // isValid is true // IsValid returns false for invalid exception var invalidEx = new ProcessExecutionException("Invalid message") { ExitCode = -5, ErrorOutput = null }; bool isInvalidValid = invalidEx.IsValid(); // isInvalidValid is false // EnsureValid throws no exception for valid exception var ensureValidEx = new ProcessExecutionException("Ensure valid message"); var ensureValidException = Record.Exception(() => ensureValidEx.EnsureValid()); // ensureValidException is null // EnsureValid throws ArgumentException for invalid exception var ensureInvalidEx = new ProcessExecutionException("Ensure invalid message") { ExitCode = -1, ErrorOutput = null }; var ensureInvalidException = Assert.Throws(() => ensureInvalidEx.EnsureValid()); // ensureInvalidException.Message contains "ProcessExecutionException is invalid" // EnsureValid throws ArgumentNullException for null exception ProcessExecutionException nullEx = null; var nullException = Assert.Throws(() => nullEx.EnsureValid()); // nullException is of type ArgumentNullException ``` ## JobQueueTests The `JobQueueTests` class provides comprehensive unit tests for the `JobQueue` service, ensuring reliable background job management. It covers various scenarios including priority handling, delayed job execution, tag support, and critical lifecycle operations like enqueuing, dequeuing, fetching, and removing jobs. Here is an example usage of the `JobQueue` class, demonstrating the functionality verified by these tests: ```csharp using FFmpegDotnetWrapper.BackgroundJobs; using System.Threading.Tasks; // Initialize the job queue var queue = new JobQueue(); // Enqueue a job with a payload var payload = new { Action = "Transcode", File = "input.mp4" }; var jobId = await queue.EnqueueAsync(payload); // Get the job details var job = await queue.GetJobAsync(jobId); // Dequeue the next job var nextJob = await queue.DequeueAsync(); // Get the current queue count var count = await queue.GetQueueCountAsync(); // Remove a job by ID var removed = await queue.RemoveJobAsync(jobId); // Clear all pending jobs from the queue await queue.Clear(); ``` ## FileOperationExceptionExtensionsTests The `FileOperationExceptionExtensionsTests` class provides unit tests for the `FileOperationExceptionExtensions` class, which adds utility methods to `FileOperationException` for extracting file names, checking file path existence, formatting log messages, and enriching exceptions with additional context. Here is an example usage of the `FileOperationExceptionExtensions` class with its public members: ```csharp using FFmpegDotnetWrapper.Exceptions; using System; // Create a file operation exception with a file path var exception = new FileOperationException("Failed to process video", "/videos/input/file.mp4"); // Get the file name from the exception string fileName = exception.GetFileName(); // Returns "file.mp4" // Check if the exception has a file path bool hasPath = exception.HasFilePath(); // Returns true // Get a formatted log string representation string logMessage = exception.ToLogString(); // Returns "Error: Failed to process video (File: /videos/input/file.mp4)" // Add additional context information to the exception var enrichedException = exception.WithAdditionalInfo("Encoding failed due to insufficient disk space"); // enrichedException.Message will be "Failed to process video - Encoding failed due to insufficient disk space" ``` ## ValidationExceptionTests The `ValidationExceptionTests` class provides unit tests for the `ValidationException` class, verifying that constructors and the FromDictionary method work correctly with various parameters including message, validation errors, and inner exceptions. Here is an example usage of the `ValidationException` class with its public members: ```csharp using FFmpegDotnetWrapper.Exceptions; using System.Collections.Generic; // Constructor with message only var exception1 = new ValidationException("Test validation error"); // exception1.Message == "Test validation error" // exception1.ValidationErrors == null // exception1.InnerException == null // Constructor with message and validation errors var errors = new Dictionary { { "Name", new[] { "Name is required" } }, { "Email", new[] { "Email is invalid", "Email is required" } } }; var exception2 = new ValidationException("Validation failed", errors); // exception2.Message == "Validation failed" // exception2.ValidationErrors == errors // exception2.InnerException == null // Constructor with message and inner exception var innerException = new ArgumentException("Inner error"); var exception3 = new ValidationException("Validation failed", innerException); // exception3.Message == "Validation failed" // exception3.ValidationErrors == null // exception3.InnerException == innerException // Constructor with message, validation errors, and inner exception var exception4 = new ValidationException( "Validation failed", errors, innerException); // exception4.Message == "Validation failed" // exception4.ValidationErrors == errors // exception4.InnerException == innerException // FromDictionary with empty errors var exception5 = ValidationException.FromDictionary(new Dictionary()); // exception5.Message == "Validation failed" // exception5.ValidationErrors == empty dictionary // FromDictionary with custom message var exception6 = ValidationException.FromDictionary(errors, "Custom validation message"); // exception6.Message == "Custom validation message" // exception6.ValidationErrors == errors // FromDictionary with multiple errors var multipleErrors = new Dictionary { { "Name", new[] { "Name is required", "Name must be at least 3 characters" } }, { "Email", new[] { "Email is invalid" } }, { "Age", new[] { "Age must be positive", "Age must be less than 120" } } }; var exception7 = ValidationException.FromDictionary(multipleErrors); // exception7.ValidationErrors has 3 entries // FromDictionary with null values (empty arrays) in errors dictionary var exception8 = ValidationException.FromDictionary(new Dictionary { { "Field1", new[] { "Error 1" } }, { "Field2", new string[0] }, { "Field3", new string[0] } }); // exception8.ValidationErrors["Field1"] has one error // exception8.ValidationErrors["Field2"] is empty // exception8.ValidationErrors["Field3"] is empty // Modifying ValidationErrors after construction var exception9 = new ValidationException("Test", errors); exception9.ValidationErrors["NewField"] = new[] { "New error" }; // exception9.ValidationErrors now contains the original fields plus "NewField" // ValidationErrors is null when not set via constructor var exception10 = new ValidationException("Test message"); // exception10.ValidationErrors == null ``` ## ProcessUtilitiesTests The `ProcessUtilitiesTests` class provides unit tests for the `ProcessUtilities` class, verifying argument escaping, synchronous and asynchronous process execution, timeout handling, cancellation, executable availability checks, and command injection prevention. Here is an example usage of the `ProcessUtilities` class with its public members: ```csharp using FFmpegDotnetWrapper.Utilities; using System; using System.Threading; using System.Threading.Tasks; // Escape command-line arguments to prevent injection string escaped = ProcessUtilities.EscapeArgument("file with spaces && rm -rf /"); Console.WriteLine($"Escaped argument: {escaped}"); // Output: "file with spaces && rm -rf /" // Check if an executable is available in PATH bool ffmpegExists = ProcessUtilities.IsExecutableAvailable("ffmpeg"); Console.WriteLine($"FFmpeg available: {ffmpegExists}"); // Execute a process synchronously and capture output var result = ProcessUtilities.ExecuteProcess("echo", "Hello World"); Console.WriteLine($"Exit code: {result.ExitCode}"); Console.WriteLine($"Output: {result.StandardOutput}"); // Execute a process asynchronously with timeout var asyncResult = await ProcessUtilities.ExecuteProcessAsync( "ping", "-n 1 127.0.0.1", timeout: TimeSpan.FromSeconds(5)); Console.WriteLine($"Async exit code: {asyncResult.ExitCode}"); Console.WriteLine($"Async output: {asyncResult.StandardOutput}"); // Execute with cancellation token using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); var cancelledResult = await ProcessUtilities.ExecuteProcessAsync( "timeout", "/t 10", cancellationToken: cts.Token); Console.WriteLine($"Cancelled: {cancelledResult.TimedOut}"); ``` ## AdaptiveBitrateServiceTests The `AdaptiveBitrateServiceTests` class provides unit tests for adaptive bitrate streaming functionality, including streaming profile ladder generation, resolution validation, bitrate calculations, and pipeline settings validation. It verifies that adaptive streaming configurations work correctly for various source resolutions and encoding scenarios. Here is an example usage of the `AdaptiveBitrateServiceTests` class with its public members: ```csharp using FFmpegDotnetWrapper.Models; using FFmpegDotnetWrapper.Tests; using FluentAssertions; using Xunit; // Test that ladder generation for a 1080p source contains expected renditions [Fact] public void StreamingProfile_DefaultLadder_ShouldContainExpectedRenditions() { // Arrange & Act var ladder = StreamingProfile.DefaultLadder; // Assert ladder.Should().HaveCount(4); ladder[0].Should().Be(StreamingProfile.FullHD); ladder[0].Name.Should().Be("1080p"); ladder[0].Width.Should().Be(1920); ladder[0].Height.Should().Be(1080); ladder[0].VideoBitrateKbps.Should().Be(4500); ladder[1].Should().Be(StreamingProfile.HD); ladder[1].Name.Should().Be("720p"); ladder[1].Width.Should().Be(1280); ladder[1].Height.Should().Be(720); ladder[1].VideoBitrateKbps.Should().Be(2500); ladder[2].Should().Be(StreamingProfile.SD); ladder[2].Name.Should().Be("480p"); ladder[2].Width.Should().Be(854); ladder[2].Height.Should().Be(480); ladder[2].VideoBitrateKbps.Should().Be(1000); ladder[3].Should().Be(StreamingProfile.Mobile); ladder[3].Name.Should().Be("360p"); ladder[3].Width.Should().Be(640); ladder[3].Height.Should().Be(360); ladder[3].VideoBitrateKbps.Should().Be(500); } // Test that low-res source does not upscale to higher resolutions [Fact] public void StreamingProfile_ShouldNotUpscale_WhenSourceResolutionIsLow() { // Arrange var settings = new StreamingPipelineSettings { InputFilePath = "/path/to/input.mp4", OutputDirectory = "/tmp/output", Profiles = [ new StreamingProfile("360p", 640, 360, 500, 64), new StreamingProfile("480p", 854, 480, 1000, 96), new StreamingProfile("720p", 1280, 720, 2500, 128), new StreamingProfile("1080p", 1920, 1080, 4500, 192) ] }; // Act - simulate the sorting that happens in AdaptiveBitrateService var orderedProfiles = settings.Profiles .OrderByDescending(p => p.VideoBitrateKbps) .ToList(); // Assert orderedProfiles.Should().HaveCount(4); orderedProfiles[0].Name.Should().Be("1080p"); orderedProfiles[1].Name.Should().Be("720p"); orderedProfiles[2].Name.Should().Be("480p"); orderedProfiles[3].Name.Should().Be("360p"); } // Test that correctly calculates combined video and audio bitrate [Fact] public void StreamingProfile_TotalBitrateKbps_ShouldCalculateCombinedBitrate() { // Arrange var profile = new StreamingProfile("test", 1280, 720, 2500, 128); // Act var totalBitrate = profile.TotalBitrateKbps; // Assert totalBitrate.Should().Be(2628); // 2500 + 128 } // Test that correctly formats width and height as resolution string [Fact] public void StreamingProfile_Resolution_ShouldFormatAsResolutionString() { // Arrange var profile = new StreamingProfile("test", 1920, 1080, 4500, 192); // Act var resolution = profile.Resolution; // Assert resolution.Should().Be("1920x1080"); } // Test that returns zero when duration is zero to avoid division by zero [Fact] public void StreamingSegment_ActualBitrateKbps_ShouldReturnZero_WhenDurationIsZero() { // Arrange var segment = new StreamingSegment { Id = Guid.NewGuid().ToString("N"), PipelineId = "test-pipeline", Profile = StreamingProfile.FullHD, SequenceNumber = 0, FilePath = "/tmp/segment.ts", DurationSeconds = 0, FileSizeBytes = 1000000 }; // Act var actualBitrate = segment.ActualBitrateKbps; // Assert actualBitrate.Should().Be(0); } // Test that correctly calculates bitrate from file size and duration [Fact] public void StreamingSegment_ActualBitrateKbps_ShouldCalculateCorrectBitrate() { // Arrange var segment = new StreamingSegment { Id = Guid.NewGuid().ToString("N"), PipelineId = "test-pipeline", Profile = StreamingProfile.FullHD, SequenceNumber = 0, FilePath = "/tmp/segment.ts", DurationSeconds = 6.0, FileSizeBytes = 4500000 // 4.5 MB = 36 Mb = 4500 kbps }; // Act var actualBitrate = segment.ActualBitrateKbps; // Assert actualBitrate.Should().BeApproximately(6000, 1); // 4500000 * 8 / (6 * 1000) = 6000 kbps } ``` ## FFmpegExceptionExtensionsTests The `FFmpegExceptionExtensionsTests` class provides unit tests for the `FFmpegExceptionExtensions` class, which adds utility methods to FFmpeg-related exceptions. It verifies that `ToDetailedErrorMessage` produces a formatted, human-readable message for each exception type (including file paths, configuration keys, and timeouts), that the classification helpers (`IsProcessFailure`, `IsInvalidMediaFileError`, `IsInvalidConfigurationError`, `IsUnsupportedOperationError`) correctly identify exception categories, and that a null input throws an `ArgumentNullException`. Here is an example usage of the `FFmpegExceptionExtensions` class with its public members: ```csharp using FFmpegDotnetWrapper.Exceptions; using System; // Format a detailed error message for a general FFmpeg exception var ffmpegError = new FFmpegException("Test Message", 1, "Error Output"); string detailed = ffmpegError.ToDetailedErrorMessage(); Console.WriteLine(detailed); // Contains: "FFmpeg Error: FFmpegException", "Message: Test Message", // "Exit Code: 1", "Error Output: Error Output" // Format a detailed error message for an invalid media file (includes file path) var invalidFile = new InvalidMediaFileException("Invalid File", "path/to/file.mp4"); string fileDetail = invalidFile.ToDetailedErrorMessage(); Console.WriteLine(fileDetail); // Contains: "FFmpeg Error: InvalidMediaFileException", "File Path: path/to/file.mp4" // Format a detailed error message for an invalid configuration (includes config key) var invalidConfig = new InvalidOperationConfigurationException("Invalid Config", "myKey"); string configDetail = invalidConfig.ToDetailedErrorMessage(); Console.WriteLine(configDetail); // Contains: "FFmpeg Error: InvalidOperationConfigurationException", "Configuration Key: myKey" // Format a detailed error message for a process exception (includes timeout) var processError = new FFmpegProcessException("Timeout", TimeSpan.FromSeconds(30)); string timeoutDetail = processError.ToDetailedErrorMessage(); Console.WriteLine(timeoutDetail); // Contains: "FFmpeg Error: FFmpegProcessException", "Timeout: 30 seconds" // A null input throws an ArgumentNullException FFmpegException? nullEx = null; try { nullEx!.ToDetailedErrorMessage(); } catch (ArgumentNullException) { Console.WriteLine("Null input rejected"); } // Classify exceptions by category var processFailure = new FFmpegProcessException(); bool isProcessFailure = processFailure.IsProcessFailure(); // true var generalError = new FFmpegException(); bool isGeneralProcessFailure = generalError.IsProcessFailure(); // false var mediaFileError = new InvalidMediaFileException(); bool isInvalidMediaFile = mediaFileError.IsInvalidMediaFileError(); // true var configError = new InvalidOperationConfigurationException(); bool isInvalidConfig = configError.IsInvalidConfigurationError(); // true var unsupportedError = new UnsupportedOperationException(); bool isUnsupported = unsupportedError.IsUnsupportedOperationError(); // true ``` ## RepositoryExceptionExtensionsTests The `RepositoryExceptionExtensionsTests` class provides unit tests for the `RepositoryExceptionExtensions` class, which adds utility methods to `RepositoryException` to check specific error conditions and add context. It verifies that methods like `IsRepositoryNotFound`, `IsRepositoryAlreadyExists`, and `IsAccessDenied` correctly identify repository-specific error conditions based on exception messages, and that the `WithContext` method properly enriches exceptions with additional contextual information while preserving the original exception as an inner exception. Here is an example usage of the `RepositoryExceptionExtensions` class with its public members: ```csharp using FFmpegDotnetWrapper.Exceptions; using System; // Check if a repository exception indicates a not found error var notFoundException = new RepositoryException("Repository not found"); bool isNotFound = notFoundException.IsRepositoryNotFound(); // Returns true // Check if a repository exception indicates an already exists error var alreadyExistsException = new RepositoryException("Repository already exists"); bool isAlreadyExists = alreadyExistsException.IsRepositoryAlreadyExists(); // Returns true // Check if a repository exception indicates an access denied error var accessDeniedException = new RepositoryException("Access denied to repository"); bool isAccessDenied = accessDeniedException.IsAccessDenied(); // Returns true // Add context to a repository exception var originalException = new RepositoryException("Original repository error", "test-repo"); var enrichedException = originalException.WithContext("Additional context about the error"); // enrichedException.Message will be "Original repository error | Context: Additional context about the error" // enrichedException.InnerException will be the original exception ```

About

Strongly-typed FFmpeg wrapper for .NET - transcode, trim, merge, watermark with fluent API. Pulled from my private toolbox and cleaned up for release.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages