diff --git a/PhotoLocator/Metadata/ExifHandler.cs b/PhotoLocator/Metadata/ExifHandler.cs index 4f55790..ce42ffa 100644 --- a/PhotoLocator/Metadata/ExifHandler.cs +++ b/PhotoLocator/Metadata/ExifHandler.cs @@ -581,10 +581,10 @@ public static IEnumerable EnumerateMetadata(BitmapMetadata metadata, str public static string GetMetadataString(BitmapMetadata metadata, Stream? imageStream) { - return GetMetadataString(metadata, DecodeTimeStamp(metadata, imageStream)); + return GetMetadataString(metadata, 0, 0, DecodeTimeStamp(metadata, imageStream)); } - private static string GetMetadataString(BitmapMetadata metadata, DateTimeOffset? timeStamp) + private static string GetMetadataString(BitmapMetadata metadata, int width, int height, DateTimeOffset? timeStamp) { var metadataStrings = new List(); @@ -622,6 +622,9 @@ private static string GetMetadataString(BitmapMetadata metadata, DateTimeOffset? if (iso != null) metadataStrings.Add("ISO" + iso.ToString()); + if (width > 0 && height > 0) + metadataStrings.Add($"{width}x{height}"); + if (timeStamp.HasValue) metadataStrings.Add(FormatTimestampForDisplay(timeStamp.Value)); @@ -641,9 +644,9 @@ internal static string FormatTimestampForDisplay(DateTimeOffset timestamp) try { using var file = await FileHelpers.OpenFileWithRetryAsync(fileName, ct); - var metadata = LoadMetadata(file); - if (metadata is null) - return (null, null, string.Empty, Rotation.Rotate0); + var frame = BitmapDecoder.Create(file, CreateOptions, BitmapCacheOption.OnDemand).Frames[0]; + if (frame.Metadata is not BitmapMetadata metadata) + return (null, null, $"{frame.PixelWidth}x{frame.PixelHeight}", Rotation.Rotate0); var orientationValue = metadata.GetQuery(OrientationQuery1) as ushort? ?? metadata.GetQuery(OrientationQuery2) as ushort? ?? 0; var orientation = orientationValue switch { @@ -653,7 +656,7 @@ internal static string FormatTimestampForDisplay(DateTimeOffset timestamp) _ => Rotation.Rotate0 }; var timeStamp = DecodeTimeStamp(metadata, file); - return (GetGeotag(metadata), timeStamp, GetMetadataString(metadata, timeStamp), orientation); + return (GetGeotag(metadata), timeStamp, GetMetadataString(metadata, frame.PixelWidth, frame.PixelHeight, timeStamp), orientation); } catch (NotSupportedException) { diff --git a/PhotoLocator/VideoTransformCommands.cs b/PhotoLocator/VideoTransformCommands.cs index bfdd1db..50d6fc4 100644 --- a/PhotoLocator/VideoTransformCommands.cs +++ b/PhotoLocator/VideoTransformCommands.cs @@ -28,6 +28,7 @@ public class VideoTransformCommands : INotifyPropertyChanged const string OpenImageFileFilter = "Image files|*.png;*.tif;*.bmp;*.jpg"; const string InputListFileName = "input.txt"; const string SaveVideoFilter = "MP4|*.mp4"; + const int DefaultFrameRate = 30; internal const string TransformsFileName = "transforms.trf"; const int DefaultAverageFramesCount = 20; readonly IMainViewModel _mainViewModel; @@ -670,7 +671,11 @@ private PictureItemViewModel[] UpdateInputArgs() if (!string.IsNullOrEmpty(SkipTo)) args += $"-ss {SkipTo} "; if (!string.IsNullOrEmpty(Duration)) + { + if (!allSelected[0].IsVideo) + args += "-loop 1 "; args += $"-t {Duration} "; + } InputArguments = args + $"-i \"{allSelected[0].FullPath}\""; } else @@ -719,7 +724,7 @@ private void UpdateProcessArgs() filters.Add(string.Format(CultureInfo.InvariantCulture, effectFilter.Filter, EffectParameter, IsScaleChecked ? ScaleTo.Replace(':', 'x') : "1920x1080", - string.IsNullOrEmpty(FrameRate) ? "30" : FrameRate)); + string.IsNullOrEmpty(FrameRate) ? DefaultFrameRate.ToString(CultureInfo.InvariantCulture) : FrameRate)); if (IsSpeedupChecked && (CombineFramesMode != CombineFramesMode.RollingAverage || !SpeedupByEqualsCombineFramesCount)) filters.Add($"setpts=PTS/({SpeedupBy})"); if (!string.IsNullOrEmpty(FrameRate) && SelectedEffect.Text != ZoomEffect) @@ -794,7 +799,7 @@ private bool IsAnyProcessingSelected() if (SelectedVideoFormat == VideoFormats[CopyVideoFormatIndex]) SelectedVideoFormat = VideoFormats[DefaultVideoFormatIndex]; if (string.IsNullOrEmpty(FrameRate)) - FrameRate = "30"; + FrameRate = DefaultFrameRate.ToString(CultureInfo.InvariantCulture); } ProcessSelected.Execute(null); }); @@ -988,7 +993,7 @@ await _mainViewModel.RunProcessWithProgressBarAsync(async (progressCallback, ct) { var sw = Stopwatch.StartNew(); progressCallback(-1); - if (allSelected.All(item => !item.IsVideo)) + if (allSelected.Length > 1 && allSelected.All(item => !item.IsVideo)) { PrepareProgressDisplay(progressCallback); _frameCount = allSelected.Length; @@ -1094,13 +1099,11 @@ await _videoTransforms.RunFFmpegWithStreamOutputImagesAsync($"{InputArguments} { } else { - if (!_hasFps && OutputMode != OutputMode.ImageSequence) - throw new UserMessageException("Unable to determine frame rate, please specify manually"); _progressOffset = 0.5; var frames = CombineFramesMode == CombineFramesMode.TimeSliceInterpolated ? timeSlice.GenerateTimeSliceVideoInterpolated(CombineFramesCount) : timeSlice.GenerateTimeSliceVideo(CombineFramesCount); - await _videoTransforms.RunFFmpegWithStreamInputImagesAsync(_hasFps ? _fps : null, $"{OutputArguments} -y \"{outFileName}\"", frames, ProcessStdError, ct).ConfigureAwait(false); + await _videoTransforms.RunFFmpegWithStreamInputImagesAsync(_hasFps ? _fps : DefaultFrameRate, $"{OutputArguments} -y \"{outFileName}\"", frames, ProcessStdError, ct).ConfigureAwait(false); } return $"Processed {timeSlice.UsedFrames} frames and skipped {timeSlice.SkippedFrames} in {sw.Elapsed.TotalSeconds:N1}s.\n" + "If frames are skipped it means that the video is too big to load into memory. To reduce the number of frames loaded, " + @@ -1143,9 +1146,7 @@ async Task RunLocalFrameProcessingAsync(string outFileName, CancellationToken ct frameEnumerator.AddItem(_localContrastSetup!.ApplyOperations(source)); }, ProcessStdError, ct); await Task.WhenAny(frameEnumerator.GotFirst, Task.Delay(TimeSpan.FromSeconds(10), ct)).ConfigureAwait(false); - if (!_hasFps && OutputMode != OutputMode.ImageSequence) - throw new UserMessageException("Unable to determine frame rate, please specify manually"); - var writeTask = _videoTransforms.RunFFmpegWithStreamInputImagesAsync(_hasFps ? _fps : null, $"{OutputArguments} -y \"{outFileName}\"", frameEnumerator, + var writeTask = _videoTransforms.RunFFmpegWithStreamInputImagesAsync(_hasFps ? _fps : DefaultFrameRate, $"{OutputArguments} -y \"{outFileName}\"", frameEnumerator, stdError => Log.Write("Writer: " + stdError), ct); await await Task.WhenAny(readTask, writeTask).ConfigureAwait(false); // Write task is not expected to finish here, only if it fails frameEnumerator.Break();