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

@ -11,24 +11,14 @@ namespace Splitter_UI.ViewModels;
public partial class JobViewModel : ObservableObject public partial class JobViewModel : ObservableObject
{ {
public SingleJob Job { get; } public SingleJob Job { get; }
public VideoInfo? Probe { get; set; }
[ObservableProperty]
private PreviewData? _preview = new(null, [], null);
public ProgressInfo? Progress { get; set; }
[ObservableProperty] [ObservableProperty] private VideoInfo? _probe;
private Bitmap? _thumbnail; [ObservableProperty] private PreviewData? _preview = new(null, [], null);
[ObservableProperty] private ProgressInfo? _progress;
[ObservableProperty] [ObservableProperty] private Bitmap? _thumbnail;
private string _suggestedAction = ""; [ObservableProperty] private string _suggestedAction = "";
[ObservableProperty] private double _sliderLiveValue;
// This updates continuously [ObservableProperty] private double _positionSeconds;
[ObservableProperty]
private double _sliderLiveValue;
// This updates only on release
[ObservableProperty]
private double _positionSeconds;
public double DurationSeconds => Probe?.Duration ?? 0; public double DurationSeconds => Probe?.Duration ?? 0;

View File

@ -9,6 +9,8 @@ public partial class PreviewPaneViewModel : ObservableObject
private JobViewModel? _selected; private JobViewModel? _selected;
public PreviewData? Preview => Selected?.Preview; public PreviewData? Preview => Selected?.Preview;
public Point2f? Sar => Selected?.Probe?.Sar;
public Point2f? Dar => Selected?.Probe?.Dar;
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue) partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
{ {
@ -19,12 +21,20 @@ public partial class PreviewPaneViewModel : ObservableObject
newValue.PropertyChanged += SelectedPropertyChanged; newValue.PropertyChanged += SelectedPropertyChanged;
OnPropertyChanged(nameof(Preview)); OnPropertyChanged(nameof(Preview));
OnPropertyChanged(nameof(Sar));
OnPropertyChanged(nameof(Dar));
} }
private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e) private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(JobViewModel.Preview)) if (e.PropertyName == nameof(JobViewModel.Preview))
OnPropertyChanged(nameof(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 = public static readonly StyledProperty<PreviewData?> PreviewProperty =
AvaloniaProperty.Register<PreviewCanvas, PreviewData?>(nameof(Preview)); 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 public PreviewData? Preview
{ {
@ -17,6 +21,18 @@ public sealed class PreviewCanvas : Control
set => SetValue(PreviewProperty, value); 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() static PreviewCanvas()
{ {
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>( PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
@ -67,20 +83,49 @@ public sealed class PreviewCanvas : Control
if (dispW <= 0 || dispH <= 0) if (dispW <= 0 || dispH <= 0)
return; 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; if (sarX <= 0 || sarY <= 0)
var scaledH = rawH * scale; {
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 offsetX = (dispW - scaledW) / 2;
var offsetY = (dispH - scaledH) / 2; var offsetY = (dispH - scaledH) / 2;
// draw frame // draw frame
context.DrawImage(frame, context.DrawImage(
frame,
new Rect(0, 0, rawW, rawH), new Rect(0, 0, rawW, rawH),
new Rect(offsetX, offsetY, scaledW, scaledH)); new Rect(offsetX, offsetY, scaledW, scaledH));
// draw overlays // overlays
if (preview.DetectedBoxes is { Count: > 0 }) if (preview.DetectedBoxes is { Count: > 0 })
{ {
var pen = new Pen(Brushes.Lime, 2); var pen = new Pen(Brushes.Lime, 2);
@ -88,13 +133,14 @@ public sealed class PreviewCanvas : Control
foreach (var r in preview.DetectedBoxes) foreach (var r in preview.DetectedBoxes)
{ {
var rr = new Rect( var rr = new Rect(
offsetX + r.X * scale, offsetX + (r.X * pixelAspect) * scale,
offsetY + r.Y * scale, offsetY + r.Y * scale,
r.Width * scale, (r.Width * pixelAspect) * scale,
r.Height * scale); r.Height * scale);
context.DrawRectangle(null, pen, rr); context.DrawRectangle(null, pen, rr);
} }
} }
} }
} }

View File

@ -11,7 +11,9 @@
<local:PreviewCanvas <local:PreviewCanvas
Grid.Row="0" Grid.Row="0"
Preview="{Binding Preview}" /> Preview="{Binding Preview}"
Sar="{Binding Sar}"
Dar="{Binding Dar}" />
<Grid Grid.Row="1" <Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto" 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; 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 public static class ProbeVideo
{ {
static ProbeVideo() static ProbeVideo()
@ -43,170 +32,6 @@ public static class ProbeVideo
return rotation; 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 = private static readonly JsonSerializerOptions _ffprobeJsonOptions =
new () new ()
{ {
@ -265,36 +90,29 @@ public static class ProbeVideo
var bitrate = stream?.Bit_rate ?? 0.0; 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) private static Point2f ParseAspectRatio(string? sar)
{
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)
{ {
if (string.IsNullOrWhiteSpace(sar)) if (string.IsNullOrWhiteSpace(sar))
return (1.0, 1.0); return new Point2f(1.0f, 1.0f);
var parts = sar.Split(':'); var parts = sar.Split(':');
if (parts.Length != 2) 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) && if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) && float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
den != 0) 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
);