SAR/DAR support added

This commit is contained in:
Alexander Shabarshov 2026-05-24 09:16:39 +01:00
parent 4f83fc1dd2
commit a408d43b61
9 changed files with 282 additions and 223 deletions

View File

@ -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;

View File

@ -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));
}
}
}

View File

@ -10,6 +10,10 @@ public sealed class PreviewCanvas : Control
{
public static readonly StyledProperty<PreviewData?> PreviewProperty =
AvaloniaProperty.Register<PreviewCanvas, PreviewData?>(nameof(Preview));
public static readonly StyledProperty<Point2f?> SarProperty =
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar));
public static readonly StyledProperty<Point2f?> DarProperty =
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(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<PreviewCanvas>(
@ -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);
}
}
}
}

View File

@ -11,7 +11,9 @@
<local:PreviewCanvas
Grid.Row="0"
Preview="{Binding Preview}" />
Preview="{Binding Preview}"
Sar="{Binding Sar}"
Dar="{Binding Dar}" />
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto"

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace splitter;
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<string, string>? Tags { get; set; }
}

View File

@ -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<FfprobeStream>? Streams { get; set; }
public FfprobeFormat? Format { get; set; }
}

View File

@ -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<string, int>? Disposition { get; set; }
public Dictionary<string, string>? 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; }
}

View File

@ -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<FfprobeStream>? 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<string, string>? 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<string, int>? Disposition { get; set; }
public Dictionary<string, string>? 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);
}
}

12
splitter-cli/VideoInfo.cs Normal file
View File

@ -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
);