Compare commits
15 Commits
9d50e0695c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a8712e4fa | |||
| 4acaa72d87 | |||
| fdf540a69b | |||
| 627a157762 | |||
| 11931854c7 | |||
|
|
959fdad037 | ||
|
|
93deacf418 | ||
|
|
bd7e71d67c | ||
|
|
8879a9375f | ||
|
|
bf436d0926 | ||
|
|
91695f350c | ||
|
|
bb5b0f2d52 | ||
| 46e94e8ee4 | |||
| 5dcb33ef40 | |||
| ef90c43c00 |
21
Ganimede/App.config
Normal file
21
Ganimede/App.config
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="Ganimede.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<Ganimede.Properties.Settings>
|
||||
<setting name="LastOutputFolder" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="LastVideoPath" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="FFmpegBinFolder" serializeAs="String">
|
||||
<value>C:\Users\alber\source\repos\Ganimede\Ganimede\FFMpeg</value>
|
||||
</setting>
|
||||
</Ganimede.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
BIN
Ganimede/FFMpeg/ffprobe.exe
Normal file
BIN
Ganimede/FFMpeg/ffprobe.exe
Normal file
Binary file not shown.
18
Ganimede/Ganimede/App.config
Normal file
18
Ganimede/Ganimede/App.config
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="Ganimede.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<Ganimede.Properties.Settings>
|
||||
<setting name="LastOutputFolder" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="LastVideoPath" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
</Ganimede.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
@@ -1,14 +1,19 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ganimede
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
// Windows Media Foundation is initialized automatically when needed
|
||||
// No external dependencies or setup required!
|
||||
System.Diagnostics.Debug.WriteLine("[Ganimede] Application started - Using native Windows Media Foundation");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
33
Ganimede/Ganimede/Converters/StatusColorConverter.cs
Normal file
33
Ganimede/Ganimede/Converters/StatusColorConverter.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using Ganimede.Models;
|
||||
|
||||
namespace Ganimede
|
||||
{
|
||||
public class StatusColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is JobStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
JobStatus.Pending => new SolidColorBrush(Colors.Orange),
|
||||
JobStatus.Processing => new SolidColorBrush(Colors.LightBlue),
|
||||
JobStatus.Completed => new SolidColorBrush(Colors.LightGreen),
|
||||
JobStatus.Failed => new SolidColorBrush(Colors.Red),
|
||||
JobStatus.Cancelled => new SolidColorBrush(Colors.Gray),
|
||||
_ => new SolidColorBrush(Colors.White)
|
||||
};
|
||||
}
|
||||
return new SolidColorBrush(Colors.White);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,47 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<!-- Informazioni Versione -->
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<InformationalVersion>1.0.0</InformationalVersion>
|
||||
|
||||
<!-- Informazioni Prodotto -->
|
||||
<Product>Ganimede</Product>
|
||||
<Description>Estrattore Frame Video con Windows Media Foundation Nativa</Description>
|
||||
<Company>Alby96</Company>
|
||||
<Copyright>Copyright © 2024 Alby96</Copyright>
|
||||
<Authors>Alby96</Authors>
|
||||
|
||||
<!-- Icona e Risorse (opzionali) -->
|
||||
<ApplicationIcon />
|
||||
<Neutral>it-IT</Neutral>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Only System.Drawing.Common for image manipulation -->
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Settings.Designer.cs">
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Native Windows libraries (included in Windows) -->
|
||||
|
||||
</Project>
|
||||
|
||||
62
Ganimede/Ganimede/Helpers/NamingHelper.cs
Normal file
62
Ganimede/Ganimede/Helpers/NamingHelper.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Ganimede.Models;
|
||||
|
||||
namespace Ganimede.Helpers
|
||||
{
|
||||
public static class NamingHelper
|
||||
{
|
||||
public static string GenerateFileName(NamingPattern pattern, VideoJob job, int frameIndex, TimeSpan frameTime, string customPrefix = "")
|
||||
{
|
||||
var videoName = Path.GetFileNameWithoutExtension(job.VideoPath);
|
||||
var progressiveNumber = frameIndex + 1; // Start from 1 instead of 0
|
||||
|
||||
return pattern switch
|
||||
{
|
||||
NamingPattern.VideoNameProgressive =>
|
||||
$"{videoName}_{progressiveNumber:D6}.png",
|
||||
|
||||
NamingPattern.FrameProgressive =>
|
||||
$"frame_{progressiveNumber:D6}.png",
|
||||
|
||||
NamingPattern.VideoNameTimestamp =>
|
||||
$"{videoName}_{(int)frameTime.TotalMilliseconds:D6}ms.png",
|
||||
|
||||
NamingPattern.CustomProgressive =>
|
||||
$"{(string.IsNullOrEmpty(customPrefix) ? "custom" : customPrefix)}_{progressiveNumber:D6}.png",
|
||||
|
||||
NamingPattern.TimestampOnly =>
|
||||
$"{(int)frameTime.Hours:D2}h{frameTime.Minutes:D2}m{frameTime.Seconds:D2}s{frameTime.Milliseconds:D3}ms.png",
|
||||
|
||||
NamingPattern.VideoNameFrameProgressive =>
|
||||
$"{videoName}_frame_{progressiveNumber:D6}.png",
|
||||
|
||||
_ => $"frame_{progressiveNumber:D6}.png"
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetPatternDescription(NamingPattern pattern)
|
||||
{
|
||||
var field = pattern.GetType().GetField(pattern.ToString());
|
||||
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attribute?.Description ?? pattern.ToString();
|
||||
}
|
||||
|
||||
public static string GetPatternExample(NamingPattern pattern, string videoName = "VID20250725", string customPrefix = "custom")
|
||||
{
|
||||
var job = new VideoJob
|
||||
{
|
||||
VideoPath = $"{videoName}.mp4",
|
||||
OutputFolder = "",
|
||||
CustomPrefix = customPrefix
|
||||
};
|
||||
|
||||
var frameTime = new TimeSpan(0, 12, 34, 567); // 12:34.567
|
||||
var example = GenerateFileName(pattern, job, 0, frameTime, customPrefix); // frameIndex 0 will become 1
|
||||
|
||||
return example;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,605 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ganimede"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
<Grid>
|
||||
Title="Ganimede - Estrattore Frame Video" Height="750" Width="1200"
|
||||
Background="#0F1419" WindowStartupLocation="CenterScreen">
|
||||
<Window.Resources>
|
||||
<local:StatusColorConverter x:Key="StatusColorConverter"/>
|
||||
|
||||
<!-- Dark Mode Color Palette -->
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#6366F1"/>
|
||||
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="#4F46E5"/>
|
||||
<SolidColorBrush x:Key="PrimaryLightBrush" Color="#818CF8"/>
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#10B981"/>
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#EF4444"/>
|
||||
<SolidColorBrush x:Key="WarningBrush" Color="#F59E0B"/>
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="#0F1419"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#1A1F26"/>
|
||||
<SolidColorBrush x:Key="SurfaceLightBrush" Color="#22272E"/>
|
||||
<SolidColorBrush x:Key="BorderBrush" Color="#30363D"/>
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#F0F6FC"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#9DA5B4"/>
|
||||
<SolidColorBrush x:Key="TextMutedBrush" Color="#636C76"/>
|
||||
<SolidColorBrush x:Key="HoverBrush" Color="#2C3138"/>
|
||||
|
||||
<!-- Modern Button Style (Dark) -->
|
||||
<Style TargetType="Button" x:Key="ModernButton">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryLightBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button" x:Key="OutlineButton" BasedOn="{StaticResource ModernButton}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource HoverBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button" x:Key="DangerButton" BasedOn="{StaticResource ModernButton}">
|
||||
<Setter Property="Background" Value="{StaticResource DangerBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button" x:Key="SuccessButton" BasedOn="{StaticResource ModernButton}">
|
||||
<Setter Property="Background" Value="{StaticResource SuccessBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Navigation Button Style -->
|
||||
<Style TargetType="RadioButton" x:Key="NavButton">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Padding" Value="16,12"/>
|
||||
<Setter Property="Margin" Value="0,4,0,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource HoverBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Modern Card Style (Dark) -->
|
||||
<Style x:Key="Card" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="20"/>
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect Color="#000000" Opacity="0.3" BlurRadius="15" ShadowDepth="0"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Modern TextBox Style (Dark) -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
<Setter Property="Padding" Value="12,8"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8">
|
||||
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Modern ProgressBar Style (Dark) -->
|
||||
<Style TargetType="ProgressBar">
|
||||
<Setter Property="Height" Value="8"/>
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ProgressBar">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="4">
|
||||
<Rectangle Name="PART_Indicator"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
RadiusX="4" RadiusY="4"
|
||||
HorizontalAlignment="Left"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ComboBox Dark Style -->
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
<Setter Property="Padding" Value="12,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- CheckBox Dark Style -->
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- RadioButton Dark Style -->
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Border Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="24,16">
|
||||
<DockPanel>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="🎬" FontSize="28" Margin="0,0,12,0"/>
|
||||
<StackPanel>
|
||||
<TextBlock Text="Ganimede"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="Estrattore Frame Video"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Main Content with Sidebar Navigation -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="240"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Vertical Sidebar Navigation -->
|
||||
<Border Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="0,0,1,0"
|
||||
Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="NAVIGAZIONE"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Margin="16,8,0,12"/>
|
||||
|
||||
<RadioButton x:Name="ProcessingNavButton"
|
||||
Style="{StaticResource NavButton}"
|
||||
Content="🎥 Elaborazione"
|
||||
IsChecked="True"
|
||||
Checked="NavigationButton_Checked"/>
|
||||
|
||||
<RadioButton x:Name="LibraryNavButton"
|
||||
Style="{StaticResource NavButton}"
|
||||
Content="📚 Libreria"
|
||||
Checked="NavigationButton_Checked"/>
|
||||
|
||||
<RadioButton x:Name="SettingsNavButton"
|
||||
Style="{StaticResource NavButton}"
|
||||
Content="⚙️ Impostazioni"
|
||||
Checked="NavigationButton_Checked"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Content Area -->
|
||||
<Grid Grid.Column="1">
|
||||
<!-- Processing View -->
|
||||
<Grid x:Name="ProcessingView" Visibility="Visible" Margin="24,16,24,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Actions Bar -->
|
||||
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource ModernButton}"
|
||||
Content="➕ Aggiungi Video"
|
||||
Click="BrowseVideoButton_Click"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button Style="{StaticResource OutlineButton}"
|
||||
Content="📁 Importa Cartella"
|
||||
Click="ImportFolderButton_Click"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button Style="{StaticResource OutlineButton}"
|
||||
Content="⚙️ Configura Selezionati"
|
||||
x:Name="ConfigureSelectedButton"
|
||||
IsEnabled="False"
|
||||
Click="ConfigureSelectedButton_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Style="{StaticResource SuccessButton}"
|
||||
Content="▶️ Avvia Coda"
|
||||
Width="130"
|
||||
x:Name="StartQueueButton"
|
||||
Click="StartQueueButton_Click"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Style="{StaticResource DangerButton}"
|
||||
Content="⏹️ Ferma"
|
||||
Width="100"
|
||||
x:Name="StopQueueButton"
|
||||
IsEnabled="False"
|
||||
Click="StopQueueButton_Click"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Style="{StaticResource OutlineButton}"
|
||||
Content="🧹 Pulisci"
|
||||
Click="ClearCompletedButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Queue List -->
|
||||
<Border Grid.Row="1" Style="{StaticResource Card}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="Coda di Elaborazione"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock x:Name="QueueCountText"
|
||||
Text="(0)"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Margin="8,2,0,0"/>
|
||||
</DockPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl x:Name="QueueItemsControl">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{StaticResource SurfaceLightBrush}"
|
||||
Margin="0,0,0,12"
|
||||
Padding="16"
|
||||
CornerRadius="8"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox x:Name="JobCheckBox"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0"
|
||||
Tag="{Binding}"
|
||||
Checked="JobCheckBox_CheckedChanged"
|
||||
Unchecked="JobCheckBox_CheckedChanged"/>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding VideoName}"
|
||||
FontWeight="Medium"
|
||||
FontSize="15"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding Progress, StringFormat={}{0:0}%}"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryBrush}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,16,0"/>
|
||||
|
||||
<Button Grid.Column="3"
|
||||
Content="✕"
|
||||
Width="32"
|
||||
Height="32"
|
||||
FontSize="16"
|
||||
Style="{StaticResource OutlineButton}"
|
||||
Padding="0"
|
||||
Tag="{Binding}"
|
||||
Click="RemoveQueueItem_Click"/>
|
||||
</Grid>
|
||||
|
||||
<ProgressBar Grid.Row="1"
|
||||
Value="{Binding Progress}"
|
||||
Margin="0,12,0,0"/>
|
||||
|
||||
<TextBlock Grid.Row="2"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Margin="0,8,0,0">
|
||||
<Run Text="Modalità:"/>
|
||||
<Run Text="{Binding ExtractionModeDisplay, Mode=OneWay}" FontWeight="Medium"/>
|
||||
<Run Text=" • Output:"/>
|
||||
<Run Text="{Binding OutputFolderDisplay, Mode=OneWay}" FontWeight="Medium"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Library View -->
|
||||
<Grid x:Name="LibraryView" Visibility="Collapsed" Margin="24,16,24,16">
|
||||
<Border Style="{StaticResource Card}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="Anteprima Output"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<DockPanel Grid.Row="1" Margin="0,0,0,16">
|
||||
<TextBlock Text="Cartella Output:"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button DockPanel.Dock="Right"
|
||||
Content="Sfoglia"
|
||||
Style="{StaticResource OutlineButton}"
|
||||
Padding="12,6"
|
||||
Click="SelectOutputFolderButton_Click"
|
||||
Margin="12,0,0,0"/>
|
||||
<TextBox x:Name="GlobalOutputFolderTextBox"
|
||||
IsReadOnly="True"
|
||||
VerticalContentAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Background="{StaticResource SurfaceLightBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="12">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl x:Name="ThumbnailsPanel">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="6"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6"
|
||||
Background="{StaticResource SurfaceBrush}">
|
||||
<Image Source="{Binding}"
|
||||
Width="140"
|
||||
Height="80"
|
||||
Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Settings View -->
|
||||
<ScrollViewer x:Name="SettingsView" Visibility="Collapsed" Margin="24,16,24,16" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel MaxWidth="800">
|
||||
<!-- Frame Settings Card -->
|
||||
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Impostazioni Frame"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<TextBlock Text="Dimensione Frame Predefinita"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
<ComboBox x:Name="FrameSizeComboBox"
|
||||
Height="42"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,16">
|
||||
<ComboBoxItem Content="Dimensione Originale" Tag="original" IsSelected="True"/>
|
||||
<ComboBoxItem Content="320x180 (Veloce)" Tag="320,180"/>
|
||||
<ComboBoxItem Content="640x360 (Medio)" Tag="640,360"/>
|
||||
<ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/>
|
||||
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Modalità di Estrazione"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<RadioButton x:Name="DefaultModeFullRadio"
|
||||
Content="Estrazione Completa"
|
||||
GroupName="DefExtraction"
|
||||
IsChecked="True"
|
||||
Margin="0,0,24,0"/>
|
||||
<RadioButton x:Name="DefaultModeSingleRadio"
|
||||
Content="Frame Singolo"
|
||||
GroupName="DefExtraction"
|
||||
Margin="0,0,24,0"/>
|
||||
<RadioButton x:Name="DefaultModeAutoRadio"
|
||||
Content="Rilevamento Automatico"
|
||||
GroupName="DefExtraction"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="La modalità automatica analizza il video e decide il metodo di estrazione migliore."
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Output Settings Card -->
|
||||
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Impostazioni Output"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<CheckBox x:Name="CreateSubfolderCheckBox"
|
||||
Content="Crea sottocartella per ogni video"
|
||||
IsChecked="True"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox"
|
||||
Content="Usa sottocartella per estrazione frame singolo"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<TextBlock Text="Comportamento Sovrascrittura"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
<ComboBox x:Name="OverwriteModeComboBox"
|
||||
Height="42"
|
||||
FontSize="14">
|
||||
<ComboBoxItem Content="Chiedi prima di sovrascrivere" Tag="Ask" IsSelected="True"/>
|
||||
<ComboBoxItem Content="Salta file esistenti" Tag="Skip"/>
|
||||
<ComboBoxItem Content="Sovrascrivi file esistenti" Tag="Overwrite"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="Salva Impostazioni"
|
||||
Style="{StaticResource ModernButton}"
|
||||
Width="150"
|
||||
Click="SaveSettings_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Footer Status Bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="24,12">
|
||||
<DockPanel>
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
|
||||
<TextBlock Text="●"
|
||||
Foreground="{StaticResource SuccessBrush}"
|
||||
FontSize="16"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Pronto"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FontSize="13"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="JobsSummaryText"
|
||||
DockPanel.Dock="Right"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Right"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,24 +1,446 @@
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Ganimede.Properties;
|
||||
using Ganimede.Services;
|
||||
using Ganimede.Models;
|
||||
using Ganimede.Windows;
|
||||
using WpfMessageBox = System.Windows.MessageBox;
|
||||
using WpfOpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
||||
using WpfButton = System.Windows.Controls.Button;
|
||||
|
||||
namespace Ganimede
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private string? outputFolder;
|
||||
private readonly VideoProcessingService _processingService = new();
|
||||
private readonly ObservableCollection<BitmapImage> thumbnails = new();
|
||||
private readonly List<VideoJob> _selectedJobs = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Imposta il titolo con la versione
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
Title = $"Ganimede v{version?.Major}.{version?.Minor}.{version?.Build}";
|
||||
|
||||
InitializeUI();
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
ThumbnailsPanel.ItemsSource = thumbnails;
|
||||
QueueItemsControl.ItemsSource = _processingService.JobQueue;
|
||||
|
||||
outputFolder = Settings.Default.LastOutputFolder;
|
||||
if (!string.IsNullOrEmpty(outputFolder))
|
||||
{
|
||||
StatusText.Text = "Pronto";
|
||||
GlobalOutputFolderTextBox.Text = outputFolder;
|
||||
}
|
||||
|
||||
_processingService.JobCompleted += OnJobCompleted;
|
||||
_processingService.JobFailed += OnJobFailed;
|
||||
_processingService.ProcessingStarted += OnProcessingStarted;
|
||||
_processingService.ProcessingStopped += OnProcessingStopped;
|
||||
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
|
||||
|
||||
// Initialize settings controls
|
||||
LoadSettingsControls();
|
||||
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void LoadSettingsControls()
|
||||
{
|
||||
// Controls are loaded after InitializeComponent, so we can't access them in constructor
|
||||
// We'll load settings when the Settings tab is first accessed instead
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load frame size
|
||||
var frameSize = Settings.Default.FrameSize;
|
||||
foreach (ComboBoxItem item in FrameSizeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == frameSize)
|
||||
{
|
||||
FrameSizeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load overwrite mode
|
||||
var overwriteMode = Settings.Default.DefaultOverwriteMode;
|
||||
foreach (ComboBoxItem item in OverwriteModeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == overwriteMode)
|
||||
{
|
||||
OverwriteModeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load extraction mode
|
||||
switch (Settings.Default.DefaultExtractionMode)
|
||||
{
|
||||
case "SingleFrame":
|
||||
DefaultModeSingleRadio.IsChecked = true;
|
||||
break;
|
||||
case "Auto":
|
||||
DefaultModeAutoRadio.IsChecked = true;
|
||||
break;
|
||||
default:
|
||||
DefaultModeFullRadio.IsChecked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Load folder settings
|
||||
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
|
||||
SingleFrameUseSubfolderCheckBox.IsChecked = Settings.Default.SingleFrameUseSubfolder;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Failed to load settings controls: {ex.Message}");
|
||||
}
|
||||
}, System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void UpdateQueueCount()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
QueueCountText.Text = $"({_processingService.JobQueue.Count})";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateJobsSummary()
|
||||
{
|
||||
var pending = _processingService.JobQueue.Count(j => j.Status == JobStatus.Pending);
|
||||
var processing = _processingService.JobQueue.Count(j => j.Status == JobStatus.Processing);
|
||||
var completed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Completed);
|
||||
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
JobsSummaryText.Text = $"In Attesa: {pending} | In Corso: {processing} | Completati: {completed} | Falliti: {failed}";
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProcessingStarted()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StartQueueButton.IsEnabled = false;
|
||||
StopQueueButton.IsEnabled = true;
|
||||
StatusText.Text = "Elaborazione coda in corso...";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProcessingStopped()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StartQueueButton.IsEnabled = true;
|
||||
StopQueueButton.IsEnabled = false;
|
||||
StatusText.Text = "Coda fermata";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnJobCompleted(VideoJob job)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText.Text = $"✓ Completato: {job.VideoName}";
|
||||
LoadThumbnailsFromFolder(job.OutputFolder);
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnJobFailed(VideoJob job)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText.Text = $"✗ Fallito: {job.VideoName}";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadThumbnailsFromFolder(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(folder)) return;
|
||||
var imageFiles = Directory.GetFiles(folder, "*.png").OrderBy(f => f).Take(60).ToList();
|
||||
thumbnails.Clear();
|
||||
foreach (var imagePath in imageFiles)
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = new Uri(imagePath);
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.EndInit();
|
||||
thumbnails.Add(bmp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Impossibile caricare thumbnails: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_processingService.JobQueue.Count == 0)
|
||||
{
|
||||
WpfMessageBox.Show("Nessun video in coda.", "Coda Vuota", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
|
||||
{
|
||||
WpfMessageBox.Show("Nessun job in attesa nella coda.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
await _processingService.StartProcessingAsync();
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
|
||||
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_processingService.StopProcessing();
|
||||
StatusText.Text = "Arresto in corso...";
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
|
||||
private void JobCheckBox_CheckedChanged(object sender, RoutedEventArgs e) => UpdateSelectedJobs();
|
||||
|
||||
private void UpdateSelectedJobs()
|
||||
{
|
||||
_selectedJobs.Clear();
|
||||
var boxes = FindVisualChildren<System.Windows.Controls.CheckBox>(QueueItemsControl).Where(cb => cb.Name == "JobCheckBox");
|
||||
foreach (var cb in boxes) if (cb.IsChecked == true && cb.Tag is VideoJob job) _selectedJobs.Add(job);
|
||||
ConfigureSelectedButton.IsEnabled = _selectedJobs.Count > 0;
|
||||
}
|
||||
|
||||
private void ConfigureSelectedButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selectedJobs.Count == 0) return;
|
||||
|
||||
var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this };
|
||||
if (cfg.ShowDialog() == true)
|
||||
{
|
||||
StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
|
||||
|
||||
if (!string.IsNullOrEmpty(outputFolder))
|
||||
{
|
||||
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
|
||||
{
|
||||
var createSub = Settings.Default.CreateSubfolder;
|
||||
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame
|
||||
? outputFolder
|
||||
: (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveQueueItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is WpfButton btn && btn.Tag is VideoJob job)
|
||||
{
|
||||
_processingService.CancelJob(job);
|
||||
if (job.Status == JobStatus.Cancelled || job.Status == JobStatus.Pending)
|
||||
_processingService.JobQueue.Remove(job);
|
||||
UpdateQueueCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_processingService.RemoveCompletedJobs();
|
||||
StatusText.Text = "Job completati rimossi";
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing);
|
||||
if (processing)
|
||||
{
|
||||
var res = WpfMessageBox.Show("Ci sono job in elaborazione.\n\nSì: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla operazione", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
||||
if (res == MessageBoxResult.Cancel) return;
|
||||
if (res == MessageBoxResult.Yes)
|
||||
{
|
||||
_processingService.StopProcessing();
|
||||
_processingService.JobQueue.Clear();
|
||||
thumbnails.Clear();
|
||||
}
|
||||
else if (res == MessageBoxResult.No)
|
||||
{
|
||||
for (int i = _processingService.JobQueue.Count - 1; i >= 0; i--)
|
||||
if (_processingService.JobQueue[i].Status != JobStatus.Processing)
|
||||
_processingService.JobQueue.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (WpfMessageBox.Show("Rimuovere tutti i job dalla coda?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||
{
|
||||
_processingService.JobQueue.Clear();
|
||||
thumbnails.Clear();
|
||||
}
|
||||
}
|
||||
StatusText.Text = "Coda aggiornata";
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new WpfOpenFileDialog { Filter = "Video Files|*.mp4;*.avi;*.mov;*.mkv;*.wmv;*.flv;*.webm|All Files|*.*", Multiselect = true };
|
||||
if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
|
||||
}
|
||||
|
||||
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona la cartella contenente i video", ShowNewFolderButton = false };
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
|
||||
if (files.Length == 0)
|
||||
{
|
||||
WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
AddVideosToQueue(files);
|
||||
StatusText.Text = $"Importati {files.Length} video";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVideosToQueue(string[] paths)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outputFolder))
|
||||
{
|
||||
WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
var createSub = Settings.Default.CreateSubfolder;
|
||||
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
|
||||
StatusText.Text = $"Aggiunti {paths.Length} video";
|
||||
Settings.Default.LastVideoPath = paths.FirstOrDefault();
|
||||
Settings.Default.Save();
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||
if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
outputFolder = dialog.SelectedPath;
|
||||
GlobalOutputFolderTextBox.Text = outputFolder;
|
||||
StatusText.Text = "Cartella output aggiornata";
|
||||
Settings.Default.LastOutputFolder = outputFolder;
|
||||
Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSettings_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selectedFrameSize = FrameSizeComboBox.SelectedItem as ComboBoxItem;
|
||||
Settings.Default.FrameSize = selectedFrameSize?.Tag?.ToString() ?? "original";
|
||||
|
||||
var selectedOverwrite = OverwriteModeComboBox.SelectedItem as ComboBoxItem;
|
||||
Settings.Default.DefaultOverwriteMode = selectedOverwrite?.Tag?.ToString() ?? "Ask";
|
||||
|
||||
if (DefaultModeSingleRadio.IsChecked == true)
|
||||
Settings.Default.DefaultExtractionMode = "SingleFrame";
|
||||
else if (DefaultModeAutoRadio.IsChecked == true)
|
||||
Settings.Default.DefaultExtractionMode = "Auto";
|
||||
else
|
||||
Settings.Default.DefaultExtractionMode = "Full";
|
||||
|
||||
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||
Settings.Default.SingleFrameUseSubfolder = SingleFrameUseSubfolderCheckBox.IsChecked ?? false;
|
||||
|
||||
Settings.Default.Save();
|
||||
|
||||
StatusText.Text = "✓ Impostazioni salvate con successo";
|
||||
Debug.WriteLine("[SETTINGS] Impostazioni salvate");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText.Text = "✗ Impossibile salvare le impostazioni";
|
||||
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
|
||||
WpfMessageBox.Show($"Errore nel salvataggio: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsVideoFile(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
return ext is ".mp4" or ".avi" or ".mov" or ".mkv" or ".wmv" or ".flv" or ".webm";
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject dep) where T : DependencyObject
|
||||
{
|
||||
if (dep == null) yield break;
|
||||
for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(dep); i++)
|
||||
{
|
||||
var child = System.Windows.Media.VisualTreeHelper.GetChild(dep, i);
|
||||
if (child is T t) yield return t;
|
||||
foreach (var c in FindVisualChildren<T>(child)) yield return c;
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationButton_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is System.Windows.Controls.RadioButton rb)
|
||||
{
|
||||
// Find views by name
|
||||
var processingView = FindName("ProcessingView") as Grid;
|
||||
var libraryView = FindName("LibraryView") as Grid;
|
||||
var settingsView = FindName("SettingsView") as ScrollViewer;
|
||||
|
||||
if (processingView == null || libraryView == null || settingsView == null)
|
||||
return;
|
||||
|
||||
// Hide all views
|
||||
processingView.Visibility = Visibility.Collapsed;
|
||||
libraryView.Visibility = Visibility.Collapsed;
|
||||
settingsView.Visibility = Visibility.Collapsed;
|
||||
|
||||
// Show selected view
|
||||
if (rb.Name == "ProcessingNavButton")
|
||||
processingView.Visibility = Visibility.Visible;
|
||||
else if (rb.Name == "LibraryNavButton")
|
||||
libraryView.Visibility = Visibility.Visible;
|
||||
else if (rb.Name == "SettingsNavButton")
|
||||
settingsView.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Ganimede/Ganimede/Models/VideoJob.cs
Normal file
231
Ganimede/Ganimede/Models/VideoJob.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Ganimede.Models
|
||||
{
|
||||
public enum JobStatus
|
||||
{
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public enum OverwriteMode
|
||||
{
|
||||
Ask,
|
||||
Skip,
|
||||
Overwrite
|
||||
}
|
||||
|
||||
public enum NamingPattern
|
||||
{
|
||||
[Description("VideoName + Progressive (VID20250725_000001)")]
|
||||
VideoNameProgressive,
|
||||
|
||||
[Description("Frame + Progressive (frame_000001)")]
|
||||
FrameProgressive,
|
||||
|
||||
[Description("VideoName + Timestamp (VID20250725_001234ms)")]
|
||||
VideoNameTimestamp,
|
||||
|
||||
[Description("Custom Prefix + Progressive (custom_000001)")]
|
||||
CustomProgressive,
|
||||
|
||||
[Description("Timestamp Only (00h12m34s567ms)")]
|
||||
TimestampOnly,
|
||||
|
||||
[Description("VideoName + Frame + Progressive (VID20250725_frame_000001)")]
|
||||
VideoNameFrameProgressive
|
||||
}
|
||||
|
||||
// NEW: extraction mode
|
||||
public enum ExtractionMode
|
||||
{
|
||||
Full, // Extract all frames (default)
|
||||
SingleFrame, // Extract only one representative frame
|
||||
Auto // Let the system analyze and decide
|
||||
}
|
||||
|
||||
public class VideoJob : INotifyPropertyChanged
|
||||
{
|
||||
private JobStatus _status = JobStatus.Pending;
|
||||
private double _progress = 0;
|
||||
private string _statusMessage = "Pending";
|
||||
private string _customOutputFolder = string.Empty;
|
||||
private string _customFrameSize = string.Empty;
|
||||
private OverwriteMode? _customOverwriteMode = null;
|
||||
private bool _customCreateSubfolder = true;
|
||||
private NamingPattern? _customNamingPattern = null;
|
||||
private string _customPrefix = string.Empty;
|
||||
private ExtractionMode _extractionMode = ExtractionMode.Full; // user chosen / default
|
||||
private ExtractionMode? _suggestedExtractionMode = null; // suggestion after analysis
|
||||
|
||||
public required string VideoPath { get; set; }
|
||||
public string VideoName => System.IO.Path.GetFileNameWithoutExtension(VideoPath);
|
||||
public required string OutputFolder { get; set; }
|
||||
public DateTime AddedTime { get; set; } = DateTime.Now;
|
||||
|
||||
public JobStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
_status = value;
|
||||
OnPropertyChanged(nameof(Status));
|
||||
}
|
||||
}
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set
|
||||
{
|
||||
_progress = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set
|
||||
{
|
||||
_statusMessage = value;
|
||||
OnPropertyChanged(nameof(StatusMessage));
|
||||
}
|
||||
}
|
||||
|
||||
// Custom settings for individual jobs
|
||||
public string CustomOutputFolder
|
||||
{
|
||||
get => _customOutputFolder;
|
||||
set
|
||||
{
|
||||
_customOutputFolder = value;
|
||||
OnPropertyChanged(nameof(CustomOutputFolder));
|
||||
OnPropertyChanged(nameof(OutputFolderDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomFrameSize
|
||||
{
|
||||
get => _customFrameSize;
|
||||
set
|
||||
{
|
||||
_customFrameSize = value;
|
||||
OnPropertyChanged(nameof(CustomFrameSize));
|
||||
OnPropertyChanged(nameof(FrameSizeDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public OverwriteMode? CustomOverwriteMode
|
||||
{
|
||||
get => _customOverwriteMode;
|
||||
set
|
||||
{
|
||||
_customOverwriteMode = value;
|
||||
OnPropertyChanged(nameof(CustomOverwriteMode));
|
||||
OnPropertyChanged(nameof(OverwriteModeDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public bool CustomCreateSubfolder
|
||||
{
|
||||
get => _customCreateSubfolder;
|
||||
set
|
||||
{
|
||||
_customCreateSubfolder = value;
|
||||
OnPropertyChanged(nameof(CustomCreateSubfolder));
|
||||
OnPropertyChanged(nameof(OutputFolderDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public NamingPattern? CustomNamingPattern
|
||||
{
|
||||
get => _customNamingPattern;
|
||||
set
|
||||
{
|
||||
_customNamingPattern = value;
|
||||
OnPropertyChanged(nameof(CustomNamingPattern));
|
||||
OnPropertyChanged(nameof(NamingPatternDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomPrefix
|
||||
{
|
||||
get => _customPrefix;
|
||||
set
|
||||
{
|
||||
_customPrefix = value;
|
||||
OnPropertyChanged(nameof(CustomPrefix));
|
||||
OnPropertyChanged(nameof(NamingPatternDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: extraction mode chosen by user (or default)
|
||||
public ExtractionMode ExtractionMode
|
||||
{
|
||||
get => _extractionMode;
|
||||
set
|
||||
{
|
||||
_extractionMode = value;
|
||||
OnPropertyChanged(nameof(ExtractionMode));
|
||||
OnPropertyChanged(nameof(ExtractionModeDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: suggested extraction mode discovered during quick analysis
|
||||
public ExtractionMode? SuggestedExtractionMode
|
||||
{
|
||||
get => _suggestedExtractionMode;
|
||||
set
|
||||
{
|
||||
_suggestedExtractionMode = value;
|
||||
OnPropertyChanged(nameof(SuggestedExtractionMode));
|
||||
OnPropertyChanged(nameof(ExtractionModeDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
// Display properties for UI
|
||||
public string OutputFolderDisplay =>
|
||||
string.IsNullOrEmpty(CustomOutputFolder) ? "Default" : System.IO.Path.GetFileName(CustomOutputFolder) + (CustomCreateSubfolder ? "/??" : "");
|
||||
|
||||
public string FrameSizeDisplay =>
|
||||
string.IsNullOrEmpty(CustomFrameSize) ? "Default" :
|
||||
(CustomFrameSize == "original" ? "Original" : CustomFrameSize);
|
||||
|
||||
public string OverwriteModeDisplay =>
|
||||
CustomOverwriteMode?.ToString() ?? "Default";
|
||||
|
||||
public string NamingPatternDisplay =>
|
||||
CustomNamingPattern?.ToString() ?? "Default" +
|
||||
(!string.IsNullOrEmpty(CustomPrefix) ? $" ({CustomPrefix}_)" : "");
|
||||
|
||||
// NEW: display extraction mode with suggestion
|
||||
public string ExtractionModeDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
var mode = ExtractionMode.ToString();
|
||||
if (SuggestedExtractionMode.HasValue && ExtractionMode == ExtractionMode.Full && SuggestedExtractionMode.Value == ExtractionMode.SingleFrame)
|
||||
{
|
||||
mode += " (Suggerito: SingleFrame)";
|
||||
}
|
||||
else if (SuggestedExtractionMode.HasValue && ExtractionMode == ExtractionMode.Auto)
|
||||
{
|
||||
mode += $" -> {SuggestedExtractionMode.Value}";
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
134
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
Normal file
134
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,134 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// Il codice è stato generato da uno strumento.
|
||||
// Versione runtime:4.0.30319.42000
|
||||
//
|
||||
// Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se
|
||||
// il codice viene rigenerato.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Ganimede.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string LastOutputFolder {
|
||||
get {
|
||||
return ((string)(this["LastOutputFolder"]));
|
||||
}
|
||||
set {
|
||||
this["LastOutputFolder"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string LastVideoPath {
|
||||
get {
|
||||
return ((string)(this["LastVideoPath"]));
|
||||
}
|
||||
set {
|
||||
this["LastVideoPath"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("True")]
|
||||
public bool CreateSubfolder {
|
||||
get {
|
||||
return ((bool)(this["CreateSubfolder"]));
|
||||
}
|
||||
set {
|
||||
this["CreateSubfolder"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("original")]
|
||||
public string FrameSize {
|
||||
get {
|
||||
return ((string)(this["FrameSize"]));
|
||||
}
|
||||
set {
|
||||
this["FrameSize"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("Ask")]
|
||||
public string DefaultOverwriteMode {
|
||||
get {
|
||||
return ((string)(this["DefaultOverwriteMode"]));
|
||||
}
|
||||
set {
|
||||
this["DefaultOverwriteMode"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("VideoNameProgressive")]
|
||||
public string DefaultNamingPattern {
|
||||
get {
|
||||
return ((string)(this["DefaultNamingPattern"]));
|
||||
}
|
||||
set {
|
||||
this["DefaultNamingPattern"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("custom")]
|
||||
public string DefaultCustomPrefix {
|
||||
get {
|
||||
return ((string)(this["DefaultCustomPrefix"]));
|
||||
}
|
||||
set {
|
||||
this["DefaultCustomPrefix"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("Full")]
|
||||
public string DefaultExtractionMode {
|
||||
get {
|
||||
return ((string)(this["DefaultExtractionMode"]));
|
||||
}
|
||||
set {
|
||||
this["DefaultExtractionMode"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("False")]
|
||||
public bool SingleFrameUseSubfolder {
|
||||
get {
|
||||
return ((bool)(this["SingleFrameUseSubfolder"]));
|
||||
}
|
||||
set {
|
||||
this["SingleFrameUseSubfolder"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Ganimede/Ganimede/Properties/Settings.settings
Normal file
36
Ganimede/Ganimede/Properties/Settings.settings
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="Ganimede.Properties" GeneratedClassName="Settings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="LastOutputFolder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="LastVideoPath" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="FFmpegBinFolder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg</Value>
|
||||
</Setting>
|
||||
<Setting Name="CreateSubfolder" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">True</Value>
|
||||
</Setting>
|
||||
<Setting Name="FrameSize" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">original</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultOverwriteMode" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">Ask</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultNamingPattern" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">VideoNameProgressive</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultCustomPrefix" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">custom</Value>
|
||||
</Setting>
|
||||
<Setting Name="DefaultExtractionMode" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">Full</Value>
|
||||
</Setting>
|
||||
<Setting Name="SingleFrameUseSubfolder" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
365
Ganimede/Ganimede/Services/VideoProcessingService.cs
Normal file
365
Ganimede/Ganimede/Services/VideoProcessingService.cs
Normal file
@@ -0,0 +1,365 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Ganimede.Models;
|
||||
using Ganimede.Properties;
|
||||
using Ganimede.Helpers;
|
||||
using Ganimede.VideoProcessing;
|
||||
|
||||
namespace Ganimede.Services
|
||||
{
|
||||
public class VideoProcessingService
|
||||
{
|
||||
private readonly ObservableCollection<VideoJob> _jobQueue = new();
|
||||
private readonly SemaphoreSlim _processingSemaphore = new(1, 1);
|
||||
private bool _isProcessing = false;
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
public ObservableCollection<VideoJob> JobQueue => _jobQueue;
|
||||
public bool IsProcessing => _isProcessing;
|
||||
|
||||
public event Action<VideoJob>? JobCompleted;
|
||||
public event Action<VideoJob>? JobFailed;
|
||||
public event Action? ProcessingStarted;
|
||||
public event Action? ProcessingStopped;
|
||||
|
||||
public void AddJob(string videoPath, string outputFolder, bool createSubfolder = true)
|
||||
{
|
||||
// Determine default extraction mode from settings
|
||||
var settingMode = Settings.Default.DefaultExtractionMode;
|
||||
ExtractionMode extractionMode = ExtractionMode.Full;
|
||||
if (!string.IsNullOrEmpty(settingMode) && Enum.TryParse<ExtractionMode>(settingMode, out var parsed))
|
||||
{
|
||||
extractionMode = parsed;
|
||||
}
|
||||
|
||||
var useSingleSub = Settings.Default.SingleFrameUseSubfolder;
|
||||
var jobOutput = createSubfolder && (extractionMode != ExtractionMode.SingleFrame || (extractionMode == ExtractionMode.SingleFrame && useSingleSub))
|
||||
? Path.Combine(outputFolder, Path.GetFileNameWithoutExtension(videoPath))
|
||||
: outputFolder;
|
||||
|
||||
var job = new VideoJob
|
||||
{
|
||||
VideoPath = videoPath,
|
||||
OutputFolder = jobOutput,
|
||||
ExtractionMode = extractionMode
|
||||
};
|
||||
|
||||
_jobQueue.Add(job);
|
||||
Debug.WriteLine($"[QUEUE] Added job: {job.VideoName} (Status: Pending) Mode={job.ExtractionMode}");
|
||||
}
|
||||
|
||||
public async Task StartProcessingAsync()
|
||||
{
|
||||
if (_isProcessing)
|
||||
return;
|
||||
|
||||
await _processingSemaphore.WaitAsync();
|
||||
_isProcessing = true;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
ProcessingStarted?.Invoke();
|
||||
Debug.WriteLine("[QUEUE] Processing started by user");
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
Debug.WriteLine("[QUEUE] Processing cancelled by user");
|
||||
break;
|
||||
}
|
||||
|
||||
var nextJob = GetNextPendingJob();
|
||||
if (nextJob == null)
|
||||
{
|
||||
Debug.WriteLine("[QUEUE] No more pending jobs");
|
||||
break;
|
||||
}
|
||||
|
||||
await ProcessJobAsync(nextJob, _cancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
ProcessingStopped?.Invoke();
|
||||
_processingSemaphore.Release();
|
||||
Debug.WriteLine("[QUEUE] Processing stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public void StopProcessing()
|
||||
{
|
||||
if (_isProcessing)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
Debug.WriteLine("[QUEUE] Stop processing requested");
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelJob(VideoJob job)
|
||||
{
|
||||
if (job.Status == JobStatus.Pending)
|
||||
{
|
||||
job.Status = JobStatus.Cancelled;
|
||||
job.StatusMessage = "Cancelled";
|
||||
Debug.WriteLine($"[QUEUE] Cancelled job: {job.VideoName}");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCompletedJobs()
|
||||
{
|
||||
for (int i = _jobQueue.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_jobQueue[i].Status == JobStatus.Completed ||
|
||||
_jobQueue[i].Status == JobStatus.Failed ||
|
||||
_jobQueue[i].Status == JobStatus.Cancelled)
|
||||
{
|
||||
_jobQueue.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
Debug.WriteLine("[QUEUE] Removed completed jobs");
|
||||
}
|
||||
|
||||
private VideoJob? GetNextPendingJob()
|
||||
{
|
||||
foreach (var job in _jobQueue)
|
||||
{
|
||||
if (job.Status == JobStatus.Pending)
|
||||
return job;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(VideoJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
job.Status = JobStatus.Processing;
|
||||
job.StatusMessage = "Analyzing video...";
|
||||
job.Progress = 0;
|
||||
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
|
||||
|
||||
// Analyze video using VideoAnalyzer
|
||||
var mediaInfo = await Task.Run(() => VideoAnalyzer.Analyze(job.VideoPath), cancellationToken);
|
||||
int frameRate = (int)mediaInfo.FrameRate;
|
||||
int totalFrames = mediaInfo.TotalFrames;
|
||||
Debug.WriteLine($"[INFO] Video {job.VideoName}: {totalFrames} frames at {frameRate} fps, duration {mediaInfo.Duration}");
|
||||
|
||||
// Heuristic suggestion
|
||||
var suggestSingleFrame = false;
|
||||
try
|
||||
{
|
||||
if (totalFrames <= frameRate * 2)
|
||||
suggestSingleFrame = true;
|
||||
else if (mediaInfo.Duration.TotalSeconds >= 3 && mediaInfo.Duration.TotalSeconds <= 45)
|
||||
{
|
||||
if (mediaInfo.BitRate > 0 && mediaInfo.Width > 0 && mediaInfo.Height > 0)
|
||||
{
|
||||
double pixels = mediaInfo.Width * mediaInfo.Height;
|
||||
if (mediaInfo.BitRate < pixels * 0.3)
|
||||
suggestSingleFrame = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
job.SuggestedExtractionMode = suggestSingleFrame ? ExtractionMode.SingleFrame : ExtractionMode.Full;
|
||||
var effectiveMode = job.ExtractionMode == ExtractionMode.Auto
|
||||
? (job.SuggestedExtractionMode ?? ExtractionMode.Full)
|
||||
: job.ExtractionMode;
|
||||
|
||||
// ADJUST OUTPUT FOLDER NOW based on effective mode (includes Auto->SingleFrame)
|
||||
if (effectiveMode == ExtractionMode.SingleFrame && !Settings.Default.SingleFrameUseSubfolder)
|
||||
{
|
||||
// If current output folder ends with the video name (subfolder), move up
|
||||
if (Path.GetFileName(job.OutputFolder).Equals(job.VideoName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parent = Directory.GetParent(job.OutputFolder);
|
||||
if (parent != null)
|
||||
{
|
||||
Debug.WriteLine($"[PROCESS] Adjusting output folder for single frame (removing subfolder): {job.OutputFolder} -> {parent.FullName}");
|
||||
job.OutputFolder = parent.FullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(job.OutputFolder);
|
||||
|
||||
var frameSize = GetFrameSize(job);
|
||||
var overwriteMode = GetOverwriteMode(job);
|
||||
var namingPattern = GetNamingPattern(job);
|
||||
var customPrefix = GetCustomPrefix(job);
|
||||
|
||||
var existingFiles = Directory.GetFiles(job.OutputFolder, "*.png");
|
||||
if (existingFiles.Length > 0 && overwriteMode == OverwriteMode.Ask)
|
||||
{
|
||||
var dialogResult = System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
return System.Windows.MessageBox.Show(
|
||||
$"Found {existingFiles.Length} existing frame files in:\n{job.OutputFolder}\n\n" +
|
||||
"Do you want to overwrite them?",
|
||||
$"Existing Files - {job.VideoName}",
|
||||
System.Windows.MessageBoxButton.YesNo,
|
||||
System.Windows.MessageBoxImage.Question);
|
||||
});
|
||||
overwriteMode = dialogResult == System.Windows.MessageBoxResult.Yes ? OverwriteMode.Overwrite : OverwriteMode.Skip;
|
||||
}
|
||||
|
||||
if (effectiveMode == ExtractionMode.SingleFrame)
|
||||
{
|
||||
int targetIndex = totalFrames > 0 ? totalFrames / 2 : 0;
|
||||
var frameTime = TimeSpan.FromSeconds((double)targetIndex / Math.Max(frameRate,1));
|
||||
var fileName = NamingHelper.GenerateFileName(namingPattern, job, targetIndex, frameTime, customPrefix);
|
||||
string framePath = Path.Combine(job.OutputFolder, fileName);
|
||||
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
|
||||
job.StatusMessage = "Frame already exists (skipped)";
|
||||
else
|
||||
{
|
||||
await ExtractFrameAsync(job, frameTime, frameSize, framePath, cancellationToken);
|
||||
job.StatusMessage = "Single frame extracted";
|
||||
}
|
||||
job.Progress = 100;
|
||||
job.Status = JobStatus.Completed;
|
||||
JobCompleted?.Invoke(job);
|
||||
Debug.WriteLine($"[SUCCESS] Single frame extraction completed: {job.VideoName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Full extraction using FrameExtractor
|
||||
int processedFrames = 0;
|
||||
int skippedFrames = 0;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
FrameExtractor.ExtractAllFrames(
|
||||
job.VideoPath,
|
||||
job.OutputFolder,
|
||||
(frameIndex, timePosition) => NamingHelper.GenerateFileName(namingPattern, job, frameIndex, timePosition, customPrefix),
|
||||
frameSize.width,
|
||||
frameSize.height,
|
||||
(current, total) =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
job.Progress = (double)current / total * 100;
|
||||
job.StatusMessage = $"Processed {current}/{total} frames ({job.Progress:F1}%)";
|
||||
processedFrames = current;
|
||||
},
|
||||
(framePath) =>
|
||||
{
|
||||
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
|
||||
{
|
||||
skippedFrames++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}, cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
job.Status = JobStatus.Cancelled;
|
||||
job.StatusMessage = "Cancelled by user";
|
||||
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
|
||||
return;
|
||||
}
|
||||
|
||||
job.Status = JobStatus.Completed;
|
||||
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
|
||||
job.Progress = 100;
|
||||
Debug.WriteLine($"[SUCCESS] Completed job: {job.VideoName}");
|
||||
JobCompleted?.Invoke(job);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
job.Status = JobStatus.Cancelled;
|
||||
job.StatusMessage = "Cancelled by user";
|
||||
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
job.Status = JobStatus.Failed;
|
||||
job.StatusMessage = $"Error: {ex.Message}";
|
||||
Debug.WriteLine($"[ERROR] Job failed for {job.VideoName}: {ex.Message}");
|
||||
JobFailed?.Invoke(job);
|
||||
}
|
||||
}
|
||||
|
||||
private (int width, int height) GetFrameSize(VideoJob job)
|
||||
{
|
||||
var frameSize = !string.IsNullOrEmpty(job.CustomFrameSize) ? job.CustomFrameSize : Settings.Default.FrameSize;
|
||||
|
||||
if (string.IsNullOrEmpty(frameSize) || frameSize == "original")
|
||||
return (-1, -1);
|
||||
|
||||
var parts = frameSize.Split(',');
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out int width) && int.TryParse(parts[1], out int height))
|
||||
return (width, height);
|
||||
|
||||
return (-1, -1);
|
||||
}
|
||||
|
||||
private OverwriteMode GetOverwriteMode(VideoJob job)
|
||||
{
|
||||
if (job.CustomOverwriteMode.HasValue)
|
||||
return job.CustomOverwriteMode.Value;
|
||||
|
||||
var defaultMode = Settings.Default.DefaultOverwriteMode;
|
||||
if (Enum.TryParse<OverwriteMode>(defaultMode, out var mode))
|
||||
return mode;
|
||||
|
||||
return OverwriteMode.Ask;
|
||||
}
|
||||
|
||||
private NamingPattern GetNamingPattern(VideoJob job)
|
||||
{
|
||||
if (job.CustomNamingPattern.HasValue)
|
||||
return job.CustomNamingPattern.Value;
|
||||
|
||||
var defaultPattern = Settings.Default.DefaultNamingPattern;
|
||||
if (Enum.TryParse<NamingPattern>(defaultPattern, out var pattern))
|
||||
return pattern;
|
||||
|
||||
return NamingPattern.VideoNameProgressive;
|
||||
}
|
||||
|
||||
private string GetCustomPrefix(VideoJob job)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(job.CustomPrefix))
|
||||
return job.CustomPrefix;
|
||||
|
||||
return Settings.Default.DefaultCustomPrefix ?? "custom";
|
||||
}
|
||||
|
||||
private async Task ExtractFrameAsync(VideoJob job, TimeSpan frameTime, (int width, int height) frameSize, string framePath, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
FrameExtractor.ExtractFrame(
|
||||
job.VideoPath,
|
||||
frameTime,
|
||||
framePath,
|
||||
frameSize.width,
|
||||
frameSize.height
|
||||
);
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ERROR] Failed to extract frame from {job.VideoName}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
147
Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs
Normal file
147
Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using Ganimede.VideoProcessing.MediaFoundation;
|
||||
|
||||
namespace Ganimede.VideoProcessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides frame extraction capabilities from video files using Windows Media Foundation
|
||||
/// NO external dependencies - uses only Windows built-in APIs
|
||||
/// </summary>
|
||||
public class FrameExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts a single frame from a video at a specific time position
|
||||
/// </summary>
|
||||
public static void ExtractFrame(
|
||||
string videoPath,
|
||||
TimeSpan timePosition,
|
||||
string outputPath,
|
||||
int targetWidth = -1,
|
||||
int targetHeight = -1)
|
||||
{
|
||||
if (!File.Exists(videoPath))
|
||||
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||
|
||||
try
|
||||
{
|
||||
using (var reader = new MFVideoReader(videoPath))
|
||||
{
|
||||
// Read frame at specified time
|
||||
using (var bitmap = reader.ReadFrameAtTime(timePosition))
|
||||
{
|
||||
if (bitmap == null)
|
||||
throw new InvalidOperationException($"Could not extract frame at position {timePosition}");
|
||||
|
||||
// Resize if needed
|
||||
if (targetWidth > 0 && targetHeight > 0 &&
|
||||
(targetWidth != bitmap.Width || targetHeight != bitmap.Height))
|
||||
{
|
||||
using (var resized = new Bitmap(bitmap, targetWidth, targetHeight))
|
||||
{
|
||||
SaveBitmapAsPng(resized, outputPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveBitmapAsPng(bitmap, outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to extract frame: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all frames from a video
|
||||
/// </summary>
|
||||
public static void ExtractAllFrames(
|
||||
string videoPath,
|
||||
string outputFolder,
|
||||
Func<int, TimeSpan, string> fileNameGenerator,
|
||||
int targetWidth = -1,
|
||||
int targetHeight = -1,
|
||||
Action<int, int>? onProgress = null,
|
||||
Func<string, bool>? shouldSkipFrame = null)
|
||||
{
|
||||
if (!File.Exists(videoPath))
|
||||
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||
|
||||
if (!Directory.Exists(outputFolder))
|
||||
Directory.CreateDirectory(outputFolder);
|
||||
|
||||
try
|
||||
{
|
||||
using (var reader = new MFVideoReader(videoPath))
|
||||
{
|
||||
// Calculate total frames
|
||||
int totalFrames = (int)(reader.Duration.TotalSeconds * reader.FrameRate);
|
||||
int frameIndex = 0;
|
||||
TimeSpan currentTime = TimeSpan.Zero;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Read next frame
|
||||
using (var bitmap = reader.ReadFrame())
|
||||
{
|
||||
if (bitmap == null)
|
||||
break; // End of stream
|
||||
|
||||
// Generate filename
|
||||
var fileName = fileNameGenerator(frameIndex, currentTime);
|
||||
var fullPath = Path.Combine(outputFolder, fileName);
|
||||
|
||||
// Check if frame should be skipped
|
||||
if (shouldSkipFrame != null && shouldSkipFrame(fullPath))
|
||||
{
|
||||
frameIndex++;
|
||||
currentTime = TimeSpan.FromSeconds(frameIndex / reader.FrameRate);
|
||||
onProgress?.Invoke(frameIndex, totalFrames);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resize if needed
|
||||
if (targetWidth > 0 && targetHeight > 0 &&
|
||||
(targetWidth != bitmap.Width || targetHeight != bitmap.Height))
|
||||
{
|
||||
using (var resized = new Bitmap(bitmap, targetWidth, targetHeight))
|
||||
{
|
||||
SaveBitmapAsPng(resized, fullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveBitmapAsPng(bitmap, fullPath);
|
||||
}
|
||||
|
||||
frameIndex++;
|
||||
currentTime = TimeSpan.FromSeconds(frameIndex / reader.FrameRate);
|
||||
onProgress?.Invoke(frameIndex, totalFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to extract frames: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a bitmap as PNG file
|
||||
/// </summary>
|
||||
private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
bitmap.Save(outputPath, ImageFormat.Png);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ganimede.VideoProcessing.MediaFoundation
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional Media Foundation COM interfaces for frame extraction
|
||||
/// These interfaces extend the basic WMF functionality for video decoding
|
||||
/// </summary>
|
||||
|
||||
#region Source Reader Interfaces
|
||||
|
||||
[ComImport, Guid("70ae66f2-c809-4e4f-8915-bdcb406b7993")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFSourceReader
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetStreamSelection(
|
||||
int dwStreamIndex,
|
||||
out bool pfSelected);
|
||||
|
||||
[PreserveSig]
|
||||
int SetStreamSelection(
|
||||
int dwStreamIndex,
|
||||
bool fSelected);
|
||||
|
||||
[PreserveSig]
|
||||
int GetNativeMediaType(
|
||||
int dwStreamIndex,
|
||||
int dwMediaTypeIndex,
|
||||
out IMFMediaType ppMediaType);
|
||||
|
||||
[PreserveSig]
|
||||
int GetCurrentMediaType(
|
||||
int dwStreamIndex,
|
||||
out IMFMediaType ppMediaType);
|
||||
|
||||
[PreserveSig]
|
||||
int SetCurrentMediaType(
|
||||
int dwStreamIndex,
|
||||
IntPtr pdwReserved,
|
||||
IMFMediaType pMediaType);
|
||||
|
||||
[PreserveSig]
|
||||
int SetCurrentPosition(
|
||||
Guid guidTimeFormat,
|
||||
IntPtr varPosition);
|
||||
|
||||
[PreserveSig]
|
||||
int ReadSample(
|
||||
int dwStreamIndex,
|
||||
int dwControlFlags,
|
||||
out int pdwActualStreamIndex,
|
||||
out MF_SOURCE_READER_FLAG pdwStreamFlags,
|
||||
out long pllTimestamp,
|
||||
out IMFSample ppSample);
|
||||
|
||||
[PreserveSig]
|
||||
int Flush(int dwStreamIndex);
|
||||
|
||||
[PreserveSig]
|
||||
int GetServiceForStream(
|
||||
int dwStreamIndex,
|
||||
Guid guidService,
|
||||
Guid riid,
|
||||
out IntPtr ppvObject);
|
||||
|
||||
[PreserveSig]
|
||||
int GetPresentationAttribute(
|
||||
int dwStreamIndex,
|
||||
Guid guidAttribute,
|
||||
IntPtr pvarAttribute);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum MF_SOURCE_READER_FLAG
|
||||
{
|
||||
None = 0,
|
||||
Error = 0x00000001,
|
||||
EndOfStream = 0x00000002,
|
||||
NewStream = 0x00000004,
|
||||
NativeMediaTypeChanged = 0x00000010,
|
||||
CurrentMediaTypeChanged = 0x00000020,
|
||||
StreamTick = 0x00000100,
|
||||
AllEffectsRemoved = 0x00000200
|
||||
}
|
||||
|
||||
internal static class MFSourceReaderIndex
|
||||
{
|
||||
public const int FirstVideoStream = unchecked((int)0xFFFFFFFC);
|
||||
public const int FirstAudioStream = unchecked((int)0xFFFFFFFD);
|
||||
public const int MediaSource = unchecked((int)0xFFFFFFFE);
|
||||
public const int AnyStream = unchecked((int)0xFFFFFFFE);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample and Buffer Interfaces
|
||||
|
||||
[ComImport, Guid("c40a00f2-b93a-4d80-ae8c-5a1c634f58e4")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFSample : IMFAttributes
|
||||
{
|
||||
#region IMFAttributes methods
|
||||
[PreserveSig] new int GetItem(Guid guidKey, IntPtr pValue);
|
||||
[PreserveSig] new int GetItemType(Guid guidKey, out ushort pType);
|
||||
[PreserveSig] new int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||
[PreserveSig] new int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||
[PreserveSig] new int GetUINT32(Guid guidKey, out int punValue);
|
||||
[PreserveSig] new int GetUINT64(Guid guidKey, out long punValue);
|
||||
[PreserveSig] new int GetDouble(Guid guidKey, out double pfValue);
|
||||
[PreserveSig] new int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||
[PreserveSig] new int GetStringLength(Guid guidKey, out int pcchLength);
|
||||
[PreserveSig] new int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||
[PreserveSig] new int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||
[PreserveSig] new int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||
[PreserveSig] new int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||
[PreserveSig] new int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||
[PreserveSig] new int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] new int SetItem(Guid guidKey, IntPtr Value);
|
||||
[PreserveSig] new int DeleteItem(Guid guidKey);
|
||||
[PreserveSig] new int DeleteAllItems();
|
||||
[PreserveSig] new int SetUINT32(Guid guidKey, int unValue);
|
||||
[PreserveSig] new int SetUINT64(Guid guidKey, long unValue);
|
||||
[PreserveSig] new int SetDouble(Guid guidKey, double fValue);
|
||||
[PreserveSig] new int SetGUID(Guid guidKey, Guid guidValue);
|
||||
[PreserveSig] new int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||
[PreserveSig] new int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||
[PreserveSig] new int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||
[PreserveSig] new int LockStore();
|
||||
[PreserveSig] new int UnlockStore();
|
||||
[PreserveSig] new int GetCount(out int pcItems);
|
||||
[PreserveSig] new int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||
[PreserveSig] new int CopyAllItems(IMFAttributes pDest);
|
||||
#endregion
|
||||
|
||||
[PreserveSig] int GetSampleFlags(out int pdwSampleFlags);
|
||||
[PreserveSig] int SetSampleFlags(int dwSampleFlags);
|
||||
[PreserveSig] int GetSampleTime(out long phnsSampleTime);
|
||||
[PreserveSig] int SetSampleTime(long hnsSampleTime);
|
||||
[PreserveSig] int GetSampleDuration(out long phnsSampleDuration);
|
||||
[PreserveSig] int SetSampleDuration(long hnsSampleDuration);
|
||||
[PreserveSig] int GetBufferCount(out int pdwBufferCount);
|
||||
[PreserveSig] int GetBufferByIndex(int dwIndex, out IMFMediaBuffer ppBuffer);
|
||||
[PreserveSig] int ConvertToContiguousBuffer(out IMFMediaBuffer ppBuffer);
|
||||
[PreserveSig] int AddBuffer(IMFMediaBuffer pBuffer);
|
||||
[PreserveSig] int RemoveBufferByIndex(int dwIndex);
|
||||
[PreserveSig] int RemoveAllBuffers();
|
||||
[PreserveSig] int GetTotalLength(out int pcbTotalLength);
|
||||
[PreserveSig] int CopyToBuffer(IMFMediaBuffer pBuffer);
|
||||
}
|
||||
|
||||
[ComImport, Guid("045FA593-8799-42b8-BC8D-8968C6453507")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFMediaBuffer
|
||||
{
|
||||
[PreserveSig] int Lock(out IntPtr ppbBuffer, out int pcbMaxLength, out int pcbCurrentLength);
|
||||
[PreserveSig] int Unlock();
|
||||
[PreserveSig] int GetCurrentLength(out int pcbCurrentLength);
|
||||
[PreserveSig] int SetCurrentLength(int cbCurrentLength);
|
||||
[PreserveSig] int GetMaxLength(out int pcbMaxLength);
|
||||
}
|
||||
|
||||
[ComImport, Guid("7DC9D5F9-9ED9-44ec-9BBF-0600BB589FBB")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMF2DBuffer
|
||||
{
|
||||
[PreserveSig] int Lock2D(out IntPtr pbScanline0, out int plPitch);
|
||||
[PreserveSig] int Unlock2D();
|
||||
[PreserveSig] int GetScanline0AndPitch(out IntPtr pbScanline0, out int plPitch);
|
||||
[PreserveSig] int IsContiguousFormat(out bool pfIsContiguous);
|
||||
[PreserveSig] int GetContiguousLength(out int pcbLength);
|
||||
[PreserveSig] int ContiguousCopyTo(IntPtr pbDestBuffer, int cbDestBuffer);
|
||||
[PreserveSig] int ContiguousCopyFrom(IntPtr pbSrcBuffer, int cbSrcBuffer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Functions and Constants
|
||||
|
||||
internal static class MFExternExtended
|
||||
{
|
||||
[DllImport("mfreadwrite.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFCreateSourceReaderFromURL(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string pwszURL,
|
||||
IMFAttributes pAttributes,
|
||||
out IMFSourceReader ppSourceReader);
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFCreateMediaType(out IMFMediaType ppMFType);
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFCreateAttributes(out IMFAttributes ppMFAttributes, int cInitialSize);
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFCreateMemoryBuffer(int cbMaxLength, out IMFMediaBuffer ppBuffer);
|
||||
}
|
||||
|
||||
internal static class MFVideoFormat
|
||||
{
|
||||
// Uncompressed RGB formats
|
||||
public static readonly Guid RGB32 = new Guid("00000016-0000-0010-8000-00aa00389b71");
|
||||
public static readonly Guid RGB24 = new Guid("00000014-0000-0010-8000-00aa00389b71");
|
||||
public static readonly Guid RGB555 = new Guid("00000018-0000-0010-8000-00aa00389b71");
|
||||
public static readonly Guid RGB565 = new Guid("00000017-0000-0010-8000-00aa00389b71");
|
||||
|
||||
// YUV formats
|
||||
public static readonly Guid NV12 = new Guid("3231564E-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid YUY2 = new Guid("32595559-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid UYVY = new Guid("59565955-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid YV12 = new Guid("32315659-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid I420 = new Guid("30323449-0000-0010-8000-00AA00389B71");
|
||||
}
|
||||
|
||||
internal static class MFAttributesClsidExtended
|
||||
{
|
||||
// Source Reader attributes
|
||||
public static readonly Guid MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING =
|
||||
new Guid("fb394f3d-ccf1-42ee-bbb3-f9b845d5681d");
|
||||
public static readonly Guid MF_SOURCE_READER_DISABLE_DXVA =
|
||||
new Guid("aa456cfd-3943-4a1e-a77d-1838c0ea2e35");
|
||||
public static readonly Guid MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING =
|
||||
new Guid("0f81da2c-b537-4672-a8b2-a681b17307a3");
|
||||
|
||||
// Low latency mode
|
||||
public static readonly Guid MF_LOW_LATENCY =
|
||||
new Guid("9c27891a-ed7a-40e1-88e8-b22727a024ee");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Base Interfaces (referenced from VideoAnalyzer)
|
||||
|
||||
[ComImport, Guid("2CD2D921-C447-44A7-A13C-4ADABFC247E3")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFAttributes
|
||||
{
|
||||
[PreserveSig] int GetItem(Guid guidKey, IntPtr pValue);
|
||||
[PreserveSig] int GetItemType(Guid guidKey, out ushort pType);
|
||||
[PreserveSig] int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||
[PreserveSig] int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||
[PreserveSig] int GetUINT32(Guid guidKey, out int punValue);
|
||||
[PreserveSig] int GetUINT64(Guid guidKey, out long punValue);
|
||||
[PreserveSig] int GetDouble(Guid guidKey, out double pfValue);
|
||||
[PreserveSig] int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||
[PreserveSig] int GetStringLength(Guid guidKey, out int pcchLength);
|
||||
[PreserveSig] int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||
[PreserveSig] int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||
[PreserveSig] int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||
[PreserveSig] int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||
[PreserveSig] int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||
[PreserveSig] int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int SetItem(Guid guidKey, IntPtr Value);
|
||||
[PreserveSig] int DeleteItem(Guid guidKey);
|
||||
[PreserveSig] int DeleteAllItems();
|
||||
[PreserveSig] int SetUINT32(Guid guidKey, int unValue);
|
||||
[PreserveSig] int SetUINT64(Guid guidKey, long unValue);
|
||||
[PreserveSig] int SetDouble(Guid guidKey, double fValue);
|
||||
[PreserveSig] int SetGUID(Guid guidKey, Guid guidValue);
|
||||
[PreserveSig] int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||
[PreserveSig] int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||
[PreserveSig] int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||
[PreserveSig] int LockStore();
|
||||
[PreserveSig] int UnlockStore();
|
||||
[PreserveSig] int GetCount(out int pcItems);
|
||||
[PreserveSig] int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||
[PreserveSig] int CopyAllItems(IMFAttributes pDest);
|
||||
}
|
||||
|
||||
[ComImport, Guid("44AE0FA8-EA31-4109-8D2E-4CAE4997C555")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFMediaType : IMFAttributes
|
||||
{
|
||||
#region IMFAttributes methods
|
||||
[PreserveSig] new int GetItem(Guid guidKey, IntPtr pValue);
|
||||
[PreserveSig] new int GetItemType(Guid guidKey, out ushort pType);
|
||||
[PreserveSig] new int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||
[PreserveSig] new int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||
[PreserveSig] new int GetUINT32(Guid guidKey, out int punValue);
|
||||
[PreserveSig] new int GetUINT64(Guid guidKey, out long punValue);
|
||||
[PreserveSig] new int GetDouble(Guid guidKey, out double pfValue);
|
||||
[PreserveSig] new int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||
[PreserveSig] new int GetStringLength(Guid guidKey, out int pcchLength);
|
||||
[PreserveSig] new int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||
[PreserveSig] new int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||
[PreserveSig] new int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||
[PreserveSig] new int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||
[PreserveSig] new int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||
[PreserveSig] new int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] new int SetItem(Guid guidKey, IntPtr Value);
|
||||
[PreserveSig] new int DeleteItem(Guid guidKey);
|
||||
[PreserveSig] new int DeleteAllItems();
|
||||
[PreserveSig] new int SetUINT32(Guid guidKey, int unValue);
|
||||
[PreserveSig] new int SetUINT64(Guid guidKey, long unValue);
|
||||
[PreserveSig] new int SetDouble(Guid guidKey, double fValue);
|
||||
[PreserveSig] new int SetGUID(Guid guidKey, Guid guidValue);
|
||||
[PreserveSig] new int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||
[PreserveSig] new int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||
[PreserveSig] new int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||
[PreserveSig] new int LockStore();
|
||||
[PreserveSig] new int UnlockStore();
|
||||
[PreserveSig] new int GetCount(out int pcItems);
|
||||
[PreserveSig] new int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||
[PreserveSig] new int CopyAllItems(IMFAttributes pDest);
|
||||
#endregion
|
||||
|
||||
[PreserveSig] int GetMajorType(out Guid pguidMajorType);
|
||||
[PreserveSig] int IsCompressedFormat(out bool pfCompressed);
|
||||
[PreserveSig] int IsEqual(IMFMediaType pIMediaType, out uint pdwFlags);
|
||||
[PreserveSig] int GetRepresentation(Guid guidRepresentation, out IntPtr ppvRepresentation);
|
||||
[PreserveSig] int FreeRepresentation(Guid guidRepresentation, IntPtr pvRepresentation);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ganimede.VideoProcessing.MediaFoundation
|
||||
{
|
||||
/// <summary>
|
||||
/// Video reader class using Windows Media Foundation Source Reader
|
||||
/// Handles frame extraction from video files
|
||||
/// </summary>
|
||||
public class MFVideoReader : IDisposable
|
||||
{
|
||||
private IMFSourceReader? _sourceReader;
|
||||
private bool _disposed = false;
|
||||
private int _videoStreamIndex = 0;
|
||||
private int _width;
|
||||
private int _height;
|
||||
private double _frameRate;
|
||||
private TimeSpan _duration;
|
||||
|
||||
public int Width => _width;
|
||||
public int Height => _height;
|
||||
public double FrameRate => _frameRate;
|
||||
public TimeSpan Duration => _duration;
|
||||
|
||||
public MFVideoReader(string videoPath)
|
||||
{
|
||||
if (!File.Exists(videoPath))
|
||||
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||
|
||||
// Initialize Media Foundation
|
||||
int hr = MFExtern.MFStartup(MFExtern.MF_VERSION, 0);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
try
|
||||
{
|
||||
// Create attributes for source reader
|
||||
IMFAttributes? attributes = null;
|
||||
hr = MFExternExtended.MFCreateAttributes(out attributes, 2);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Enable video processing
|
||||
hr = attributes!.SetUINT32(MFAttributesClsidExtended.MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, 1);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Low latency mode
|
||||
hr = attributes.SetUINT32(MFAttributesClsidExtended.MF_LOW_LATENCY, 1);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Create source reader
|
||||
hr = MFExternExtended.MFCreateSourceReaderFromURL(videoPath, attributes, out _sourceReader);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Find first video stream
|
||||
_videoStreamIndex = MFSourceReaderIndex.FirstVideoStream;
|
||||
|
||||
// Configure output media type (RGB32)
|
||||
IMFMediaType? mediaType = null;
|
||||
hr = MFExternExtended.MFCreateMediaType(out mediaType);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
try
|
||||
{
|
||||
hr = mediaType!.SetGUID(MFAttributesClsid.MF_MT_MAJOR_TYPE, MFMediaType.Video);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
hr = mediaType.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFVideoFormat.RGB32);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
hr = _sourceReader!.SetCurrentMediaType(_videoStreamIndex, IntPtr.Zero, mediaType);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mediaType != null) Marshal.ReleaseComObject(mediaType);
|
||||
}
|
||||
|
||||
// Get actual output media type
|
||||
IMFMediaType? currentMediaType = null;
|
||||
hr = _sourceReader.GetCurrentMediaType(_videoStreamIndex, out currentMediaType);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
try
|
||||
{
|
||||
// Get frame size
|
||||
long frameSize;
|
||||
hr = currentMediaType!.GetUINT64(MFAttributesClsid.MF_MT_FRAME_SIZE, out frameSize);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
_width = (int)(frameSize >> 32);
|
||||
_height = (int)(frameSize & 0xFFFFFFFF);
|
||||
|
||||
// Get frame rate
|
||||
long frameRateRatio;
|
||||
hr = currentMediaType.GetUINT64(MFAttributesClsid.MF_MT_FRAME_RATE, out frameRateRatio);
|
||||
if (hr >= 0)
|
||||
{
|
||||
int numerator = (int)(frameRateRatio >> 32);
|
||||
int denominator = (int)(frameRateRatio & 0xFFFFFFFF);
|
||||
_frameRate = denominator > 0 ? (double)numerator / denominator : 30.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_frameRate = 30.0; // Default
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (currentMediaType != null) Marshal.ReleaseComObject(currentMediaType);
|
||||
}
|
||||
|
||||
// Get duration
|
||||
IntPtr durationPtr = IntPtr.Zero;
|
||||
hr = _sourceReader.GetPresentationAttribute(
|
||||
MFSourceReaderIndex.MediaSource,
|
||||
MFAttributesClsid.MF_PD_DURATION,
|
||||
durationPtr);
|
||||
|
||||
if (hr >= 0 && durationPtr != IntPtr.Zero)
|
||||
{
|
||||
long durationTicks = Marshal.ReadInt64(durationPtr);
|
||||
_duration = TimeSpan.FromTicks(durationTicks / 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
_duration = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
// Cleanup attributes
|
||||
if (attributes != null) Marshal.ReleaseComObject(attributes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeks to a specific time position in the video
|
||||
/// </summary>
|
||||
public void SeekTo(TimeSpan position)
|
||||
{
|
||||
if (_disposed || _sourceReader == null)
|
||||
throw new ObjectDisposedException(nameof(MFVideoReader));
|
||||
|
||||
long timeInHundredNanoseconds = position.Ticks * 10;
|
||||
IntPtr varPosition = Marshal.AllocHGlobal(16); // PROPVARIANT size
|
||||
|
||||
try
|
||||
{
|
||||
// Write PROPVARIANT structure
|
||||
// VT_I8 = 20 (64-bit signed integer)
|
||||
Marshal.WriteInt16(varPosition, 0, 20); // vt
|
||||
Marshal.WriteInt64(varPosition, 8, timeInHundredNanoseconds); // hVal
|
||||
|
||||
int hr = _sourceReader.SetCurrentPosition(Guid.Empty, varPosition);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(varPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next video frame
|
||||
/// </summary>
|
||||
public Bitmap? ReadFrame()
|
||||
{
|
||||
if (_disposed || _sourceReader == null)
|
||||
throw new ObjectDisposedException(nameof(MFVideoReader));
|
||||
|
||||
IMFSample? sample = null;
|
||||
int streamIndex;
|
||||
MF_SOURCE_READER_FLAG flags;
|
||||
long timestamp;
|
||||
|
||||
int hr = _sourceReader.ReadSample(
|
||||
_videoStreamIndex,
|
||||
0,
|
||||
out streamIndex,
|
||||
out flags,
|
||||
out timestamp,
|
||||
out sample);
|
||||
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Check for end of stream
|
||||
if ((flags & MF_SOURCE_READER_FLAG.EndOfStream) != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sample == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return ConvertSampleToBitmap(sample);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(sample);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a frame at a specific time position
|
||||
/// </summary>
|
||||
public Bitmap? ReadFrameAtTime(TimeSpan position)
|
||||
{
|
||||
SeekTo(position);
|
||||
return ReadFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an IMFSample to a Bitmap
|
||||
/// </summary>
|
||||
private Bitmap ConvertSampleToBitmap(IMFSample sample)
|
||||
{
|
||||
IMFMediaBuffer? buffer = null;
|
||||
int hr = sample.ConvertToContiguousBuffer(out buffer);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr pData;
|
||||
int cbMaxLength, cbCurrentLength;
|
||||
|
||||
hr = buffer!.Lock(out pData, out cbMaxLength, out cbCurrentLength);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
try
|
||||
{
|
||||
// Create bitmap
|
||||
Bitmap bitmap = new Bitmap(_width, _height, PixelFormat.Format32bppRgb);
|
||||
|
||||
// Lock bitmap data
|
||||
Rectangle rect = new Rectangle(0, 0, _width, _height);
|
||||
BitmapData bmpData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
|
||||
|
||||
try
|
||||
{
|
||||
int stride = _width * 4; // 4 bytes per pixel (RGB32)
|
||||
|
||||
// Copy data line by line (bottom-up for RGB32)
|
||||
for (int y = 0; y < _height; y++)
|
||||
{
|
||||
IntPtr srcLine = IntPtr.Add(pData, y * stride);
|
||||
IntPtr dstLine = IntPtr.Add(bmpData.Scan0, (_height - 1 - y) * bmpData.Stride);
|
||||
|
||||
// Copy scanline
|
||||
unsafe
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
srcLine.ToPointer(),
|
||||
dstLine.ToPointer(),
|
||||
bmpData.Stride,
|
||||
stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap.UnlockBits(bmpData);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
finally
|
||||
{
|
||||
buffer.Unlock();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (buffer != null) Marshal.ReleaseComObject(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (_sourceReader != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(_sourceReader);
|
||||
_sourceReader = null;
|
||||
}
|
||||
|
||||
MFExtern.MFShutdown();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~MFVideoReader()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#region Helper Classes from VideoAnalyzer
|
||||
|
||||
internal static class MFExtern
|
||||
{
|
||||
private const uint MF_SDK_VERSION = 0x0002;
|
||||
private const uint MF_API_VERSION = 0x0070;
|
||||
internal const uint MF_VERSION = (MF_SDK_VERSION << 16) | MF_API_VERSION;
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFStartup(uint Version, uint dwFlags = 0);
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFShutdown();
|
||||
}
|
||||
|
||||
internal static class MFAttributesClsid
|
||||
{
|
||||
public static readonly Guid MF_PD_DURATION = new Guid("6c990d33-bb8e-477a-8598-0d5d96fcd88a");
|
||||
public static readonly Guid MF_MT_MAJOR_TYPE = new Guid("48eba18e-f8c9-4687-bf11-0a74c9f96a8f");
|
||||
public static readonly Guid MF_MT_SUBTYPE = new Guid("f7e34c9a-42e8-4714-b74b-cb29d72c35e5");
|
||||
public static readonly Guid MF_MT_FRAME_SIZE = new Guid("1652c33d-d6b2-4012-b834-72030849a37d");
|
||||
public static readonly Guid MF_MT_FRAME_RATE = new Guid("c459a2e8-3d2c-4e44-b132-fee5156c7bb0");
|
||||
}
|
||||
|
||||
internal static class MFMediaType
|
||||
{
|
||||
public static readonly Guid Video = new Guid("73646976-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid Audio = new Guid("73647561-0000-0010-8000-00AA00389B71");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
312
Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs
Normal file
312
Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ganimede.VideoProcessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides video analysis capabilities using native Windows Media Foundation
|
||||
/// NO external dependencies required - uses only Windows built-in APIs
|
||||
/// </summary>
|
||||
public class VideoAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyzes a video file and returns its metadata using Windows Media Foundation Source Reader
|
||||
/// </summary>
|
||||
public static VideoMetadata Analyze(string videoPath)
|
||||
{
|
||||
if (!File.Exists(videoPath))
|
||||
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||
|
||||
// Initialize Media Foundation
|
||||
int hr = MFExtern.MFStartup(MFExtern.MF_VERSION, 0);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr pSourceReader = IntPtr.Zero;
|
||||
IMFSourceReader? sourceReader = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Create source reader from URL
|
||||
hr = MFExtern.MFCreateSourceReaderFromURL(videoPath, IntPtr.Zero, out pSourceReader);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Convert to interface
|
||||
sourceReader = (IMFSourceReader)Marshal.GetObjectForIUnknown(pSourceReader);
|
||||
|
||||
// Get native media type for first video stream
|
||||
IntPtr pMediaType = IntPtr.Zero;
|
||||
hr = sourceReader!.GetNativeMediaType(
|
||||
MFSourceReaderIndex.FirstVideoStream,
|
||||
0,
|
||||
out pMediaType);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
IMFMediaType? mediaType = null;
|
||||
try
|
||||
{
|
||||
mediaType = (IMFMediaType)Marshal.GetObjectForIUnknown(pMediaType);
|
||||
|
||||
// Get frame size
|
||||
long frameSize;
|
||||
hr = mediaType!.GetUINT64(MFAttributesClsid.MF_MT_FRAME_SIZE, out frameSize);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
int width = (int)(frameSize >> 32);
|
||||
int height = (int)(frameSize & 0xFFFFFFFF);
|
||||
|
||||
// Get frame rate
|
||||
long frameRateRatio;
|
||||
hr = mediaType.GetUINT64(MFAttributesClsid.MF_MT_FRAME_RATE, out frameRateRatio);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
int frameRateNumerator = (int)(frameRateRatio >> 32);
|
||||
int frameRateDenominator = (int)(frameRateRatio & 0xFFFFFFFF);
|
||||
double fps = frameRateDenominator > 0 ? (double)frameRateNumerator / frameRateDenominator : 30.0;
|
||||
|
||||
// Get codec subtype
|
||||
Guid subType;
|
||||
hr = mediaType.GetGUID(MFAttributesClsid.MF_MT_SUBTYPE, out subType);
|
||||
string codecName = GetCodecName(subType);
|
||||
|
||||
// Get duration from presentation attribute
|
||||
IntPtr varDuration = Marshal.AllocHGlobal(16); // PROPVARIANT size
|
||||
try
|
||||
{
|
||||
hr = sourceReader.GetPresentationAttribute(
|
||||
MFSourceReaderIndex.MediaSource,
|
||||
MFAttributesClsid.MF_PD_DURATION,
|
||||
varDuration);
|
||||
|
||||
TimeSpan duration = TimeSpan.Zero;
|
||||
if (hr >= 0)
|
||||
{
|
||||
// Read PROPVARIANT (VT_I8)
|
||||
long durationTicks = Marshal.ReadInt64(varDuration, 8);
|
||||
duration = TimeSpan.FromTicks(durationTicks / 10); // Convert from 100-nanosecond units
|
||||
}
|
||||
|
||||
// Calculate total frames
|
||||
int totalFrames = (int)(duration.TotalSeconds * fps);
|
||||
|
||||
// Get bitrate (approximate from file size)
|
||||
long fileSize = new FileInfo(videoPath).Length;
|
||||
long bitrate = duration.TotalSeconds > 0 ? (long)((fileSize * 8) / duration.TotalSeconds) : 0;
|
||||
|
||||
return new VideoMetadata
|
||||
{
|
||||
Duration = duration,
|
||||
FrameRate = fps,
|
||||
TotalFrames = totalFrames,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = bitrate,
|
||||
CodecName = codecName
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(varDuration);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mediaType != null) Marshal.ReleaseComObject(mediaType);
|
||||
if (pMediaType != IntPtr.Zero) Marshal.Release(pMediaType);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (sourceReader != null) Marshal.ReleaseComObject(sourceReader);
|
||||
if (pSourceReader != IntPtr.Zero) Marshal.Release(pSourceReader);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
MFExtern.MFShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCodecName(Guid subType)
|
||||
{
|
||||
if (subType == MFMediaType.H264) return "H.264";
|
||||
if (subType == MFMediaType.HEVC) return "H.265/HEVC";
|
||||
if (subType == MFMediaType.MP4V) return "MPEG-4";
|
||||
if (subType == MFMediaType.WMV3) return "WMV3";
|
||||
if (subType == MFMediaType.VP80) return "VP8";
|
||||
if (subType == MFMediaType.VP90) return "VP9";
|
||||
if (subType == MFMediaType.AV1) return "AV1";
|
||||
if (subType == MFMediaType.MJPG) return "Motion JPEG";
|
||||
|
||||
return $"Unknown ({subType})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains metadata information about a video file
|
||||
/// </summary>
|
||||
public class VideoMetadata
|
||||
{
|
||||
public TimeSpan Duration { get; set; }
|
||||
public double FrameRate { get; set; }
|
||||
public int TotalFrames { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public long BitRate { get; set; }
|
||||
public string CodecName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#region Windows Media Foundation P/Invoke Declarations
|
||||
|
||||
internal static class MFExtern
|
||||
{
|
||||
private const uint MF_SDK_VERSION = 0x0002;
|
||||
private const uint MF_API_VERSION = 0x0070;
|
||||
internal const uint MF_VERSION = (MF_SDK_VERSION << 16) | MF_API_VERSION;
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFStartup(uint Version, uint dwFlags = 0);
|
||||
|
||||
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFShutdown();
|
||||
|
||||
[DllImport("mfreadwrite.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
internal static extern int MFCreateSourceReaderFromURL(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string pwszURL,
|
||||
IntPtr pAttributes,
|
||||
out IntPtr ppSourceReader);
|
||||
}
|
||||
|
||||
[ComImport, Guid("70ae66f2-c809-4e4f-8915-bdcb406b7993")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFSourceReader
|
||||
{
|
||||
[PreserveSig] int GetStreamSelection(int dwStreamIndex, out bool pfSelected);
|
||||
[PreserveSig] int SetStreamSelection(int dwStreamIndex, bool fSelected);
|
||||
[PreserveSig] int GetNativeMediaType(int dwStreamIndex, int dwMediaTypeIndex, out IntPtr ppMediaType);
|
||||
[PreserveSig] int GetCurrentMediaType(int dwStreamIndex, out IntPtr ppMediaType);
|
||||
[PreserveSig] int SetCurrentMediaType(int dwStreamIndex, IntPtr pdwReserved, IntPtr pMediaType);
|
||||
[PreserveSig] int SetCurrentPosition(Guid guidTimeFormat, IntPtr varPosition);
|
||||
[PreserveSig] int ReadSample(int dwStreamIndex, int dwControlFlags, out int pdwActualStreamIndex,
|
||||
out int pdwStreamFlags, out long pllTimestamp, out IntPtr ppSample);
|
||||
[PreserveSig] int Flush(int dwStreamIndex);
|
||||
[PreserveSig] int GetServiceForStream(int dwStreamIndex, Guid guidService, Guid riid, out IntPtr ppvObject);
|
||||
[PreserveSig] int GetPresentationAttribute(int dwStreamIndex, Guid guidAttribute, IntPtr pvarAttribute);
|
||||
}
|
||||
|
||||
internal static class MFSourceReaderIndex
|
||||
{
|
||||
public const int FirstVideoStream = unchecked((int)0xFFFFFFFC);
|
||||
public const int FirstAudioStream = unchecked((int)0xFFFFFFFD);
|
||||
public const int MediaSource = unchecked((int)0xFFFFFFFF);
|
||||
}
|
||||
|
||||
[ComImport, Guid("2CD2D921-C447-44A7-A13C-4ADABFC247E3")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFAttributes
|
||||
{
|
||||
[PreserveSig] int GetItem(Guid guidKey, IntPtr pValue);
|
||||
[PreserveSig] int GetItemType(Guid guidKey, out ushort pType);
|
||||
[PreserveSig] int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||
[PreserveSig] int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||
[PreserveSig] int GetUINT32(Guid guidKey, out int punValue);
|
||||
[PreserveSig] int GetUINT64(Guid guidKey, out long punValue);
|
||||
[PreserveSig] int GetDouble(Guid guidKey, out double pfValue);
|
||||
[PreserveSig] int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||
[PreserveSig] int GetStringLength(Guid guidKey, out int pcchLength);
|
||||
[PreserveSig] int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||
[PreserveSig] int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||
[PreserveSig] int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||
[PreserveSig] int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||
[PreserveSig] int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||
[PreserveSig] int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int SetItem(Guid guidKey, IntPtr Value);
|
||||
[PreserveSig] int DeleteItem(Guid guidKey);
|
||||
[PreserveSig] int DeleteAllItems();
|
||||
[PreserveSig] int SetUINT32(Guid guidKey, int unValue);
|
||||
[PreserveSig] int SetUINT64(Guid guidKey, long unValue);
|
||||
[PreserveSig] int SetDouble(Guid guidKey, double fValue);
|
||||
[PreserveSig] int SetGUID(Guid guidKey, Guid guidValue);
|
||||
[PreserveSig] int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||
[PreserveSig] int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||
[PreserveSig] int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||
[PreserveSig] int LockStore();
|
||||
[PreserveSig] int UnlockStore();
|
||||
[PreserveSig] int GetCount(out int pcItems);
|
||||
[PreserveSig] int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||
[PreserveSig] int CopyAllItems(IMFAttributes pDest);
|
||||
}
|
||||
|
||||
[ComImport, Guid("44AE0FA8-EA31-4109-8D2E-4CAE4997C555")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMFMediaType : IMFAttributes
|
||||
{
|
||||
#region IMFAttributes methods
|
||||
[PreserveSig] new int GetItem(Guid guidKey, IntPtr pValue);
|
||||
[PreserveSig] new int GetItemType(Guid guidKey, out ushort pType);
|
||||
[PreserveSig] new int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||
[PreserveSig] new int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||
[PreserveSig] new int GetUINT32(Guid guidKey, out int punValue);
|
||||
[PreserveSig] new int GetUINT64(Guid guidKey, out long punValue);
|
||||
[PreserveSig] new int GetDouble(Guid guidKey, out double pfValue);
|
||||
[PreserveSig] new int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||
[PreserveSig] new int GetStringLength(Guid guidKey, out int pcchLength);
|
||||
[PreserveSig] new int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||
[PreserveSig] new int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||
[PreserveSig] new int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||
[PreserveSig] new int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||
[PreserveSig] new int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||
[PreserveSig] new int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] new int SetItem(Guid guidKey, IntPtr Value);
|
||||
[PreserveSig] new int DeleteItem(Guid guidKey);
|
||||
[PreserveSig] new int DeleteAllItems();
|
||||
[PreserveSig] new int SetUINT32(Guid guidKey, int unValue);
|
||||
[PreserveSig] new int SetUINT64(Guid guidKey, long unValue);
|
||||
[PreserveSig] new int SetDouble(Guid guidKey, double fValue);
|
||||
[PreserveSig] new int SetGUID(Guid guidKey, Guid guidValue);
|
||||
[PreserveSig] new int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||
[PreserveSig] new int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||
[PreserveSig] new int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||
[PreserveSig] new int LockStore();
|
||||
[PreserveSig] new int UnlockStore();
|
||||
[PreserveSig] new int GetCount(out int pcItems);
|
||||
[PreserveSig] new int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||
[PreserveSig] new int CopyAllItems(IMFAttributes pDest);
|
||||
#endregion
|
||||
|
||||
[PreserveSig] int GetMajorType(out Guid pguidMajorType);
|
||||
[PreserveSig] int IsCompressedFormat(out bool pfCompressed);
|
||||
[PreserveSig] int IsEqual(IMFMediaType pIMediaType, out uint pdwFlags);
|
||||
[PreserveSig] int GetRepresentation(Guid guidRepresentation, out IntPtr ppvRepresentation);
|
||||
[PreserveSig] int FreeRepresentation(Guid guidRepresentation, IntPtr pvRepresentation);
|
||||
}
|
||||
|
||||
internal static class MFAttributesClsid
|
||||
{
|
||||
public static readonly Guid MF_PD_DURATION = new Guid("6c990d33-bb8e-477a-8598-0d5d96fcd88a");
|
||||
public static readonly Guid MF_MT_MAJOR_TYPE = new Guid("48eba18e-f8c9-4687-bf11-0a74c9f96a8f");
|
||||
public static readonly Guid MF_MT_SUBTYPE = new Guid("f7e34c9a-42e8-4714-b74b-cb29d72c35e5");
|
||||
public static readonly Guid MF_MT_FRAME_SIZE = new Guid("1652c33d-d6b2-4012-b834-72030849a37d");
|
||||
public static readonly Guid MF_MT_FRAME_RATE = new Guid("c459a2e8-3d2c-4e44-b132-fee5156c7bb0");
|
||||
}
|
||||
|
||||
internal static class MFMediaType
|
||||
{
|
||||
public static readonly Guid Video = new Guid("73646976-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid Audio = new Guid("73647561-0000-0010-8000-00AA00389B71");
|
||||
|
||||
// Video formats
|
||||
public static readonly Guid H264 = new Guid("34363248-0000-0010-8000-00aa00389b71");
|
||||
public static readonly Guid HEVC = new Guid("43564548-0000-0010-8000-00aa00389b71");
|
||||
public static readonly Guid MP4V = new Guid("5634504D-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid WMV3 = new Guid("33564D57-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid VP80 = new Guid("30385056-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid VP90 = new Guid("30395056-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid AV1 = new Guid("31305641-0000-0010-8000-00AA00389B71");
|
||||
public static readonly Guid MJPG = new Guid("47504A4D-0000-0010-8000-00AA00389B71");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
186
Ganimede/Ganimede/Windows/JobConfigWindow.xaml
Normal file
186
Ganimede/Ganimede/Windows/JobConfigWindow.xaml
Normal file
@@ -0,0 +1,186 @@
|
||||
<Window x:Class="Ganimede.Windows.JobConfigWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Configurazione Job" Height="640" Width="640"
|
||||
Background="#1E2228" WindowStartupLocation="CenterOwner">
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="PanelBrush" Color="#242A31"/>
|
||||
<SolidColorBrush x:Key="PanelSubBrush" Color="#2C333B"/>
|
||||
<SolidColorBrush x:Key="BorderBrushColor" Color="#38424D"/>
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#268BFF"/>
|
||||
<Style TargetType="GroupBox">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GroupBox">
|
||||
<Border Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="12" >
|
||||
<DockPanel>
|
||||
<Border Background="#2C333B" CornerRadius="4" Padding="6 4" Margin="0 0 0 10" DockPanel.Dock="Top">
|
||||
<ContentPresenter ContentSource="Header"/>
|
||||
</Border>
|
||||
<ContentPresenter/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="Button" x:Key="PrimaryButton">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Background" Value="#268BFF"/>
|
||||
<Setter Property="BorderBrush" Value="#1776DF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="14 8"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#3595FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#1570D4"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="Button" x:Key="FlatButton" BasedOn="{StaticResource PrimaryButton}">
|
||||
<Setter Property="Background" Value="#2F3740"/>
|
||||
<Setter Property="BorderBrush" Value="#404B55"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#38434D"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="#2C333B"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3A444E"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="6 4"/>
|
||||
</Style>
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="#2C333B"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3A444E"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="4 2"/>
|
||||
</Style>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Margin" Value="0 2 0 6"/>
|
||||
</Style>
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Margin" Value="0 0 14 0"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="0 0 0 14">
|
||||
<TextBlock Text="Configurazione Job" FontSize="22" FontWeight="SemiBold" Foreground="White"/>
|
||||
<TextBlock x:Name="SelectedJobsText" Text="0 job selezionati" Foreground="#9BA5AF" FontSize="12" Margin="0 4 0 0"/>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Output -->
|
||||
<GroupBox Header="Cartella di Output (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomOutputCheckBox" Content="Usa cartella di output personalizzata" Checked="UseCustomOutputCheckBox_CheckedChanged" Unchecked="UseCustomOutputCheckBox_CheckedChanged"/>
|
||||
<DockPanel Margin="0 4 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomOutputCheckBox}">
|
||||
<TextBox x:Name="CustomOutputTextBox" Height="34" VerticalContentAlignment="Center" DockPanel.Dock="Left" MinWidth="320" Margin="0 0 10 0"/>
|
||||
<Button x:Name="BrowseCustomOutputButton" Content="Sfoglia" Width="90" Height="34" Style="{StaticResource FlatButton}" Click="BrowseCustomOutputButton_Click"/>
|
||||
</DockPanel>
|
||||
<CheckBox x:Name="CreateSubfolderCheckBox" Content="Crea sottocartella per ogni video" IsChecked="True"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Extraction Mode -->
|
||||
<GroupBox Header="Modalita di Estrazione" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<RadioButton x:Name="ExtractionFullRadio" Content="Completa" GroupName="ExtractionMode" IsChecked="True"/>
|
||||
<RadioButton x:Name="ExtractionSingleRadio" Content="Singolo Frame" GroupName="ExtractionMode"/>
|
||||
<RadioButton x:Name="ExtractionAutoRadio" Content="Auto" GroupName="ExtractionMode"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Auto: analizza il video e decide se estrarre tutti i frame o solo uno." Foreground="#8D969F" FontSize="11" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Naming Pattern -->
|
||||
<GroupBox Header="Pattern Nome File (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomNamingCheckBox" Content="Usa pattern di naming personalizzato" Checked="UseCustomNamingCheckBox_CheckedChanged" Unchecked="UseCustomNamingCheckBox_CheckedChanged"/>
|
||||
<ComboBox x:Name="CustomNamingComboBox" Height="32" Margin="0 6 0 0" SelectionChanged="CustomNamingComboBox_SelectionChanged" IsEnabled="{Binding IsChecked, ElementName=UseCustomNamingCheckBox}">
|
||||
<ComboBoxItem Content="NomeVideo + Progressivo" Tag="VideoNameProgressive"/>
|
||||
<ComboBoxItem Content="Frame + Progressivo" Tag="FrameProgressive"/>
|
||||
<ComboBoxItem Content="NomeVideo + Timestamp" Tag="VideoNameTimestamp"/>
|
||||
<ComboBoxItem Content="Prefisso Custom + Progressivo" Tag="CustomProgressive"/>
|
||||
<ComboBoxItem Content="Solo Timestamp" Tag="TimestampOnly"/>
|
||||
<ComboBoxItem Content="NomeVideo + Frame + Progressivo" Tag="VideoNameFrameProgressive"/>
|
||||
</ComboBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomNamingCheckBox}">
|
||||
<TextBlock Text="Prefisso:" Foreground="#9BA5AF" VerticalAlignment="Center" Margin="0 0 8 0"/>
|
||||
<TextBox x:Name="CustomNamingPrefixTextBox" Width="160" Text="custom" Height="30" VerticalContentAlignment="Center" TextChanged="CustomNamingPrefixTextBox_TextChanged"/>
|
||||
</StackPanel>
|
||||
<Border Background="#2F3740" CornerRadius="4" Padding="6" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomNamingCheckBox}">
|
||||
<TextBlock x:Name="JobNamingPreviewText" Text="Video1_000001.png" FontFamily="Consolas" Foreground="#4FA6FF" FontSize="12"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Overwrite -->
|
||||
<GroupBox Header="Modalita Sovrascrittura (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomOverwriteCheckBox" Content="Usa comportamento di sovrascrittura personalizzato" Checked="UseCustomOverwriteCheckBox_CheckedChanged" Unchecked="UseCustomOverwriteCheckBox_CheckedChanged"/>
|
||||
<ComboBox x:Name="CustomOverwriteComboBox" Height="32" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomOverwriteCheckBox}">
|
||||
<ComboBoxItem Content="Chiedi" Tag="Ask"/>
|
||||
<ComboBoxItem Content="Salta" Tag="Skip"/>
|
||||
<ComboBoxItem Content="Sovrascrivi" Tag="Overwrite"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Frame size -->
|
||||
<GroupBox Header="Ridimensionamento Frame (Override facoltativo)" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="UseCustomFrameSizeCheckBox" Content="Usa dimensione frame personalizzata" Checked="UseCustomFrameSizeCheckBox_CheckedChanged" Unchecked="UseCustomFrameSizeCheckBox_CheckedChanged"/>
|
||||
<ComboBox x:Name="CustomFrameSizeComboBox" Height="32" Margin="0 6 0 0" IsEnabled="{Binding IsChecked, ElementName=UseCustomFrameSizeCheckBox}">
|
||||
<ComboBoxItem Content="Risoluzione Originale" Tag="original"/>
|
||||
<ComboBoxItem Content="320x180 (Veloce)" Tag="320,180"/>
|
||||
<ComboBoxItem Content="640x360 (Media)" Tag="640,360"/>
|
||||
<ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/>
|
||||
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 16 0 0">
|
||||
<Button x:Name="ApplyButton" Content="Applica" Width="140" Height="42" Style="{StaticResource PrimaryButton}" Click="ApplyButton_Click" Margin="0,0,12,0"/>
|
||||
<Button x:Name="CancelButton" Content="Annulla" Width="110" Height="42" Style="{StaticResource FlatButton}" Click="CancelButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
206
Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
Normal file
206
Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ganimede.Models;
|
||||
using Ganimede.Helpers;
|
||||
using WpfMessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace Ganimede.Windows
|
||||
{
|
||||
public partial class JobConfigWindow : Window
|
||||
{
|
||||
private List<VideoJob> _selectedJobs;
|
||||
|
||||
public JobConfigWindow(List<VideoJob> selectedJobs)
|
||||
{
|
||||
InitializeComponent();
|
||||
_selectedJobs = selectedJobs;
|
||||
SelectedJobsText.Text = $"{selectedJobs.Count} job selezionati";
|
||||
LoadCurrentSettings();
|
||||
}
|
||||
|
||||
private void SetExtractionModeRadio(ExtractionMode mode)
|
||||
{
|
||||
if (FindName("ExtractionFullRadio") is System.Windows.Controls.RadioButton fullRb) fullRb.IsChecked = mode == ExtractionMode.Full;
|
||||
if (FindName("ExtractionSingleRadio") is System.Windows.Controls.RadioButton singleRb) singleRb.IsChecked = mode == ExtractionMode.SingleFrame;
|
||||
if (FindName("ExtractionAutoRadio") is System.Windows.Controls.RadioButton autoRb) autoRb.IsChecked = mode == ExtractionMode.Auto;
|
||||
}
|
||||
|
||||
private ExtractionMode GetSelectedExtractionMode()
|
||||
{
|
||||
if (FindName("ExtractionSingleRadio") is System.Windows.Controls.RadioButton singleRb && singleRb.IsChecked == true) return ExtractionMode.SingleFrame;
|
||||
if (FindName("ExtractionAutoRadio") is System.Windows.Controls.RadioButton autoRb && autoRb.IsChecked == true) return ExtractionMode.Auto;
|
||||
return ExtractionMode.Full;
|
||||
}
|
||||
|
||||
private void LoadCurrentSettings()
|
||||
{
|
||||
var firstJob = _selectedJobs.FirstOrDefault();
|
||||
if (firstJob != null)
|
||||
{
|
||||
SetExtractionModeRadio(firstJob.ExtractionMode);
|
||||
if (!string.IsNullOrEmpty(firstJob.CustomOutputFolder))
|
||||
{
|
||||
UseCustomOutputCheckBox.IsChecked = true;
|
||||
CustomOutputTextBox.Text = firstJob.CustomOutputFolder;
|
||||
}
|
||||
CreateSubfolderCheckBox.IsChecked = firstJob.CustomCreateSubfolder;
|
||||
|
||||
if (!string.IsNullOrEmpty(firstJob.CustomFrameSize))
|
||||
{
|
||||
UseCustomFrameSizeCheckBox.IsChecked = true;
|
||||
foreach (ComboBoxItem item in CustomFrameSizeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == firstJob.CustomFrameSize)
|
||||
{
|
||||
CustomFrameSizeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstJob.CustomOverwriteMode.HasValue)
|
||||
{
|
||||
UseCustomOverwriteCheckBox.IsChecked = true;
|
||||
foreach (ComboBoxItem item in CustomOverwriteComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == firstJob.CustomOverwriteMode.Value.ToString())
|
||||
{
|
||||
CustomOverwriteComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstJob.CustomNamingPattern.HasValue)
|
||||
{
|
||||
UseCustomNamingCheckBox.IsChecked = true;
|
||||
foreach (ComboBoxItem item in CustomNamingComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == firstJob.CustomNamingPattern.Value.ToString())
|
||||
{
|
||||
CustomNamingComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CustomNamingPrefixTextBox.Text = firstJob.CustomPrefix ?? "custom";
|
||||
}
|
||||
}
|
||||
|
||||
if (CustomFrameSizeComboBox.SelectedItem == null) CustomFrameSizeComboBox.SelectedIndex = 0;
|
||||
if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0;
|
||||
if (CustomNamingComboBox.SelectedItem == null) CustomNamingComboBox.SelectedIndex = 0;
|
||||
|
||||
// Defer preview update until window is fully loaded
|
||||
Dispatcher.InvokeAsync(() => UpdateJobNamingPreview(), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void UpdateJobNamingPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if controls are initialized
|
||||
if (UseCustomNamingCheckBox == null || CustomNamingComboBox == null ||
|
||||
CustomNamingPrefixTextBox == null || JobNamingPreviewText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (UseCustomNamingCheckBox.IsChecked == true &&
|
||||
CustomNamingComboBox.SelectedItem is ComboBoxItem selectedItem &&
|
||||
Enum.TryParse<NamingPattern>(selectedItem.Tag?.ToString(), out var pattern))
|
||||
{
|
||||
var firstVideoName = _selectedJobs.FirstOrDefault()?.VideoName ?? "Video1";
|
||||
var customPrefix = string.IsNullOrWhiteSpace(CustomNamingPrefixTextBox.Text) ? "custom" : CustomNamingPrefixTextBox.Text;
|
||||
var example = NamingHelper.GetPatternExample(pattern, firstVideoName, customPrefix);
|
||||
JobNamingPreviewText.Text = example;
|
||||
}
|
||||
else
|
||||
{
|
||||
JobNamingPreviewText.Text = "Video1_000001.png (default)";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ERROR] UpdateJobNamingPreview: {ex.Message}");
|
||||
if (JobNamingPreviewText != null)
|
||||
{
|
||||
JobNamingPreviewText.Text = "Video1_000001.png";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
|
||||
private void UseCustomFrameSizeCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
|
||||
private void UseCustomOverwriteCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
|
||||
private void UseCustomNamingCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { UpdateJobNamingPreview(); }
|
||||
private void CustomNamingComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { UpdateJobNamingPreview(); }
|
||||
private void CustomNamingPrefixTextBox_TextChanged(object sender, TextChangedEventArgs e) { UpdateJobNamingPreview(); }
|
||||
|
||||
private void BrowseCustomOutputButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella di output personalizzata", ShowNewFolderButton = true };
|
||||
if (!string.IsNullOrEmpty(CustomOutputTextBox.Text)) dialog.SelectedPath = CustomOutputTextBox.Text;
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) CustomOutputTextBox.Text = dialog.SelectedPath;
|
||||
}
|
||||
|
||||
private void ApplyButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selectedExtraction = GetSelectedExtractionMode();
|
||||
foreach (var job in _selectedJobs)
|
||||
{
|
||||
job.ExtractionMode = selectedExtraction;
|
||||
|
||||
if (UseCustomOutputCheckBox.IsChecked == true)
|
||||
{
|
||||
job.CustomOutputFolder = CustomOutputTextBox.Text;
|
||||
job.CustomCreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||
if (job.CustomCreateSubfolder && job.ExtractionMode != ExtractionMode.SingleFrame)
|
||||
job.OutputFolder = System.IO.Path.Combine(job.CustomOutputFolder, job.VideoName);
|
||||
else
|
||||
job.OutputFolder = job.CustomOutputFolder;
|
||||
}
|
||||
else
|
||||
{
|
||||
job.CustomOutputFolder = string.Empty;
|
||||
}
|
||||
|
||||
if (UseCustomFrameSizeCheckBox.IsChecked == true && CustomFrameSizeComboBox.SelectedItem is ComboBoxItem frameSizeItem)
|
||||
job.CustomFrameSize = frameSizeItem.Tag?.ToString() ?? string.Empty;
|
||||
else job.CustomFrameSize = string.Empty;
|
||||
|
||||
if (UseCustomOverwriteCheckBox.IsChecked == true && CustomOverwriteComboBox.SelectedItem is ComboBoxItem overwriteItem && Enum.TryParse<OverwriteMode>(overwriteItem.Tag?.ToString(), out var overwriteMode))
|
||||
job.CustomOverwriteMode = overwriteMode;
|
||||
else job.CustomOverwriteMode = null;
|
||||
|
||||
if (UseCustomNamingCheckBox.IsChecked == true && CustomNamingComboBox.SelectedItem is ComboBoxItem namingItem && Enum.TryParse<NamingPattern>(namingItem.Tag?.ToString(), out var namingPattern))
|
||||
{
|
||||
job.CustomNamingPattern = namingPattern;
|
||||
job.CustomPrefix = string.IsNullOrWhiteSpace(CustomNamingPrefixTextBox.Text) ? "custom" : CustomNamingPrefixTextBox.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
job.CustomNamingPattern = null;
|
||||
job.CustomPrefix = string.Empty;
|
||||
}
|
||||
}
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WpfMessageBox.Show($"Errore durante l'applicazione delle impostazioni: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Ganimede/Ganimede/Windows/SettingsWindow.xaml
Normal file
108
Ganimede/Ganimede/Windows/SettingsWindow.xaml
Normal file
@@ -0,0 +1,108 @@
|
||||
<Window x:Class="Ganimede.Windows.SettingsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Impostazioni" Height="550" Width="640"
|
||||
Background="#1E2228" WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="22">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Grid.Row="0" Text="Impostazioni" FontSize="26" FontWeight="Bold" Foreground="White" Margin="0,0,0,18"/>
|
||||
|
||||
<!-- Settings Content -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Output Settings -->
|
||||
<GroupBox Header="Output" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
||||
<StackPanel Margin="12">
|
||||
<CheckBox x:Name="CreateSubfolderCheckBox" Content="Crea sottocartella per ogni video"
|
||||
Foreground="White" IsChecked="True" Margin="0,0,0,10"/>
|
||||
|
||||
<TextBlock Text="Cartella Output Predefinita:" Foreground="#CCC" Margin="0,0,0,6"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="DefaultOutputTextBox" Grid.Column="0" Height="34" VerticalContentAlignment="Center"
|
||||
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
|
||||
<Button x:Name="BrowseOutputButton" Grid.Column="1" Content="Sfoglia" Width="90" Height="34"
|
||||
Click="BrowseOutputButton_Click"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Performance Settings -->
|
||||
<GroupBox Header="Impostazioni di Elaborazione" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
||||
<StackPanel Margin="12">
|
||||
<TextBlock Text="Dimensione Frame Predefinita:" Foreground="#CCC" Margin="0,0,0,6"/>
|
||||
<ComboBox x:Name="FrameSizeComboBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555">
|
||||
<ComboBoxItem Content="Dimensione Originale" Tag="original" IsSelected="True" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="320x180 (Veloce)" Tag="320,180" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="640x360 (Media)" Tag="640,360" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="1280x720 (Alta)" Tag="1280,720" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080" Foreground="Black" Background="White"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Comportamento Sovrascrittura Predefinito:" Foreground="#CCC" Margin="16,16,0,6"/>
|
||||
<ComboBox x:Name="OverwriteModeComboBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555">
|
||||
<ComboBoxItem Content="Chiedi" Tag="Ask" IsSelected="True" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="Salta" Tag="Skip" Foreground="Black" Background="White"/>
|
||||
<ComboBoxItem Content="Sovrascrivi" Tag="Overwrite" Foreground="Black" Background="White"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Modalita di Estrazione Predefinita:" Foreground="#CCC" Margin="16,16,0,6"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<RadioButton x:Name="DefaultModeFullRadio" Content="Completa" GroupName="DefExtraction" Foreground="White" Margin="0,0,18,0" IsChecked="True"/>
|
||||
<RadioButton x:Name="DefaultModeSingleRadio" Content="Singolo Frame" GroupName="DefExtraction" Foreground="White" Margin="0,0,18,0"/>
|
||||
<RadioButton x:Name="DefaultModeAutoRadio" Content="Auto" GroupName="DefExtraction" Foreground="White"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Auto: analizza il video e decide cosa estrarre." Foreground="#777" FontSize="10" TextWrapping="Wrap" Margin="0,4,0,0"/>
|
||||
|
||||
<TextBlock Text="Singolo Frame: salvataggio" Foreground="#CCC" Margin="16,16,0,6"/>
|
||||
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox" Content="Crea sottocartella anche per job a singolo frame" Foreground="White" IsChecked="False"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- File Naming Settings -->
|
||||
<GroupBox Header="Naming File" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
||||
<StackPanel Margin="12">
|
||||
<TextBlock Text="Pattern di Naming Predefinito:" Foreground="#CCC" Margin="0,0,0,6"/>
|
||||
<ComboBox x:Name="NamingPatternComboBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555"
|
||||
SelectionChanged="NamingPatternComboBox_SelectionChanged">
|
||||
<ComboBoxItem Content="NomeVideo + Progressivo" Tag="VideoNameProgressive" IsSelected="True"/>
|
||||
<ComboBoxItem Content="Frame + Progressivo" Tag="FrameProgressive"/>
|
||||
<ComboBoxItem Content="NomeVideo + Timestamp" Tag="VideoNameTimestamp"/>
|
||||
<ComboBoxItem Content="Prefisso Custom + Progressivo" Tag="CustomProgressive"/>
|
||||
<ComboBoxItem Content="Solo Timestamp" Tag="TimestampOnly"/>
|
||||
<ComboBoxItem Content="NomeVideo + Frame + Progressivo" Tag="VideoNameFrameProgressive"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Prefisso Custom Predefinito:" Foreground="#CCC" Margin="0,12,0,6"/>
|
||||
<TextBox x:Name="CustomPrefixTextBox" Height="32" Background="#333" Foreground="White" BorderBrush="#555"
|
||||
VerticalContentAlignment="Center" Text="custom" TextChanged="CustomPrefixTextBox_TextChanged"/>
|
||||
|
||||
<TextBlock Text="Anteprima:" Foreground="#CCC" Margin="0,12,0,4" FontSize="11"/>
|
||||
<TextBlock x:Name="NamingPreviewText" Text="Video1_000001.png" Foreground="#4FC3F7" FontSize="11"
|
||||
FontFamily="Consolas" Background="#1A1A1A" Padding="6" Margin="0,0,0,6"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
|
||||
<Button x:Name="SaveButton" Content="Salva" Width="95" Height="40" Margin="0,0,12,0"
|
||||
Click="SaveButton_Click" Background="#4FC3F7" Foreground="White" BorderThickness="0"/>
|
||||
<Button x:Name="CancelButton" Content="Annulla" Width="100" Height="40"
|
||||
Click="CancelButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
108
Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
Normal file
108
Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Ganimede.Properties;
|
||||
using System.Diagnostics;
|
||||
using WpfMessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace Ganimede.Windows
|
||||
{
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
public SettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private System.Windows.Controls.RadioButton? GetDefaultModeRadio(string name) => FindName(name) as System.Windows.Controls.RadioButton;
|
||||
private System.Windows.Controls.CheckBox? GetCheckBox(string name) => FindName(name) as System.Windows.Controls.CheckBox;
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
|
||||
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
|
||||
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
|
||||
if (singleFrameChk != null) singleFrameChk.IsChecked = Settings.Default.SingleFrameUseSubfolder;
|
||||
|
||||
var frameSize = Settings.Default.FrameSize;
|
||||
foreach (System.Windows.Controls.ComboBoxItem item in FrameSizeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == frameSize)
|
||||
{
|
||||
FrameSizeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var overwriteMode = Settings.Default.DefaultOverwriteMode;
|
||||
foreach (System.Windows.Controls.ComboBoxItem item in OverwriteModeComboBox.Items)
|
||||
{
|
||||
if (item.Tag?.ToString() == overwriteMode)
|
||||
{
|
||||
OverwriteModeComboBox.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (Settings.Default.DefaultExtractionMode)
|
||||
{
|
||||
case "SingleFrame":
|
||||
GetDefaultModeRadio("DefaultModeSingleRadio")!.IsChecked = true; break;
|
||||
case "Auto":
|
||||
GetDefaultModeRadio("DefaultModeAutoRadio")!.IsChecked = true; break;
|
||||
default:
|
||||
GetDefaultModeRadio("DefaultModeFullRadio")!.IsChecked = true; break;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSelectedDefaultExtractionMode()
|
||||
{
|
||||
if (GetDefaultModeRadio("DefaultModeSingleRadio")?.IsChecked == true) return "SingleFrame";
|
||||
if (GetDefaultModeRadio("DefaultModeAutoRadio")?.IsChecked == true) return "Auto";
|
||||
return "Full";
|
||||
}
|
||||
|
||||
private bool GetSingleFrameUseSubfolder() => GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
|
||||
|
||||
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella output predefinita", ShowNewFolderButton = true };
|
||||
if (!string.IsNullOrEmpty(DefaultOutputTextBox.Text)) dialog.SelectedPath = DefaultOutputTextBox.Text;
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) DefaultOutputTextBox.Text = dialog.SelectedPath;
|
||||
}
|
||||
|
||||
private void NamingPatternComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { }
|
||||
private void CustomPrefixTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { }
|
||||
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
|
||||
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
|
||||
Settings.Default.FrameSize = selectedFrameSizeItem?.Tag?.ToString() ?? "320,180";
|
||||
var selectedOverwriteItem = OverwriteModeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
|
||||
Settings.Default.DefaultOverwriteMode = selectedOverwriteItem?.Tag?.ToString() ?? "Ask";
|
||||
Settings.Default.DefaultExtractionMode = GetSelectedDefaultExtractionMode();
|
||||
Settings.Default.SingleFrameUseSubfolder = GetSingleFrameUseSubfolder();
|
||||
Settings.Default.Save();
|
||||
Debug.WriteLine("[SETTINGS] Salvate");
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WpfMessageBox.Show($"Errore durante il salvataggio: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
4
Ganimede/Models/.keep
Normal file
4
Ganimede/Models/.keep
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace Ganimede.Models
|
||||
{
|
||||
// Modelli futuri (es. VideoInfo, FrameInfo)
|
||||
}
|
||||
62
Ganimede/Properties/Settings.Designer.cs
generated
Normal file
62
Ganimede/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// Il codice è stato generato da uno strumento.
|
||||
// Versione runtime:4.0.30319.42000
|
||||
//
|
||||
// Le modifiche apportate a questo file possono provocare un comportamento non corretto e andranno perse se
|
||||
// il codice viene rigenerato.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Ganimede.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string LastOutputFolder {
|
||||
get {
|
||||
return ((string)(this["LastOutputFolder"]));
|
||||
}
|
||||
set {
|
||||
this["LastOutputFolder"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string LastVideoPath {
|
||||
get {
|
||||
return ((string)(this["LastVideoPath"]));
|
||||
}
|
||||
set {
|
||||
this["LastVideoPath"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("C:\\Users\\alber\\source\\repos\\Ganimede\\Ganimede\\FFMpeg")]
|
||||
public string FFmpegBinFolder {
|
||||
get {
|
||||
return ((string)(this["FFmpegBinFolder"]));
|
||||
}
|
||||
set {
|
||||
this["FFmpegBinFolder"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Ganimede/Properties/Settings.settings
Normal file
15
Ganimede/Properties/Settings.settings
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="Ganimede.Properties" GeneratedClassName="Settings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="LastOutputFolder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="LastVideoPath" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="FFmpegBinFolder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">C:\Users\alber\source\repos\Ganimede\Ganimede\FFMpeg</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
4
Ganimede/ViewModels/.keep
Normal file
4
Ganimede/ViewModels/.keep
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace Ganimede.ViewModels
|
||||
{
|
||||
// ViewModel principale e futuri ViewModel
|
||||
}
|
||||
4
Ganimede/Views/.keep
Normal file
4
Ganimede/Views/.keep
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace Ganimede.Views
|
||||
{
|
||||
// Views aggiuntive se necessario
|
||||
}
|
||||
564
README.md
Normal file
564
README.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# Ganimede - Estrattore Frame Video
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
**Ganimede** è un'applicazione desktop Windows moderna e facile da usare per estrarre automaticamente frame (immagini) da file video. Non richiede installazioni complicate o configurazioni tecniche - basta scaricare ed eseguire!
|
||||
|
||||
---
|
||||
|
||||
## 📋 Cosa Puoi Fare con Ganimede
|
||||
|
||||
- ✅ **Estrarre immagini da video** in modo automatico
|
||||
- 🎯 **Tre modalità intelligenti**: estrazione completa, singola immagine o automatica
|
||||
- 📁 **Elaborare più video insieme** con una semplice coda
|
||||
- 🖼️ **Vedere l'anteprima** delle immagini estratte
|
||||
- ⚙️ **Personalizzare tutto**: nomi file, dimensioni, cartelle
|
||||
- 🔄 **Zero configurazione** - tutto funziona subito!
|
||||
|
||||
---
|
||||
|
||||
## 💻 Cosa Ti Serve
|
||||
|
||||
### Requisiti Minimi
|
||||
- **Computer**: Windows 10 o Windows 11 (64-bit)
|
||||
- **Software**: .NET 8 (gratuito, si installa in 2 minuti)
|
||||
- **Spazio disco**: 100 MB per l'applicazione + spazio per le tue immagini
|
||||
|
||||
**Nessun altro software richiesto!** Ganimede funziona usando le capacità video già integrate in Windows.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installazione (5 Minuti)
|
||||
|
||||
### Passo 1: Scarica .NET 8
|
||||
|
||||
1. Vai su [Download .NET 8](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
2. Clicca su **"Download .NET Desktop Runtime 8.0"** (Windows, x64)
|
||||
3. Esegui il file scaricato e segui l'installazione guidata
|
||||
4. Riavvia il computer (opzionale ma consigliato)
|
||||
|
||||
**Come verificare l'installazione:**
|
||||
- Apri il Prompt dei comandi (cerca "cmd" nel menu Start)
|
||||
- Scrivi `dotnet --version` e premi Invio
|
||||
- Dovresti vedere un numero di versione (es. `8.0.0`)
|
||||
|
||||
### Passo 2: Scarica Ganimede
|
||||
|
||||
1. Vai alla [pagina Releases](https://192.168.30.23/Alby96/Ganimede/-/releases) del progetto
|
||||
2. Scarica l'ultima versione (file `Ganimede-v1.0.zip`)
|
||||
3. Estrai il file ZIP in una cartella (es. `C:\Programmi\Ganimede`)
|
||||
|
||||
### Passo 3: Avvia Ganimede
|
||||
|
||||
1. Apri la cartella dove hai estratto Ganimede
|
||||
2. Fai doppio clic su `Ganimede.exe`
|
||||
3. Se Windows chiede conferma, clicca **"Esegui comunque"**
|
||||
|
||||
🎉 **Fatto!** L'applicazione è pronta all'uso.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Guida Rapida (Primi 5 Minuti)
|
||||
|
||||
### Come Estrarre Immagini da un Video
|
||||
|
||||
#### **Metodo Semplice (Per Iniziare)**
|
||||
|
||||
1. **Avvia Ganimede** (doppio clic su `Ganimede.exe`)
|
||||
|
||||
2. **Scegli dove salvare le immagini**
|
||||
- Clicca il pulsante 🗂️ **"Seleziona Cartella Output"** in alto
|
||||
- Scegli una cartella (es. `Documenti\FramiVideo`)
|
||||
- Clicca **"Seleziona cartella"**
|
||||
|
||||
3. **Aggiungi il tuo video**
|
||||
- Clicca il pulsante ➕ **"Aggiungi Video"**
|
||||
- Scegli un video dal tuo computer (es. `vacanze.mp4`)
|
||||
- Clicca **"Apri"**
|
||||
|
||||
4. **Avvia l'estrazione**
|
||||
- Clicca il pulsante ▶️ **"Avvia Coda"** verde in alto
|
||||
- Aspetta che la barra di avanzamento arrivi al 100%
|
||||
- Vedrai il messaggio "Completato ✅"
|
||||
|
||||
5. **Guarda le tue immagini**
|
||||
- Apri la cartella che hai scelto al passo 2
|
||||
- Troverai una sottocartella con il nome del video
|
||||
- Dentro ci sono tutte le immagini estratte!
|
||||
|
||||
**Esempio di risultato:**
|
||||
```
|
||||
📁 Documenti\FramiVideo\
|
||||
└─ 📁 vacanze\
|
||||
├─ 🖼️ vacanze_000001.png
|
||||
├─ 🖼️ vacanze_000002.png
|
||||
├─ 🖼️ vacanze_000003.png
|
||||
└─ ... (tante altre!)
|
||||
```
|
||||
|
||||
#### **Metodo Veloce (Più Video Insieme)**
|
||||
|
||||
1. **Scegli la cartella output** (come sopra)
|
||||
|
||||
2. **Aggiungi tutti i video**
|
||||
- Clicca 📁 **"Importa Cartella"**
|
||||
- Scegli una cartella con tanti video dentro
|
||||
- Ganimede li aggiunge tutti alla coda automaticamente
|
||||
|
||||
3. **Avvia e aspetta**
|
||||
- Clicca ▶️ **"Avvia Coda"**
|
||||
- Ganimede elabora tutti i video uno dopo l'altro
|
||||
- Puoi vedere il progresso di ciascuno nella lista
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Le Tre Modalità di Estrazione
|
||||
|
||||
Ganimede può estrarre immagini in tre modi diversi. Puoi scegliere quale usare nelle **Impostazioni**.
|
||||
|
||||
### 1️⃣ **Modalità Completa** (Predefinita)
|
||||
|
||||
**Cosa fa:** Estrae **tutte** le immagini dal video, una per ogni fotogramma.
|
||||
|
||||
**Quando usarla:**
|
||||
- Vuoi analizzare il video fotogramma per fotogramma
|
||||
- Stai creando un'animazione
|
||||
- Hai bisogno di tante immagini per un progetto
|
||||
|
||||
**Esempio:** Un video di 10 secondi a 30 fps = 300 immagini
|
||||
|
||||
**Pro:** Ottieni ogni singolo fotogramma
|
||||
**Contro:** Crea tanti file (può richiedere molto spazio disco)
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ **Modalità Singola Immagine**
|
||||
|
||||
**Cosa fa:** Estrae **1 sola** immagine dal centro del video.
|
||||
|
||||
**Quando usarla:**
|
||||
- Vuoi solo un'anteprima o una copertina del video
|
||||
- Hai bisogno di velocità (1 secondo per video)
|
||||
- Vuoi risparmiare spazio su disco
|
||||
|
||||
**Esempio:** Un video di 10 secondi → 1 immagine dall'istante 5 secondi
|
||||
|
||||
**Pro:** Velocissimo, occupa poco spazio
|
||||
**Contro:** Solo un'immagine per video
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ **Modalità Automatica** (Intelligente)
|
||||
|
||||
**Cosa fa:** Ganimede **analizza** il video e **decide** automaticamente:
|
||||
- Video molto breve (≤ 2 secondi) → Estrae 1 sola immagine
|
||||
- Video breve ma statico (3-45 sec, poco movimento) → Estrae 1 sola immagine
|
||||
- Altri video → Estrazione completa
|
||||
|
||||
**Quando usarla:**
|
||||
- Hai una cartella con video di tipi diversi (corti e lunghi)
|
||||
- Vuoi che Ganimede scelga il metodo migliore automaticamente
|
||||
- Non sei sicuro quale modalità usare
|
||||
|
||||
**Esempio:**
|
||||
- Video corto di 1 secondo → 1 immagine
|
||||
- Video lungo di 5 minuti → tutte le immagini
|
||||
|
||||
**Pro:** Intelligente, ottimizza spazio e tempo automaticamente
|
||||
**Contro:** Non hai controllo totale sulla scelta
|
||||
|
||||
---
|
||||
|
||||
**Come Cambiare Modalità:**
|
||||
1. Clicca ⚙️ **"Impostazioni"** in alto
|
||||
2. Vai alla sezione **"Impostazioni di Elaborazione"**
|
||||
3. Cambia **"Modalità di Estrazione Predefinita"**
|
||||
4. Clicca **"Salva"**
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Impostazioni (Personalizza Come Vuoi)
|
||||
|
||||
### Aprire le Impostazioni
|
||||
Clicca il pulsante ⚙️ **"Impostazioni"** nella barra in alto.
|
||||
|
||||
---
|
||||
|
||||
### **Sezione: Output**
|
||||
|
||||
#### 📁 Crea sottocartella per ogni video
|
||||
- **Cosa fa:** Se attivo, crea una cartella separata per ogni video
|
||||
- **Esempio:**
|
||||
- ✅ **Attivo:** `Output\vacanze\immagine_001.png`
|
||||
- ❌ **Disattivo:** `Output\immagine_001.png` (tutti i video nella stessa cartella)
|
||||
- **Consiglio:** Lascia attivo per evitare confusione
|
||||
|
||||
#### 📂 Cartella Output Predefinita
|
||||
- **Cosa fa:** La cartella dove salvare tutte le immagini estratte
|
||||
- **Come cambiarla:** Clicca **"Sfoglia"** e scegli una nuova cartella
|
||||
- **Consiglio:** Usa una cartella facile da ricordare, come `Documenti\ImmaginiVideo`
|
||||
|
||||
---
|
||||
|
||||
### **Sezione: Impostazioni di Elaborazione**
|
||||
|
||||
#### 📐 Dimensione Immagini
|
||||
Scegli quanto grandi devono essere le immagini estratte:
|
||||
|
||||
| Opzione | Risoluzione | Quando Usarla | Dimensione File |
|
||||
|---------|-------------|---------------|-----------------|
|
||||
| **Originale** | Come il video | Massima qualità | Grande (~1 MB) |
|
||||
| **1920x1080** | Full HD | Video alta qualità | Media (~800 KB) |
|
||||
| **1280x720** | HD | Uso normale | Piccola (~400 KB) |
|
||||
| **640x360** | SD | Anteprime web | Molto piccola (~150 KB) |
|
||||
| **320x180** | Miniatura | Icone/thumbnail | Mini (~56 KB) |
|
||||
|
||||
**Consiglio:** Per la maggior parte degli usi, **1280x720** è perfetto (qualità buona, file non troppo grandi).
|
||||
|
||||
---
|
||||
|
||||
#### 🔄 Comportamento Sovrascrittura
|
||||
Cosa succede se le immagini esistono già nella cartella:
|
||||
|
||||
- **Chiedi** (predefinito): Ti chiede ogni volta cosa fare
|
||||
- **Salta**: Non rielabora, salta le immagini già esistenti (utile per riprendere lavori interrotti)
|
||||
- **Sovrascrivi**: Cancella e ricrea tutto (usa se vuoi rigenerare tutto)
|
||||
|
||||
**Consiglio:** Usa **"Chiedi"** la prima volta, poi **"Salta"** per risparmiare tempo.
|
||||
|
||||
---
|
||||
|
||||
#### 🎯 Modalità di Estrazione
|
||||
Scegli la modalità predefinita (vedi sezione [Le Tre Modalità](#-le-tre-modalità-di-estrazione)):
|
||||
- **Completa**: Tutte le immagini
|
||||
- **Singola Immagine**: Solo 1 immagine
|
||||
- **Automatica**: Ganimede decide (raccomandato se non sei sicuro)
|
||||
|
||||
---
|
||||
|
||||
#### 📂 Singolo Frame: Salvataggio
|
||||
- **Cosa fa:** Quando usi "Singola Immagine", puoi decidere se creare comunque una sottocartella
|
||||
- ✅ **Attivo:** Crea `Output\video\video_000001.png`
|
||||
- ❌ **Disattivo:** Crea `Output\video_000001.png` (direttamente nella cartella principale)
|
||||
|
||||
**Consiglio:** Lascia disattivo per avere tutte le immagini singole in un unico posto.
|
||||
|
||||
---
|
||||
|
||||
### **Sezione: Naming File**
|
||||
|
||||
#### 🏷️ Come Chiamare i File
|
||||
Ganimede può chiamare le immagini in modi diversi. Scegli quello che preferisci:
|
||||
|
||||
**1. VideoNameProgressive** (Predefinito)
|
||||
```
|
||||
vacanze_000001.png
|
||||
vacanze_000002.png
|
||||
vacanze_000003.png
|
||||
```
|
||||
✅ **Usa questo se:** Vuoi file ordinati numericamente con il nome del video
|
||||
|
||||
**2. FrameProgressive**
|
||||
```
|
||||
frame_000001.png
|
||||
frame_000002.png
|
||||
frame_000003.png
|
||||
```
|
||||
✅ **Usa questo se:** Non ti interessa il nome del video, vuoi solo numerazione
|
||||
|
||||
**3. VideoNameTimestamp**
|
||||
```
|
||||
vacanze_000000ms.png
|
||||
vacanze_033ms.png
|
||||
vacanze_067ms.png
|
||||
```
|
||||
✅ **Usa questo se:** Vuoi sapere a che secondo del video corrisponde l'immagine
|
||||
|
||||
**4. CustomProgressive**
|
||||
```
|
||||
mio_prefisso_000001.png
|
||||
mio_prefisso_000002.png
|
||||
mio_prefisso_000003.png
|
||||
```
|
||||
✅ **Usa questo se:** Vuoi dare un nome personalizzato ai file
|
||||
|
||||
**Come personalizzare:** Scrivi il tuo prefisso nel campo **"Prefisso Custom"** (es. `foto_vacanze`)
|
||||
|
||||
**5. TimestampOnly**
|
||||
```
|
||||
00h00m00s000ms.png
|
||||
00h00m00s033ms.png
|
||||
00h00m01s500ms.png
|
||||
```
|
||||
✅ **Usa questo se:** Vuoi i nomi basati solo sul tempo
|
||||
|
||||
**6. VideoNameFrameProgressive**
|
||||
```
|
||||
vacanze_frame_000001.png
|
||||
vacanze_frame_000002.png
|
||||
vacanze_frame_000003.png
|
||||
```
|
||||
✅ **Usa questo se:** Vuoi il massimo della chiarezza nei nomi
|
||||
|
||||
---
|
||||
|
||||
### **Salvare le Impostazioni**
|
||||
|
||||
Dopo aver fatto le modifiche:
|
||||
1. Clicca **"Salva"** in basso
|
||||
2. La finestra si chiude
|
||||
3. Le tue impostazioni sono ora attive per tutti i nuovi video!
|
||||
|
||||
**Nota:** I video già in coda mantengono le vecchie impostazioni. Per cambiarle:
|
||||
- Seleziona i video (checkbox a sinistra)
|
||||
- Clicca ⚙️ **"Configura Selezionati"**
|
||||
- Cambia le impostazioni solo per quei video
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Funzioni Avanzate (Per Utenti Esperti)
|
||||
|
||||
### Configurare Video Singoli
|
||||
|
||||
Se vuoi impostazioni diverse per video specifici:
|
||||
|
||||
1. **Aggiungi i video** alla coda
|
||||
2. **Seleziona** i video che vuoi configurare (checkbox a sinistra)
|
||||
3. Clicca ⚙️ **"Configura Selezionati"** in alto
|
||||
4. Nella finestra che appare, puoi cambiare:
|
||||
- Cartella di output specifica
|
||||
- Dimensione immagini
|
||||
- Modalità estrazione
|
||||
- Nome file
|
||||
- Comportamento sovrascrittura
|
||||
5. Clicca **"Salva"**
|
||||
|
||||
**Esempio pratico:**
|
||||
- Video 1 → Estrazione completa in HD
|
||||
- Video 2 → Solo 1 immagine piccola
|
||||
- Video 3 → Estrazione automatica in Full HD
|
||||
|
||||
### Importare da Cartella
|
||||
|
||||
Se hai tanti video in una cartella:
|
||||
|
||||
1. Clicca 📁 **"Importa Cartella"**
|
||||
2. Scegli la cartella con i video
|
||||
3. Ganimede aggiunge automaticamente tutti i video supportati
|
||||
4. Clicca ▶️ **"Avvia Coda"**
|
||||
|
||||
### Fermare l'Elaborazione
|
||||
|
||||
Se vuoi fermare:
|
||||
1. Clicca ⏹️ **"Ferma"** in alto
|
||||
2. Ganimede finisce l'immagine corrente e poi si ferma
|
||||
3. Il video in elaborazione viene marcato come "Cancellato"
|
||||
|
||||
### Pulire la Coda
|
||||
|
||||
Dopo aver finito:
|
||||
- 🧹 **"Pulisci Completati"**: Rimuove solo i video completati/falliti
|
||||
- 🗑️ **"Pulisci Tutto"**: Svuota completamente la coda
|
||||
|
||||
---
|
||||
|
||||
## 💡 Esempi Pratici
|
||||
|
||||
### Caso 1: Creare una Copertina per Video
|
||||
|
||||
**Obiettivo:** Estrarre 1 immagine da 50 video per creare miniature.
|
||||
|
||||
**Passi:**
|
||||
1. Impostazioni → Modalità: **Singola Immagine**
|
||||
2. Impostazioni → Dimensione: **640x360**
|
||||
3. Importa Cartella → Seleziona cartella con 50 video
|
||||
4. Avvia Coda
|
||||
5. **Risultato:** 50 immagini di copertina in pochi minuti!
|
||||
|
||||
---
|
||||
|
||||
### Caso 2: Estrarre Tutti i Frame da un Video
|
||||
|
||||
**Obiettivo:** Analizzare un video fotogramma per fotogramma.
|
||||
|
||||
**Passi:**
|
||||
1. Impostazioni → Modalità: **Completa**
|
||||
2. Impostazioni → Dimensione: **Originale**
|
||||
3. Aggiungi Video → Scegli il video
|
||||
4. Avvia Coda
|
||||
5. **Risultato:** Tutte le immagini in `Output\NomeVideo\`
|
||||
|
||||
---
|
||||
|
||||
### Caso 3: Batch Automatico (Video Misti)
|
||||
|
||||
**Obiettivo:** Estrarre immagini da video di durata diversa in modo intelligente.
|
||||
|
||||
**Passi:**
|
||||
1. Impostazioni → Modalità: **Automatica**
|
||||
2. Importa Cartella → Seleziona cartella con video misti
|
||||
3. Avvia Coda
|
||||
4. **Risultato:**
|
||||
- Video corti → 1 immagine
|
||||
- Video lunghi → tutte le immagini
|
||||
- Tutto automatico!
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Risoluzione Problemi
|
||||
|
||||
### ❌ "L'applicazione non si avvia"
|
||||
|
||||
**Possibili cause:**
|
||||
- .NET 8 non installato
|
||||
- File corrotti
|
||||
|
||||
**Soluzione:**
|
||||
1. Verifica .NET 8:
|
||||
- Apri Prompt dei comandi
|
||||
- Scrivi `dotnet --version`
|
||||
- Se non vedi un numero, installa .NET 8 (vedi [Installazione](#-installazione-5-minuti))
|
||||
2. Scarica di nuovo Ganimede ed estrai in una nuova cartella
|
||||
3. Riavvia il computer
|
||||
|
||||
---
|
||||
|
||||
### ⏱️ "L'estrazione è molto lenta"
|
||||
|
||||
**Possibili cause:**
|
||||
- Video in altissima risoluzione (4K)
|
||||
- Dimensione immagini troppo grande
|
||||
- Computer lento
|
||||
|
||||
**Soluzione:**
|
||||
1. Usa dimensione **640x360** o **1280x720** invece di "Originale"
|
||||
2. Salva le immagini su un disco veloce (SSD) se possibile
|
||||
3. Elabora pochi video alla volta invece di 50 insieme
|
||||
|
||||
---
|
||||
|
||||
### 🖼️ "Le immagini sono nere o rovinate"
|
||||
|
||||
**Possibili cause:**
|
||||
- Video corrotto
|
||||
- Codec video non supportato
|
||||
|
||||
**Soluzione:**
|
||||
1. Prova ad aprire il video con VLC o Windows Media Player
|
||||
2. Se il video non si apre correttamente, è probabilmente corrotto
|
||||
3. Se il video funziona ma Ganimede no, prova a convertirlo in MP4 con [HandBrake](https://handbrake.fr/) (gratuito)
|
||||
|
||||
---
|
||||
|
||||
### 📁 "Errore: Accesso negato"
|
||||
|
||||
**Possibili cause:**
|
||||
- La cartella output è protetta (es. `C:\Programmi`)
|
||||
- Un altro programma ha aperto i file
|
||||
|
||||
**Soluzione:**
|
||||
1. Cambia cartella output in `Documenti` o `Desktop`
|
||||
2. Chiudi programmi di visualizzazione immagini
|
||||
3. Se necessario, esegui Ganimede come amministratore:
|
||||
- Click destro su `Ganimede.exe`
|
||||
- **"Esegui come amministratore"**
|
||||
|
||||
---
|
||||
|
||||
### 🖼️ "Non vedo l'anteprima delle immagini"
|
||||
|
||||
**Causa:** Pannello anteprima mostra solo le ultime 60 immagini.
|
||||
|
||||
**Soluzione:**
|
||||
- Aspetta che un video finisca di elaborare
|
||||
- Le anteprime appariranno automaticamente nella colonna destra
|
||||
- Se la colonna è vuota, verifica che ci siano effettivamente file PNG nella cartella output
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Suggerimenti per Principianti
|
||||
|
||||
### ✅ Cosa Fare Prima di Iniziare
|
||||
|
||||
1. **Crea una cartella dedicata** per le immagini estratte (es. `Documenti\ImmaginiDaVideo`)
|
||||
2. **Prova con 1 video corto** prima di elaborare 100 video
|
||||
3. **Usa la modalità Automatica** se non sei sicuro
|
||||
4. **Controlla lo spazio disco** disponibile (1 minuto di video Full HD → ~1000 immagini → ~1 GB)
|
||||
|
||||
### ❌ Cosa Evitare
|
||||
|
||||
- ❌ Non usare cartelle di sistema come `C:\Windows` o `C:\Programmi`
|
||||
- ❌ Non elaborare 100 video Full HD se hai poco spazio disco (controlla prima!)
|
||||
- ❌ Non chiudere Ganimede durante l'elaborazione (usa "Ferma" se vuoi interrompere)
|
||||
|
||||
### 💡 Trucchi Utili
|
||||
|
||||
- 💡 **Seleziona più video** (Ctrl+Click o Shift+Click) per configurarli insieme
|
||||
- 💡 **Riprendi lavori interrotti** impostando Sovrascrittura su "Salta"
|
||||
- 💡 **Organizza per progetto** usando sottocartelle diverse (es. `Output\Vacanze`, `Output\Lavoro`)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Informazioni Tecniche (Per Curiosi)
|
||||
|
||||
### Come Funziona Ganimede
|
||||
|
||||
Ganimede usa **Windows Media Foundation** (WMF), una tecnologia già presente in Windows 10/11. Per questo non servono installazioni aggiuntive o configurazioni complicate.
|
||||
|
||||
**Tecnologie usate:**
|
||||
- **.NET 8**: Framework moderno Microsoft
|
||||
- **WPF**: Interfaccia grafica moderna
|
||||
- **Windows Media Foundation**: Decodifica video nativa Windows
|
||||
- **C# 12**: Linguaggio di programmazione
|
||||
|
||||
**Caratteristiche tecniche:**
|
||||
- 🚀 Elaborazione video completamente asincrona (non blocca l'interfaccia)
|
||||
- 🎯 Supporto codec: H.264, H.265/HEVC, VP8, VP9, AV1, MPEG-4, WMV, Motion JPEG
|
||||
- 📦 Nessuna dipendenza esterna da installare
|
||||
- 💾 Immagini salvate in formato PNG (qualità lossless)
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licenza
|
||||
|
||||
Ganimede è distribuito con licenza **MIT** - puoi usarlo liberamente per scopi personali e commerciali.
|
||||
|
||||
---
|
||||
|
||||
## 👤 Supporto e Contatti
|
||||
|
||||
**Hai bisogno di aiuto?**
|
||||
- 📧 Email: [Inserire email supporto]
|
||||
- 🐛 Segnala problemi: [Issues GitHub](https://192.168.30.23/Alby96/Ganimede/issues)
|
||||
- 💬 Domande: [Forum/Discord]
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Ringraziamenti
|
||||
|
||||
- **Microsoft** per .NET e Windows Media Foundation
|
||||
- **Community .NET** per supporto e feedback
|
||||
- **Tu** per usare Ganimede! 😊
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Versioni
|
||||
|
||||
### v5.0 (Dicembre 2024) - Corrente
|
||||
- ✅ Implementazione nativa Windows Media Foundation
|
||||
- ✅ Zero dipendenze esterne
|
||||
- ✅ Installazione semplificata
|
||||
- ✅ Performance migliorate con accelerazione hardware
|
||||
|
||||
### v3.0 (Precedente)
|
||||
- ✅ Interfaccia dark moderna
|
||||
- ✅ Sistema coda avanzato
|
||||
- ✅ Tre modalità di estrazione
|
||||
|
||||
---
|
||||
|
||||
**🎬 Buon Divertimento con Ganimede!**
|
||||
|
||||
Trasforma i tuoi video in immagini in pochi click! 🚀
|
||||
Reference in New Issue
Block a user