mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Compare commits
3 Commits
e18d043b78
...
18928a23f9
| Author | SHA1 | Date | |
|---|---|---|---|
| 18928a23f9 | |||
| 42408bba38 | |||
| e566bb6137 |
21
Splitter-UI/Converters/RotationAngleToIconConverter.cs
Normal file
21
Splitter-UI/Converters/RotationAngleToIconConverter.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Splitter_UI.Converters;
|
||||
|
||||
public sealed class RotationAngleToIconConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
90 => "\uf2f9", // FA7 (fa-rotate-left / fa-arrow-rotate-left / fa-undo)
|
||||
180 => "\uf2f1", // FA7 (fa-sync-alt)
|
||||
270 => "\uf2ea", // FA7 (fa-rotate-right / fa-arrow-rotate-right / fa-redo)
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
13
Splitter-UI/Converters/ZeroToBoolConverter.cs
Normal file
13
Splitter-UI/Converters/ZeroToBoolConverter.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Splitter_UI.Converters;
|
||||
|
||||
public sealed class ZeroToBoolConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> (value is int i && i == 0);
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
@ -2,10 +2,17 @@
|
||||
|
||||
namespace Splitter_UI.Models;
|
||||
|
||||
public sealed class PreviewData
|
||||
public class PreviewData
|
||||
{
|
||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; init; }
|
||||
public IReadOnlyList<Rect> FaceBoxes { get; init; } = [];
|
||||
public IReadOnlyList<Rect> BodyBoxes { get; init; } = [];
|
||||
public Rect? CropRect { get; init; }
|
||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||
public IReadOnlyList<Rect> DetectedBoxes { get; }
|
||||
public Rect? CropRect { get; }
|
||||
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<Rect> boxes, Rect? crop)
|
||||
{
|
||||
Frame = frame;
|
||||
DetectedBoxes = boxes;
|
||||
CropRect = crop;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Splitter_UI;
|
||||
|
||||
@ -31,6 +32,22 @@ internal sealed class Program
|
||||
services.AddTransient<StatusBarViewModel>();
|
||||
services.AddTransient<LogPaneViewModel>();
|
||||
|
||||
// splitter services
|
||||
services.AddSingleton<UltraFaceDetector>();
|
||||
services.AddSingleton<YoloOnnxObjectDetector>();
|
||||
services.AddSingleton( x => new SingleThreadedDetector<UltraFaceDetector>(x.GetRequiredService<UltraFaceDetector>()) );
|
||||
services.AddSingleton( x => new SingleThreadedDetector<YoloOnnxObjectDetector>(x.GetRequiredService<YoloOnnxObjectDetector>()));
|
||||
services.AddSingleton<Func<string, IObjectDetector>>( x => detectorName =>
|
||||
{
|
||||
return detectorName switch
|
||||
{
|
||||
"face" => x.GetRequiredService<SingleThreadedDetector<UltraFaceDetector>>(),
|
||||
"body" => x.GetRequiredService<SingleThreadedDetector<YoloOnnxObjectDetector>>(),
|
||||
_ => new DummyDetector()
|
||||
};
|
||||
});
|
||||
services.AddSingleton<splitter.ILogger, GlobalLogger>();
|
||||
|
||||
// Domain services (your pipeline)
|
||||
services.AddTransient<IFileProbeService, FileProbeService>();
|
||||
services.AddTransient<IThumbnailService, ThumbnailService>();
|
||||
|
||||
43
Splitter-UI/Services/AvaloniaBitmapExtensions.cs
Normal file
43
Splitter-UI/Services/AvaloniaBitmapExtensions.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public static class AvaloniaBitmapExtensions
|
||||
{
|
||||
public static Mat ToMatContinuous(this Bitmap bmp)
|
||||
{
|
||||
var w = bmp.PixelSize.Width;
|
||||
var h = bmp.PixelSize.Height;
|
||||
var stride = w * 4;
|
||||
var size = h * stride;
|
||||
|
||||
var buffer = new byte[size];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
|
||||
try
|
||||
{
|
||||
bmp.CopyPixels(
|
||||
new PixelRect(0, 0, w, h),
|
||||
handle.AddrOfPinnedObject(),
|
||||
size,
|
||||
stride);
|
||||
|
||||
return Mat.FromPixelData(h, w, MatType.CV_8UC4, buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public static Mat ToMatBgrContinuous(this Bitmap bmp)
|
||||
{
|
||||
using var bgra = bmp.ToMatContinuous();
|
||||
var bgr = new Mat();
|
||||
Cv2.CvtColor(bgra, bgr, ColorConversionCodes.BGRA2BGR);
|
||||
return bgr;
|
||||
}
|
||||
}
|
||||
12
Splitter-UI/Services/DummyDetector.cs
Normal file
12
Splitter-UI/Services/DummyDetector.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
internal class DummyDetector : IObjectDetector
|
||||
{
|
||||
public List<(Rect box, splitter.Point2f center)> DetectAll(Mat frameCont) => [];
|
||||
public void Dispose() {}
|
||||
}
|
||||
15
Splitter-UI/Services/GlobalLogger.cs
Normal file
15
Splitter-UI/Services/GlobalLogger.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
internal class GlobalLogger(ILogService _logService) : ILogger
|
||||
{
|
||||
public void ClearProgress(int progressLevel) { }
|
||||
public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) { }
|
||||
public void Log(string prefix, ConsoleColor color, string msg)
|
||||
{
|
||||
_logService.Write($"[{prefix}] {msg}");
|
||||
}
|
||||
}
|
||||
@ -5,5 +5,5 @@ namespace Splitter_UI.Services;
|
||||
|
||||
public interface IThumbnailService
|
||||
{
|
||||
Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe);
|
||||
Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe, TimeSpan? skip = null, int? width = null, int? height = null, int? rotateDegree = null);
|
||||
}
|
||||
|
||||
23
Splitter-UI/Services/SingleThreadedDetector.cs
Normal file
23
Splitter-UI/Services/SingleThreadedDetector.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public class SingleThreadedDetector<T>(IObjectDetector _detector) : IObjectDetector
|
||||
where T : IObjectDetector
|
||||
{
|
||||
private Lock _lock = new();
|
||||
|
||||
public List<(Rect box, splitter.Point2f center)> DetectAll(Mat frameCont)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _detector.DetectAll(frameCont);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if ( _detector is IDisposable d )
|
||||
d.Dispose();
|
||||
}
|
||||
}
|
||||
@ -7,41 +7,55 @@ namespace Splitter_UI.Services;
|
||||
|
||||
public sealed class ThumbnailService : IThumbnailService
|
||||
{
|
||||
private readonly int _thumbWidth = 160;
|
||||
private readonly int _thumbHeight = 90;
|
||||
private const int _thumbWidth = 160;
|
||||
private const int _thumbHeight = 90;
|
||||
|
||||
// Reusable buffer for BGR24 → 3 bytes per pixel
|
||||
private readonly byte[] _bgrBuffer;
|
||||
private readonly byte[] _bgraBuffer;
|
||||
private readonly byte [] _bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3];
|
||||
private readonly byte [] _bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4];
|
||||
|
||||
public ThumbnailService()
|
||||
public async Task<Bitmap?> CreateThumbnailAsync(
|
||||
string file,
|
||||
VideoInfo probe,
|
||||
TimeSpan? skip = null,
|
||||
int? width = null,
|
||||
int? height = null,
|
||||
int? rotateDegree = null)
|
||||
{
|
||||
_bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3];
|
||||
_bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4];
|
||||
}
|
||||
width ??= _thumbWidth;
|
||||
height ??= _thumbHeight;
|
||||
skip ??= TimeSpan.Zero;
|
||||
|
||||
// buffer for BGR24 → 3 bytes per pixel
|
||||
|
||||
var canUseStaticBuffers =
|
||||
width.Value == _thumbWidth &&
|
||||
height.Value == _thumbHeight;
|
||||
|
||||
var bgrBuffer = canUseStaticBuffers ? _bgrBuffer : new byte[width.Value * height.Value * 3];
|
||||
var bgraBuffer = canUseStaticBuffers ? _bgraBuffer : new byte[width.Value * height.Value * 4];
|
||||
|
||||
public async Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe)
|
||||
{
|
||||
// Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer
|
||||
bool ok = await DecodeFrameAsync(file);
|
||||
bool ok = await DecodeFrameAsync(bgrBuffer, file, skip.Value, width.Value, height.Value, rotateDegree);
|
||||
if (!ok)
|
||||
return null;
|
||||
|
||||
// Convert BGR24 → BGRA32
|
||||
ConvertBgrToBgra(_bgrBuffer, _bgraBuffer, _thumbWidth, _thumbHeight);
|
||||
ConvertBgrToBgra(bgrBuffer, bgraBuffer, width.Value, height.Value);
|
||||
|
||||
// Create Avalonia Bitmap
|
||||
return CreateBitmap(_bgraBuffer, _thumbWidth, _thumbHeight);
|
||||
return CreateBitmap(bgraBuffer, width.Value, height.Value, rotateDegree == 90 || rotateDegree == 270);
|
||||
}
|
||||
|
||||
private async Task<bool> DecodeFrameAsync(string file)
|
||||
private static async Task<bool> DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height, int? rotateDegree)
|
||||
{
|
||||
var rotationStr = TrackingSplitter.GetRorationArg(rotateDegree);
|
||||
|
||||
// ffmpeg command: decode one frame, resize, output raw BGR24
|
||||
var args =
|
||||
$"-ss 0 -t 0.1 -i \"{file}\" " +
|
||||
$"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " +
|
||||
"-an -sn " +
|
||||
$"-vf \"scale={_thumbWidth}:{_thumbHeight}:force_original_aspect_ratio=decrease," +
|
||||
$"pad={_thumbWidth}:{_thumbHeight}:(ow-iw)/2:(oh-ih)/2,format=bgr24\" " +
|
||||
$"-vf \"scale={width}:{height}:force_original_aspect_ratio=decrease," +
|
||||
$"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,format=bgr24{rotationStr}\" " +
|
||||
"-f rawvideo -";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
@ -57,14 +71,14 @@ public sealed class ThumbnailService : IThumbnailService
|
||||
var p = new Process { StartInfo = psi };
|
||||
p.Start();
|
||||
|
||||
int needed = _bgrBuffer.Length;
|
||||
int needed = bgrBuffer.Length;
|
||||
int read = 0;
|
||||
|
||||
using var stdout = p.StandardOutput.BaseStream;
|
||||
|
||||
while (read < needed)
|
||||
{
|
||||
int r = await stdout.ReadAsync(_bgrBuffer, read, needed - read);
|
||||
int r = await stdout.ReadAsync(bgrBuffer, read, needed - read);
|
||||
if (r == 0)
|
||||
{
|
||||
TryKill(p);
|
||||
@ -101,8 +115,13 @@ public sealed class ThumbnailService : IThumbnailService
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height)
|
||||
private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height, bool isRotated)
|
||||
{
|
||||
if (isRotated)
|
||||
{
|
||||
(height, width) = (width, height);
|
||||
}
|
||||
|
||||
int stride = width * 4;
|
||||
|
||||
fixed (byte* p = bgra)
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
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;
|
||||
|
||||
@ -10,7 +12,8 @@ public partial class JobViewModel : ObservableObject
|
||||
{
|
||||
public SingleJob Job { get; }
|
||||
public VideoInfo? Probe { get; set; }
|
||||
public PreviewData? Preview { get; set; }
|
||||
[ObservableProperty]
|
||||
private PreviewData? _preview = new(null, [], null);
|
||||
public ProgressInfo? Progress { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@ -19,11 +22,33 @@ public partial class JobViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string _suggestedAction = "";
|
||||
|
||||
// This updates continuously
|
||||
[ObservableProperty]
|
||||
private double _sliderLiveValue;
|
||||
|
||||
// This updates only on release
|
||||
[ObservableProperty]
|
||||
private double _positionSeconds;
|
||||
|
||||
public double DurationSeconds => Probe?.Duration ?? 0;
|
||||
|
||||
public IRelayCommand StepForwardCommand { get; }
|
||||
public IRelayCommand StepBackwardCommand { get; }
|
||||
|
||||
private readonly IThumbnailService _thumbnails;
|
||||
private readonly IFileProbeService _fileProbe;
|
||||
private readonly DispatcherTimer _debounceTimer;
|
||||
private readonly Func<string, IObjectDetector> _detectorFactory;
|
||||
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();
|
||||
|
||||
@ -88,14 +113,17 @@ public partial class JobViewModel : ObservableObject
|
||||
{
|
||||
Job.Rotate = value;
|
||||
OnPropertyChanged();
|
||||
Task.Run(CreatePreview);
|
||||
}
|
||||
}
|
||||
|
||||
public JobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe)
|
||||
public JobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe, Func<string, IObjectDetector> detectorFactory, ILogger log)
|
||||
{
|
||||
Job = job;
|
||||
_thumbnails = thumbnails;
|
||||
_fileProbe = fileProbe;
|
||||
_detectorFactory = detectorFactory;
|
||||
_log = log;
|
||||
|
||||
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames", ""));
|
||||
ParametersList.Add(new ParameterEntry("EmaFactor", ""));
|
||||
@ -113,6 +141,14 @@ public partial class JobViewModel : ObservableObject
|
||||
|
||||
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
||||
|
||||
StepForwardCommand = new RelayCommand(StepForward);
|
||||
StepBackwardCommand = new RelayCommand(StepBackward);
|
||||
|
||||
_debounceTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
_debounceTimer.Tick += DebounceTimerTick;
|
||||
|
||||
_ = Task.Run( LoadThumbnailAsync );
|
||||
}
|
||||
@ -120,8 +156,34 @@ public partial class JobViewModel : ObservableObject
|
||||
private async Task LoadThumbnailAsync()
|
||||
{
|
||||
Probe = await _fileProbe.ProbeAsync(Job);
|
||||
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe);
|
||||
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, rotateDegree: Job.Rotate);
|
||||
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
|
||||
|
||||
await CreatePreview();
|
||||
}
|
||||
|
||||
private async Task CreatePreview()
|
||||
{
|
||||
if ( Probe == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
var frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, TimeSpan.FromSeconds(PositionSeconds), Probe.Width, Probe.Height, Job.Rotate);
|
||||
if ( frame == null )
|
||||
return;
|
||||
|
||||
Preview = new PreviewData(frame, [], null);
|
||||
|
||||
var detector = _detectorFactory(Job.Detect ?? "");
|
||||
var detections = detector.DetectAll(frame.ToMatContinuous());
|
||||
|
||||
var boxes = detections.Select(x => new Avalonia.Rect(x.box.X, x.box.Y, x.box.Width, x.box.Height)).ToList();
|
||||
Preview = new PreviewData(frame, boxes, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError($"Error creating preview for {FileName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
|
||||
@ -153,4 +215,45 @@ public partial class JobViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private void StepForward()
|
||||
{
|
||||
if (DurationSeconds <= 0)
|
||||
return;
|
||||
|
||||
var step = DurationSeconds * 0.1; // 10% of total duration
|
||||
|
||||
SliderLiveValue = Math.Min(DurationSeconds, SliderLiveValue + step);
|
||||
// trigger seek in your playback pipeline here
|
||||
}
|
||||
|
||||
private void StepBackward()
|
||||
{
|
||||
if (DurationSeconds <= 0)
|
||||
return;
|
||||
|
||||
var step = DurationSeconds * 0.1; // 10% of total duration
|
||||
|
||||
SliderLiveValue = Math.Max(0, SliderLiveValue - step);
|
||||
// trigger seek in your playback pipeline here
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
@ -7,7 +8,23 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private JobViewModel? _selected;
|
||||
|
||||
public PreviewPaneViewModel()
|
||||
public PreviewData? Preview => Selected?.Preview;
|
||||
|
||||
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
|
||||
{
|
||||
if (oldValue != null)
|
||||
oldValue.PropertyChanged -= SelectedPropertyChanged;
|
||||
|
||||
if (newValue != null)
|
||||
newValue.PropertyChanged += SelectedPropertyChanged;
|
||||
|
||||
OnPropertyChanged(nameof(Preview));
|
||||
}
|
||||
|
||||
private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(JobViewModel.Preview))
|
||||
OnPropertyChanged(nameof(Preview));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,10 +3,15 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||
xmlns:views="clr-namespace:Splitter_UI.Views"
|
||||
xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia"
|
||||
xmlns:conv="clr-namespace:Splitter_UI.Converters"
|
||||
x:Class="Splitter_UI.Views.FileListView"
|
||||
x:DataType="vm:FileListViewModel">
|
||||
|
||||
<UserControl.Resources>
|
||||
<conv:ZeroToBoolConverter x:Key="ZeroToBoolConverter"/>
|
||||
<conv:RotationAngleToIconConverter x:Key="RotationAngleToIconConverter"/>
|
||||
</UserControl.Resources>
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="views|FileListView Border#DropZone">
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
@ -29,7 +34,17 @@
|
||||
DragDrop.DragLeave="OnDragLeave">
|
||||
|
||||
|
||||
<Grid>
|
||||
|
||||
<!-- Empty message -->
|
||||
<TextBlock Text="Drag files here"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="#666"
|
||||
IsVisible="{Binding Files.Count, Converter={StaticResource ZeroToBoolConverter}}"/>
|
||||
|
||||
<!-- File list -->
|
||||
<ScrollViewer>
|
||||
<ListBox ItemsSource="{Binding Files}"
|
||||
SelectedItems="{Binding SelectedFiles}"
|
||||
@ -71,6 +86,11 @@
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="LimeGreen"/>
|
||||
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||
Text="{Binding Rotate, Converter={StaticResource RotationAngleToIconConverter}}"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="LimeGreen"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@ -90,5 +110,6 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
@ -12,8 +12,9 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<TextBlock Text="Parameters" FontSize="10" Margin="0,0,0,10" FontWeight="Bold"/>
|
||||
|
||||
<!-- InputFile -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Selected.FileName}" Width="360" Margin="0,0,0,10" FontStyle="Italic"/>
|
||||
<StackPanel Orientation="Vertical" Spacing="8">
|
||||
<TextBlock Text="{Binding Selected.FileName}" Width="360" Margin="0,0,0,5" FontStyle="Italic"/>
|
||||
<TextBlock Text="{Binding Selected.TextDesc}" Width="360" FontSize="10" Margin="0,0,0,10" FontWeight="Bold" Foreground="#676767"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Rotate -->
|
||||
@ -113,13 +114,22 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<DataGrid ItemsSource="{Binding Selected.ParametersList}"
|
||||
AutoGenerateColumns="False"
|
||||
HeadersVisibility="Column"
|
||||
Margin="0,0,20,0"
|
||||
Height="160">
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTextColumn Header="Key"
|
||||
Binding="{Binding Key}"
|
||||
Width="*"/>
|
||||
<DataGridTemplateColumn Header="Key" Width="*">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Key}"
|
||||
FontSize="10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip.Tip="{Binding Key}">
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Header="Value" Width="2*">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
x:Class="Splitter_UI.Views.MainWindow"
|
||||
x:DataType="vm:MainViewModel"
|
||||
Width="1400"
|
||||
Height="900"
|
||||
Height="950"
|
||||
Title="Splitter UI">
|
||||
|
||||
<DockPanel>
|
||||
@ -20,7 +20,7 @@
|
||||
DataContext="{Binding LogPane}" />
|
||||
|
||||
<!-- Main Content -->
|
||||
<Grid ColumnDefinitions="2*,3*,2*">
|
||||
<Grid ColumnDefinitions="2*,3*,430">
|
||||
|
||||
<!-- File List -->
|
||||
<views:FileListView Grid.Column="0"
|
||||
|
||||
100
Splitter-UI/Views/PreviewCanvas.cs
Normal file
100
Splitter-UI/Views/PreviewCanvas.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
public sealed class PreviewCanvas : Control
|
||||
{
|
||||
public static readonly StyledProperty<PreviewData?> PreviewProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, PreviewData?>(nameof(Preview));
|
||||
|
||||
public PreviewData? Preview
|
||||
{
|
||||
get => GetValue(PreviewProperty);
|
||||
set => SetValue(PreviewProperty, value);
|
||||
}
|
||||
|
||||
static PreviewCanvas()
|
||||
{
|
||||
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
|
||||
(canvas, args) =>
|
||||
canvas.OnPreviewChanged(args.OldValue as PreviewData,
|
||||
args.NewValue as PreviewData));
|
||||
}
|
||||
|
||||
private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue)
|
||||
{
|
||||
if (oldValue is INotifyPropertyChanged oldNotify)
|
||||
oldNotify.PropertyChanged -= PreviewPropertyChanged;
|
||||
|
||||
if (newValue is INotifyPropertyChanged newNotify)
|
||||
newNotify.PropertyChanged += PreviewPropertyChanged;
|
||||
|
||||
// Always marshal to UI thread
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
private void PreviewPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PreviewData.Frame) ||
|
||||
e.PropertyName == nameof(PreviewData.DetectedBoxes) ||
|
||||
e.PropertyName == nameof(PreviewData.CropRect))
|
||||
{
|
||||
// Always marshal to UI thread
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) => availableSize;
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
var preview = Preview;
|
||||
if (preview?.Frame is null)
|
||||
return;
|
||||
|
||||
var frame = preview.Frame;
|
||||
var rawW = frame.PixelSize.Width;
|
||||
var rawH = frame.PixelSize.Height;
|
||||
|
||||
var dispW = Bounds.Width;
|
||||
var dispH = Bounds.Height;
|
||||
|
||||
if (dispW <= 0 || dispH <= 0)
|
||||
return;
|
||||
|
||||
var scale = Math.Min(dispW / rawW, dispH / rawH);
|
||||
|
||||
var scaledW = rawW * scale;
|
||||
var scaledH = rawH * scale;
|
||||
|
||||
var offsetX = (dispW - scaledW) / 2;
|
||||
var offsetY = (dispH - scaledH) / 2;
|
||||
|
||||
// draw frame
|
||||
context.DrawImage(frame,
|
||||
new Rect(0, 0, rawW, rawH),
|
||||
new Rect(offsetX, offsetY, scaledW, scaledH));
|
||||
|
||||
// draw overlays
|
||||
if (preview.DetectedBoxes is { Count: > 0 })
|
||||
{
|
||||
var pen = new Pen(Brushes.Lime, 2);
|
||||
|
||||
foreach (var r in preview.DetectedBoxes )
|
||||
{
|
||||
var rr = new Rect(
|
||||
offsetX + r.X * scale,
|
||||
offsetY + r.Y * scale,
|
||||
r.Width * scale,
|
||||
r.Height * scale);
|
||||
|
||||
context.DrawRectangle(null, pen, rr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,57 @@
|
||||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Splitter_UI.Views.PreviewPane"
|
||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||
xmlns:local="clr-namespace:Splitter_UI.Views"
|
||||
x:Class="Splitter_UI.Views.PreviewPane"
|
||||
x:DataType="vm:PreviewPaneViewModel">
|
||||
|
||||
<Border Background="#202020" Padding="10">
|
||||
<Grid>
|
||||
<Image Source="{Binding Selected.Preview.Frame}" Stretch="Uniform"/>
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<local:PreviewCanvas
|
||||
Grid.Row="0"
|
||||
Preview="{Binding Preview}" />
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="0,10,0,0">
|
||||
|
||||
<Button Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
Width="24" Height="24"
|
||||
Padding="0"
|
||||
Margin="0,0,5,0"
|
||||
Command="{Binding Selected.StepBackwardCommand}">
|
||||
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||
Text=""
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
Minimum="0"
|
||||
Maximum="{Binding Selected.DurationSeconds}"
|
||||
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
||||
Margin="5,0,5,0" />
|
||||
|
||||
<Button Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Width="24" Height="24"
|
||||
Padding="0"
|
||||
Margin="5,0,0,0"
|
||||
Command="{Binding Selected.StepForwardCommand}">
|
||||
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||
Text=""
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Optional overlays -->
|
||||
<ItemsControl ItemsSource="{Binding Selected.Preview.FaceBoxes}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="Lime" BorderThickness="2"
|
||||
Width="{Binding Width}" Height="{Binding Height}"
|
||||
Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
@ -8,4 +12,5 @@ public partial class PreviewPane : UserControl
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -170,16 +170,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
var ss = start .ToString("0.###", CultureInfo.InvariantCulture);
|
||||
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
|
||||
var rotateStr = "";
|
||||
if (rotate != null)
|
||||
{
|
||||
switch (rotate.Value)
|
||||
{
|
||||
case 90: rotateStr = ",transpose=1"; break;
|
||||
case 180: rotateStr = ",transpose=PI"; break;
|
||||
case 270: rotateStr = ",transpose=2"; break;
|
||||
}
|
||||
}
|
||||
var rotateStr = GetRorationArg(rotate);
|
||||
|
||||
var args =
|
||||
$"-i \"{inputFile}\" -ss {ss} -t {t} " +
|
||||
@ -217,6 +208,22 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
return p;
|
||||
}
|
||||
|
||||
public static string GetRorationArg(int? rotate)
|
||||
{
|
||||
var rotateStr = "";
|
||||
if (rotate != null)
|
||||
{
|
||||
switch (rotate.Value)
|
||||
{
|
||||
case 90: rotateStr = ",transpose=1"; break;
|
||||
case 180: rotateStr = ",transpose=PI"; break;
|
||||
case 270: rotateStr = ",transpose=2"; break;
|
||||
}
|
||||
}
|
||||
|
||||
return rotateStr;
|
||||
}
|
||||
|
||||
private Process StartFfmpegEncode(
|
||||
string inputFile,
|
||||
string outputFile,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user