diff --git a/Splitter-UI/ViewModels/JobViewModel.cs b/Splitter-UI/ViewModels/JobViewModel.cs index 300244f..d854096 100644 --- a/Splitter-UI/ViewModels/JobViewModel.cs +++ b/Splitter-UI/ViewModels/JobViewModel.cs @@ -10,25 +10,15 @@ namespace Splitter_UI.ViewModels; public partial class JobViewModel : ObservableObject { - public SingleJob Job { get; } - public VideoInfo? Probe { get; set; } - [ObservableProperty] - private PreviewData? _preview = new(null, [], null); - public ProgressInfo? Progress { get; set; } + public SingleJob Job { get; } - [ObservableProperty] - private Bitmap? _thumbnail; - - [ObservableProperty] - private string _suggestedAction = ""; - - // This updates continuously - [ObservableProperty] - private double _sliderLiveValue; - - // This updates only on release - [ObservableProperty] - private double _positionSeconds; + [ObservableProperty] private VideoInfo? _probe; + [ObservableProperty] private PreviewData? _preview = new(null, [], null); + [ObservableProperty] private ProgressInfo? _progress; + [ObservableProperty] private Bitmap? _thumbnail; + [ObservableProperty] private string _suggestedAction = ""; + [ObservableProperty] private double _sliderLiveValue; + [ObservableProperty] private double _positionSeconds; public double DurationSeconds => Probe?.Duration ?? 0; diff --git a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs index 301044c..a9cdad6 100644 --- a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs +++ b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs @@ -9,6 +9,8 @@ public partial class PreviewPaneViewModel : ObservableObject private JobViewModel? _selected; public PreviewData? Preview => Selected?.Preview; + public Point2f? Sar => Selected?.Probe?.Sar; + public Point2f? Dar => Selected?.Probe?.Dar; partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue) { @@ -19,12 +21,20 @@ public partial class PreviewPaneViewModel : ObservableObject newValue.PropertyChanged += SelectedPropertyChanged; OnPropertyChanged(nameof(Preview)); + OnPropertyChanged(nameof(Sar)); + OnPropertyChanged(nameof(Dar)); } private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(JobViewModel.Preview)) OnPropertyChanged(nameof(Preview)); + + if (e.PropertyName == nameof(JobViewModel.Probe)) + { + OnPropertyChanged(nameof(Sar)); + OnPropertyChanged(nameof(Dar)); + } } } diff --git a/Splitter-UI/Views/PreviewCanvas.cs b/Splitter-UI/Views/PreviewCanvas.cs index 8ea8e8d..0b646b3 100644 --- a/Splitter-UI/Views/PreviewCanvas.cs +++ b/Splitter-UI/Views/PreviewCanvas.cs @@ -10,6 +10,10 @@ public sealed class PreviewCanvas : Control { public static readonly StyledProperty PreviewProperty = AvaloniaProperty.Register(nameof(Preview)); + public static readonly StyledProperty SarProperty = + AvaloniaProperty.Register(nameof(Sar)); + public static readonly StyledProperty DarProperty = + AvaloniaProperty.Register(nameof(Dar)); public PreviewData? Preview { @@ -17,6 +21,18 @@ public sealed class PreviewCanvas : Control set => SetValue(PreviewProperty, value); } + public Point2f? Sar + { + get => GetValue(SarProperty); + set => SetValue(SarProperty, value); + } + + public Point2f? Dar + { + get => GetValue(DarProperty); + set => SetValue(DarProperty, value); + } + static PreviewCanvas() { PreviewProperty.Changed.AddClassHandler( @@ -56,7 +72,7 @@ public sealed class PreviewCanvas : Control var preview = Preview; if (preview?.Frame is null) return; - + var frame = preview.Frame; var rawW = frame.PixelSize.Width; var rawH = frame.PixelSize.Height; @@ -67,34 +83,64 @@ public sealed class PreviewCanvas : Control if (dispW <= 0 || dispH <= 0) return; - var scale = Math.Min(dispW / rawW, dispH / rawH); + // SAR + var sar = Sar ?? new Point2f(1, 1); + var sarX = (double)sar.X; + var sarY = (double)sar.Y; - var scaledW = rawW * scale; - var scaledH = rawH * scale; + if (sarX <= 0 || sarY <= 0) + { + sarX = 1; + sarY = 1; + } + + // DAR override (only if SAR missing or invalid) + if ((sarX == 1 && sarY == 1) && Dar is { } dar && dar.X > 0 && dar.Y > 0) + { + var darRatio = dar.X / dar.Y; + var encodedRatio = rawW / (double)rawH; + + // recompute SAR from DAR + sarX = darRatio / encodedRatio; + sarY = 1; + } + + var pixelAspect = sarX / sarY; + + // display size after SAR correction + var displayW = rawW * pixelAspect; + var displayH = rawH; + + var scale = Math.Min(dispW / displayW, dispH / displayH); + + var scaledW = displayW * scale; + var scaledH = displayH * scale; var offsetX = (dispW - scaledW) / 2; var offsetY = (dispH - scaledH) / 2; // draw frame - context.DrawImage(frame, + context.DrawImage( + frame, new Rect(0, 0, rawW, rawH), new Rect(offsetX, offsetY, scaledW, scaledH)); - // draw overlays - if (preview.DetectedBoxes is { Count: > 0 }) + // overlays + if (preview.DetectedBoxes is { Count: > 0 }) { var pen = new Pen(Brushes.Lime, 2); - foreach (var r in preview.DetectedBoxes ) + foreach (var r in preview.DetectedBoxes) { var rr = new Rect( - offsetX + r.X * scale, - offsetY + r.Y * scale, - r.Width * scale, - r.Height * scale); + offsetX + (r.X * pixelAspect) * scale, + offsetY + r.Y * scale, + (r.Width * pixelAspect) * scale, + r.Height * scale); context.DrawRectangle(null, pen, rr); } } } + } diff --git a/Splitter-UI/Views/PreviewPane.axaml b/Splitter-UI/Views/PreviewPane.axaml index 5f0e5b9..0f380a2 100644 --- a/Splitter-UI/Views/PreviewPane.axaml +++ b/Splitter-UI/Views/PreviewPane.axaml @@ -11,7 +11,9 @@ + Preview="{Binding Preview}" + Sar="{Binding Sar}" + Dar="{Binding Dar}" /> ? Tags { get; set; } +} diff --git a/splitter-cli/FfprobeResult.cs b/splitter-cli/FfprobeResult.cs new file mode 100644 index 0000000..db930bd --- /dev/null +++ b/splitter-cli/FfprobeResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using static splitter.ProbeVideo; + +namespace splitter; + +public sealed class FfprobeResult +{ + public List? Streams { get; set; } + public FfprobeFormat? Format { get; set; } +} diff --git a/splitter-cli/FfprobeStream.cs b/splitter-cli/FfprobeStream.cs new file mode 100644 index 0000000..e9cf8e0 --- /dev/null +++ b/splitter-cli/FfprobeStream.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace splitter; + +public sealed class FfprobeStream +{ + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Index { get; set; } + + public string? Codec_name { get; set; } + public string? Codec_long_name { get; set; } + public string? Profile { get; set; } + public string? Codec_type { get; set; } + public string? Codec_tag_string { get; set; } + public string? Codec_tag { get; set; } + public string? Mime_codec_string { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Width { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Height { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Coded_width { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Coded_height { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Closed_captions { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Film_grain { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Has_b_frames { get; set; } + + public string? Sample_aspect_ratio { get; set; } + public string? Display_aspect_ratio { get; set; } + + public string? Pix_fmt { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Level { get; set; } + + public string? Color_range { get; set; } + public string? Color_space { get; set; } + public string? Color_transfer { get; set; } + public string? Color_primaries { get; set; } + public string? Chroma_location { get; set; } + public string? Field_order { get; set; } + + public string? Is_avc { get; set; } + public string? Nal_length_size { get; set; } + + public string? Id { get; set; } + public string? R_frame_rate { get; set; } + public string? Avg_frame_rate { get; set; } + public string? Time_base { get; set; } + + [JsonConverter(typeof(FlexibleLongConverter))] + public long? Start_pts { get; set; } + + [JsonConverter(typeof(FlexibleDoubleConverter))] + public double? Start_time { get; set; } + + [JsonConverter(typeof(FlexibleLongConverter))] + public long? Duration_ts { get; set; } + + [JsonConverter(typeof(FlexibleDoubleConverter))] + public double? Duration { get; set; } + + [JsonConverter(typeof(FlexibleLongConverter))] + public long? Bit_rate { get; set; } + + [JsonConverter(typeof(FlexibleLongConverter))] + public long? Max_bit_rate { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Bits_per_raw_sample { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Bits_per_sample { get; set; } + + [JsonConverter(typeof(FlexibleLongConverter))] + public long? Nb_frames { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Extradata_size { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Channels { get; set; } + + public string? Channel_layout { get; set; } + public string? Sample_fmt { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Sample_rate { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Initial_padding { get; set; } + + public string? Disposition_raw { get; set; } + public Dictionary? Disposition { get; set; } + + public Dictionary? Tags { get; set; } + + public string? Language { get; set; } + public string? Title { get; set; } + + [JsonConverter(typeof(FlexibleIntConverter))] + public int? Bits_per_coded_sample { get; set; } + + public string? Codec_time_base { get; set; } + + [JsonConverter(typeof(FlexibleDoubleConverter))] + public double? Start_pts_time { get; set; } + + [JsonConverter(typeof(FlexibleDoubleConverter))] + public double? Duration_time { get; set; } + + public string? Extradata { get; set; } + public string? Default { get; set; } + public string? Forced { get; set; } +} diff --git a/splitter-cli/ProbeVideo.cs b/splitter-cli/ProbeVideo.cs index 701f408..f19ca39 100644 --- a/splitter-cli/ProbeVideo.cs +++ b/splitter-cli/ProbeVideo.cs @@ -5,17 +5,6 @@ using System.Text.Json.Serialization; namespace splitter; -public record VideoInfo( - double Duration, - int Width, - int Height, - double Fps, - double Bitrate, - double SarX, - double SarY, - int Rotation = 0 -); - public static class ProbeVideo { static ProbeVideo() @@ -43,170 +32,6 @@ public static class ProbeVideo return rotation; } - public sealed class FfprobeResult - { - public List? Streams { get; set; } - public FfprobeFormat? Format { get; set; } - } - - public sealed class FfprobeFormat - { - public string? Filename { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Nb_streams { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Nb_programs { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Nb_stream_groups { get; set; } - - public string? Format_name { get; set; } - public string? Format_long_name { get; set; } - - [JsonConverter(typeof(FlexibleDoubleConverter))] - public double? Start_time { get; set; } - - [JsonConverter(typeof(FlexibleDoubleConverter))] - public double? Duration { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Size { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Bit_rate { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Probe_score { get; set; } - - public Dictionary? Tags { get; set; } - } - - - public sealed class FfprobeStream - { - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Index { get; set; } - - public string? Codec_name { get; set; } - public string? Codec_long_name { get; set; } - public string? Profile { get; set; } - public string? Codec_type { get; set; } - public string? Codec_tag_string { get; set; } - public string? Codec_tag { get; set; } - public string? Mime_codec_string { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Width { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Height { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Coded_width { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Coded_height { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Closed_captions { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Film_grain { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Has_b_frames { get; set; } - - public string? Sample_aspect_ratio { get; set; } - public string? Display_aspect_ratio { get; set; } - - public string? Pix_fmt { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Level { get; set; } - - public string? Color_range { get; set; } - public string? Color_space { get; set; } - public string? Color_transfer { get; set; } - public string? Color_primaries { get; set; } - public string? Chroma_location { get; set; } - public string? Field_order { get; set; } - - public string? Is_avc { get; set; } - public string? Nal_length_size { get; set; } - - public string? Id { get; set; } - public string? R_frame_rate { get; set; } - public string? Avg_frame_rate { get; set; } - public string? Time_base { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Start_pts { get; set; } - - [JsonConverter(typeof(FlexibleDoubleConverter))] - public double? Start_time { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Duration_ts { get; set; } - - [JsonConverter(typeof(FlexibleDoubleConverter))] - public double? Duration { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Bit_rate { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Max_bit_rate { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Bits_per_raw_sample { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Bits_per_sample { get; set; } - - [JsonConverter(typeof(FlexibleLongConverter))] - public long? Nb_frames { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Extradata_size { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Channels { get; set; } - - public string? Channel_layout { get; set; } - public string? Sample_fmt { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Sample_rate { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Initial_padding { get; set; } - - public string? Disposition_raw { get; set; } - public Dictionary? Disposition { get; set; } - - public Dictionary? Tags { get; set; } - - public string? Language { get; set; } - public string? Title { get; set; } - - [JsonConverter(typeof(FlexibleIntConverter))] - public int? Bits_per_coded_sample { get; set; } - - public string? Codec_time_base { get; set; } - - [JsonConverter(typeof(FlexibleDoubleConverter))] - public double? Start_pts_time { get; set; } - - [JsonConverter(typeof(FlexibleDoubleConverter))] - public double? Duration_time { get; set; } - - public string? Extradata { get; set; } - public string? Default { get; set; } - public string? Forced { get; set; } - } - private static readonly JsonSerializerOptions _ffprobeJsonOptions = new () { @@ -265,36 +90,29 @@ public static class ProbeVideo var bitrate = stream?.Bit_rate ?? 0.0; - var (sarX, sarY) = ParseSar(stream?.Sample_aspect_ratio); + var sar = ParseAspectRatio(stream?.Sample_aspect_ratio); + var dar = ParseAspectRatio(stream?.Display_aspect_ratio); - return new VideoInfo(duration, width, height, fps, bitrate, sarX, sarY); + return new VideoInfo(duration, width, height, fps, bitrate, sar, dar); } - static double ParseDouble(string? s) - { - if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) - return v; - - return 0.0; - } - - private static (double x, double y) ParseSar(string? sar) + private static Point2f ParseAspectRatio(string? sar) { if (string.IsNullOrWhiteSpace(sar)) - return (1.0, 1.0); + return new Point2f(1.0f, 1.0f); var parts = sar.Split(':'); if (parts.Length != 2) - return (1.0, 1.0); + return new(1.0f, 1.0f); - if (double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) && - double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) && + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) && + float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) && den != 0) { - return (num, den); + return new(num, den); } - return (1.0, 1.0); + return new(1.0f, 1.0f); } } diff --git a/splitter-cli/VideoInfo.cs b/splitter-cli/VideoInfo.cs new file mode 100644 index 0000000..60743eb --- /dev/null +++ b/splitter-cli/VideoInfo.cs @@ -0,0 +1,12 @@ +namespace splitter; + +public record VideoInfo( + double Duration, + int Width, + int Height, + double Fps, + double Bitrate, + Point2f Sar, + Point2f Dar, + int Rotation = 0 +);