mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
413 lines
14 KiB
C#
413 lines
14 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Threading;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
namespace Splitter_UI.ViewModels;
|
|
|
|
public partial class JobViewModel : ObservableObject
|
|
{
|
|
private SingleJob Job => _f.Model;
|
|
private ViewModelForwarder<SingleJob> _f;
|
|
|
|
public SingleJob GetJob() => Job;
|
|
|
|
[ObservableProperty] private VideoInfo? _probe;
|
|
[ObservableProperty] private PreviewData? _preview = new(null, [], null, new(0.5f, 0.5f), TimeSpan.Zero, null);
|
|
[ObservableProperty] private Bitmap? _thumbnail;
|
|
[ObservableProperty] private double _sliderLiveValue;
|
|
[ObservableProperty] private double _positionSeconds;
|
|
|
|
public string InputFile => Job.InputFile;
|
|
public double DurationSeconds => Probe?.Duration ?? 0;
|
|
public double SegmentDuration
|
|
{
|
|
get
|
|
{
|
|
if (Probe == null || Probe.Duration <= 0)
|
|
return 58.0;
|
|
|
|
var target = Job.OverrideTargetDuration ?? 58.0;
|
|
|
|
int segments;
|
|
double segmentLength;
|
|
|
|
if (Job.ForceFixed)
|
|
{
|
|
// Fixed chunk size, last one may be shorter
|
|
segments = (int)Math.Ceiling(Probe.Duration / target);
|
|
segmentLength = target;
|
|
}
|
|
else
|
|
{
|
|
// Equalized segments
|
|
segments = (int)Math.Ceiling(Probe.Duration / target);
|
|
segmentLength = Probe.Duration / segments;
|
|
}
|
|
|
|
return segmentLength;
|
|
}
|
|
}
|
|
|
|
public IRelayCommand StepForwardCommand { get; }
|
|
public IRelayCommand StepBackwardCommand { get; }
|
|
public IRelayCommand PlayPreviewCommand { get; }
|
|
|
|
private readonly IThumbnailService _thumbnails;
|
|
private readonly DispatcherTimer _debounceTimer;
|
|
private readonly Func<string, IObjectTracker> _trackerFactory;
|
|
private readonly ILogger _log;
|
|
|
|
public string FileName => Path.GetFileName(Job.InputFile);
|
|
|
|
public string TextDesc => Probe != null
|
|
? $"{Probe.Width}x{Probe.Height}, {TimeSpan.FromSeconds(Probe.Duration).ToString(@"hh\:mm\:ss")}), FPS: {Probe.Fps:F2}, Bitrate: {Probe.Bitrate/1024/1024:F2} MB/s"
|
|
: "";
|
|
|
|
public override string ToString() => $"{FileName}: {TextDesc}";
|
|
|
|
public ObservableCollection<ParameterEntry> ParametersList { get; }
|
|
= new();
|
|
|
|
public ObservableCollection<Segment> Segments { get; } = new();
|
|
|
|
public string CropText
|
|
{
|
|
get => Job.Crop is { } c ? $"{c.width},{c.height}" : "";
|
|
set
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
Job.Crop = null;
|
|
}
|
|
else
|
|
{
|
|
var parts = value.Split(',');
|
|
if (parts.Length == 2 &&
|
|
int.TryParse(parts[0], out var w) &&
|
|
int.TryParse(parts[1], out var h))
|
|
Job.Crop = (w, h);
|
|
}
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string GravitateText
|
|
{
|
|
get => Job.GravitateTo is { } p ? $"{p.X:F3},{p.Y:F3}" : "";
|
|
set
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
Job.GravitateTo = new Point2f(0.5f, 0.5f);
|
|
}
|
|
else
|
|
{
|
|
var parts = value.Split(',');
|
|
if (parts.Length == 2 &&
|
|
float.TryParse(parts[0], out var x) &&
|
|
float.TryParse(parts[1], out var y))
|
|
Job.GravitateTo = new Point2f(x, y);
|
|
}
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(GravitateTo));
|
|
}
|
|
}
|
|
|
|
public string PassthroughText
|
|
{
|
|
get => string.Join(' ', Job.Passthrough);
|
|
set
|
|
{
|
|
Job.Passthrough = string.IsNullOrWhiteSpace(value)
|
|
? Array.Empty<string>()
|
|
: value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public Point2f GravitateTo
|
|
{
|
|
get => Job.GravitateTo;
|
|
set
|
|
{
|
|
if (Math.Abs(Job.GravitateTo.X - value.X) < 0.001 && Math.Abs(Job.GravitateTo.Y - value.Y) < 0.001)
|
|
return;
|
|
|
|
Job.GravitateTo = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(GravitateText));
|
|
}
|
|
}
|
|
|
|
public string? Detect { get => Job.Detect; set => _f.Forward(value); }
|
|
public string? Mask { get => Job.Mask; set => _f.Forward(value); }
|
|
public string OutputFolder { get => Job.OutputFolder; set => _f.Forward(value); }
|
|
public bool ForceFixed { get => Job.ForceFixed; set => _f.Forward(value); }
|
|
public bool Debug { get => Job.Debug; set => _f.Forward(value); }
|
|
public bool Enhance { get => Job.Enhance; set => _f.Forward(value); }
|
|
public double? OverrideTargetDuration { get => Job.OverrideTargetDuration; set => _f.Forward(value); }
|
|
public float ScoreThreshold { get => Job.ScoreThreshold; set { _f.Forward(value); Task.Run(CreatePreview); } }
|
|
public float IdentityThreshold { get => Job.IdentityThreshold; set { _f.Forward(value); Task.Run(CreatePreview); } }
|
|
public int? Rotate { get => Job.Rotate; set { _f.Forward(value); Task.Run(CreatePreview); } }
|
|
public float DetectAbove { get => Job.DetectAbove; set { _f.Forward(value); Task.Run(CreatePreview); } }
|
|
public ulong? DetectId { get => Job.DetectId; set { _f.Forward(value); Task.Run(CreatePreview); } }
|
|
|
|
public JobViewModel(SingleJob job, IThumbnailService thumbnails, Func<string, IObjectTracker> trackerFactory, ILogger log)
|
|
{
|
|
_f = new ViewModelForwarder<SingleJob>(job, this.OnPropertyChanged);
|
|
_thumbnails = thumbnails;
|
|
_trackerFactory = trackerFactory;
|
|
_log = log;
|
|
|
|
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames" , ""));
|
|
ParametersList.Add(new ParameterEntry("EmaFactor" , ""));
|
|
ParametersList.Add(new ParameterEntry("CameraEasing" , ""));
|
|
ParametersList.Add(new ParameterEntry("LostFreezeFrames" , ""));
|
|
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount" , ""));
|
|
ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
|
|
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth" , ""));
|
|
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight" , ""));
|
|
|
|
foreach (var entry in ParametersList)
|
|
{
|
|
entry.PropertyChanged += OnParameterChanged;
|
|
}
|
|
|
|
PropertyChanged += (sender, e) =>
|
|
{
|
|
if (e.PropertyName == nameof(Probe))
|
|
{
|
|
if (Segments.Count == 0)
|
|
GenerateSegments();
|
|
OnPropertyChanged(nameof(DurationSeconds));
|
|
}
|
|
};
|
|
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
|
|
|
StepForwardCommand = new RelayCommand(StepForward);
|
|
StepBackwardCommand = new RelayCommand(StepBackward);
|
|
PlayPreviewCommand = new RelayCommand(PlayPreview);
|
|
|
|
_debounceTimer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromSeconds(1)
|
|
};
|
|
_debounceTimer.Tick += DebounceTimerTick;
|
|
}
|
|
|
|
public void GenerateSegments()
|
|
{
|
|
Segments.Clear();
|
|
if (Probe == null || Probe.Duration <= 0)
|
|
return;
|
|
var duration = SegmentDuration;
|
|
var segments = (int)Math.Ceiling(Probe.Duration / duration);
|
|
for (int i = 0; i < segments; i++)
|
|
{
|
|
var start = i * duration;
|
|
var end = Math.Min(start + duration, Probe.Duration);
|
|
Segments.Add(new Segment(start, end));
|
|
}
|
|
}
|
|
|
|
public void CopyFrom(JobViewModel src)
|
|
{
|
|
Job.CopyFrom(src.Job);
|
|
OnPropertyChanged(string.Empty); // Refresh all properties
|
|
}
|
|
|
|
public async Task CreatePreview()
|
|
{
|
|
if ( Probe == null)
|
|
return;
|
|
try
|
|
{
|
|
var pos = TimeSpan.FromSeconds(PositionSeconds);
|
|
|
|
Bitmap? frame;
|
|
if (Preview?.Frame == null || Preview.Position != pos)
|
|
frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, pos, Probe.Width, Probe.Height, Job.Rotate);
|
|
else
|
|
frame = Preview.Frame;
|
|
if ( frame == null )
|
|
return;
|
|
|
|
Preview = new PreviewData(frame, [], null, Job.GravitateTo, pos, Job.Rotate);
|
|
|
|
var tracker = _trackerFactory(Job.Detect ?? "");
|
|
var j = new SingleTask
|
|
(
|
|
Job : Job,
|
|
Info : Probe,
|
|
OutputFileName : "preview.jpg",
|
|
SegmentIndex : 0,
|
|
TotalSegments : 1,
|
|
SegmentStart : PositionSeconds,
|
|
SegmentLength : 1, // 1 second segment for detection
|
|
ProcessorFactory: _ => throw new NotImplementedException()
|
|
);
|
|
|
|
var (detections, primaryDetection) = tracker.SelectTrackedObject(j, frame.ToMatContinuous(), j.Job.GravitateTo);
|
|
|
|
Rect? crop = null;
|
|
var w = Probe.Width;
|
|
var h = Probe.Height;
|
|
|
|
var cropWidth = Job.Crop?.width ?? CommandLine.DefaultW;
|
|
var cropHeight = Job.Crop?.height ?? CommandLine.DefaultH;
|
|
|
|
var p = primaryDetection?.Center ?? new Point2f(w * Job.GravitateTo.X, h * Job.GravitateTo.Y);
|
|
|
|
var cx = p.X - cropWidth / 2f;
|
|
var cy = p.Y - cropHeight / 2f;
|
|
|
|
var r = new Rect(cx, cy, cropWidth, cropHeight);
|
|
|
|
crop = ClampCrop(r, w, h);
|
|
|
|
Preview = new PreviewData(frame, detections ?? [], crop, Job.GravitateTo, pos, Job.Rotate);
|
|
OnPropertyChanged(nameof(SegmentDuration));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogError($"Error creating preview for {FileName}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static Rect ClampCrop(Rect r, float w, float h)
|
|
{
|
|
var x = r.X;
|
|
var y = r.Y;
|
|
var cw = r.Width;
|
|
var ch = r.Height;
|
|
|
|
if (x < 0) x = 0;
|
|
if (y < 0) y = 0;
|
|
|
|
if (x + cw > w) x = w - cw;
|
|
if (y + ch > h) y = h - ch;
|
|
|
|
if (x < 0) x = 0;
|
|
if (y < 0) y = 0;
|
|
|
|
return new Rect(x, y, cw, ch);
|
|
}
|
|
|
|
|
|
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))
|
|
{
|
|
Job.Parameters[p.Key] = p.Value;
|
|
}
|
|
}
|
|
|
|
private void OnParametersCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.NewItems != null)
|
|
{
|
|
foreach (ParameterEntry p in e.NewItems)
|
|
{
|
|
Job.Parameters[p.Key] = p.Value;
|
|
p.PropertyChanged += OnParameterChanged;
|
|
}
|
|
}
|
|
|
|
if (e.OldItems != null)
|
|
{
|
|
foreach (ParameterEntry p in e.OldItems)
|
|
{
|
|
Job.Parameters.Remove(p.Key);
|
|
p.PropertyChanged -= OnParameterChanged;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StepForward()
|
|
{
|
|
if (Segments.Count <= 1)
|
|
return;
|
|
|
|
var current = GetCurrentSegment();
|
|
if ( current < 0 || current >= Segments.Count - 1 )
|
|
return;
|
|
|
|
SliderLiveValue = Segments[current + 1].Start;
|
|
}
|
|
|
|
private void StepBackward()
|
|
{
|
|
if (Segments.Count <= 0)
|
|
return;
|
|
|
|
var current = GetCurrentSegment();
|
|
if (current <= 0)
|
|
{
|
|
SliderLiveValue = 0;
|
|
return;
|
|
}
|
|
|
|
if (SliderLiveValue > Segments[current].Start)
|
|
SliderLiveValue = Segments[current].Start;
|
|
else
|
|
SliderLiveValue = Segments[current - 1].Start;
|
|
}
|
|
|
|
private void PlayPreview()
|
|
{
|
|
// Implementation for playing preview
|
|
}
|
|
|
|
private int GetCurrentSegment()
|
|
{
|
|
double pos = SliderLiveValue;
|
|
|
|
for (int i = 0; i < Segments.Count; i++)
|
|
{
|
|
var s = Segments[i];
|
|
if (pos < s.Start)
|
|
return i - 1;
|
|
if (pos == s.Start)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
partial void OnSliderLiveValueChanged(double value)
|
|
{
|
|
// Restart debounce timer on every slider update
|
|
_debounceTimer.Stop();
|
|
_debounceTimer.Start();
|
|
}
|
|
|
|
private void DebounceTimerTick(object? sender, EventArgs e)
|
|
{
|
|
_debounceTimer.Stop();
|
|
|
|
// Commit the final value
|
|
PositionSeconds = SliderLiveValue;
|
|
}
|
|
|
|
partial void OnPositionSecondsChanged(double value)
|
|
{
|
|
Task.Run(CreatePreview);
|
|
}
|
|
|
|
public async Task<Avalonia.Media.Imaging.Bitmap?> GetThumbnail(double positionSec)
|
|
{
|
|
if (Probe == null)
|
|
return null;
|
|
|
|
var pos = TimeSpan.FromSeconds(positionSec);
|
|
var frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, pos, ThumbnailService.ThumbWidth, ThumbnailService.ThumbHeight, Job.Rotate).ConfigureAwait(false);
|
|
//frame.Save($"c:\\temp\\thmb-{positionSec:N4}.png");
|
|
return frame;
|
|
}
|
|
|
|
}
|