Compare commits

...

3 Commits

Author SHA1 Message Date
fdf540a69b Transizione a Windows Media Foundation (WMF)
Rimosso il supporto a `System.Drawing` e FFmpeg, sostituendoli con Windows Media Foundation per la gestione dei video.

- Aggiornato `FrameExtractor.cs` e `VideoAnalyzer.cs` per utilizzare il `Source Reader` di WMF.
- Aggiunta la classe `MFVideoReader` per l'estrazione dei frame.
- Introdotto il file `MFInterfaces.cs` con nuove interfacce COM per WMF.
- Rimosse interfacce COM obsolete e metodi non più utilizzati.
- Migliorata la gestione delle eccezioni e ottimizzate le dipendenze.
- Aggiunto supporto per il formato video RGB32.
- Pulizia del codice e rimozione di commenti obsoleti.

Questa modifica riduce le dipendenze esterne e migliora l'integrazione con le API native di Windows.
2025-12-08 17:51:14 +01:00
627a157762 Migrazione a Windows Media Foundation
- Rimosse le dipendenze da FFmpeg e FFMediaToolkit.
- Implementata Windows Media Foundation per analisi video.
- Aggiunto tema scuro e navigazione laterale nell'interfaccia.
- Tradotti testi e notifiche dall'inglese all'italiano.
- Migliorata la gestione degli errori in JobConfigWindow.
- Aggiornato README per riflettere i cambiamenti.
- Eliminato lo script di download di FFmpeg.
2025-12-08 01:09:57 +01:00
11931854c7 Sostituzione FFMpegCore con FFMediaToolkit
- Rimosso FFMpegCore e introdotto FFMediaToolkit per semplificare
  la gestione dei video e migliorare le prestazioni.
- Aggiunti `FrameExtractor` e `VideoAnalyzer` per l'estrazione
  dei frame e l'analisi dei metadati video.
- Riprogettata l'interfaccia utente con tema moderno e navigazione
  a schede (Processing, Library, Settings).
- Integrate le impostazioni nella scheda "Settings", eliminando
  la finestra legacy.
- Aggiornato `VideoProcessingService` per utilizzare i nuovi
  wrapper e migliorata la gestione della coda.
- Tradotti i testi dell'interfaccia dall'italiano all'inglese.
- Aggiornata la documentazione (`README.md`) con dettagli sulle
  funzionalità, lo stack tecnologico e la struttura del progetto.
- Ottimizzate le prestazioni e migliorata la gestione degli errori.
- Aggiunto supporto per nuovi formati video (FLV, WebM).
- Rimossi codice e risorse obsolete, migliorando la manutenibilità.
2025-12-07 23:34:48 +01:00
20 changed files with 1923 additions and 460 deletions

View File

@@ -13,9 +13,6 @@
<setting name="LastVideoPath" serializeAs="String">
<value />
</setting>
<setting name="FFmpegBinFolder" serializeAs="String">
<value>C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg</value>
</setting>
</Ganimede.Properties.Settings>
</userSettings>
</configuration>

View File

@@ -7,5 +7,13 @@ namespace Ganimede
/// </summary>
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");
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,10 +7,12 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.2.0" />
<!-- Only System.Drawing.Common for image manipulation -->
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
@@ -28,4 +30,6 @@
</None>
</ItemGroup>
<!-- Native Windows libraries (included in Windows) -->
</Project>

View File

@@ -5,48 +5,50 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ganimede"
mc:Ignorable="d"
Title="Estrattore Frame Video" Height="800" Width="1250"
Background="#1E2228" WindowStartupLocation="CenterScreen">
Title="Ganimede - Estrattore Frame Video" Height="750" Width="1200"
Background="#0F1419" WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:StatusColorConverter x:Key="StatusColorConverter"/>
<!-- Color resources -->
<Color x:Key="AccentColor">#268BFF</Color>
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
<SolidColorBrush x:Key="AccentBrushLight" Color="#39A3FF"/>
<SolidColorBrush x:Key="BaseBrush" Color="#1E2228"/>
<SolidColorBrush x:Key="PanelBrush" Color="#242A31"/>
<SolidColorBrush x:Key="PanelSubBrush" Color="#2C333B"/>
<SolidColorBrush x:Key="BorderBrushColor" Color="#38424D"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#B5BDC7"/>
<!-- 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"/>
<!-- Button Style -->
<Style TargetType="Button" x:Key="ToolbarButton">
<!-- Modern Button Style (Dark) -->
<Style TargetType="Button" x:Key="ModernButton">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#2F3740"/>
<Setter Property="BorderBrush" Value="#3F4A55"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="14 8"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="16,10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="8" SnapsToDevicePixels="True">
<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="#39444F"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#46525E"/>
<Setter Property="Background" Value="{StaticResource PrimaryLightBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
<Setter Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -54,59 +56,143 @@
</Setter>
</Style>
<!-- Accent Button -->
<Style TargetType="Button" x:Key="AccentButton" BasedOn="{StaticResource ToolbarButton}">
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
<Setter Property="BorderBrush" Value="#1673D5"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource AccentBrushLight}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Danger Button -->
<Style TargetType="Button" x:Key="DangerButton" BasedOn="{StaticResource ToolbarButton}">
<Setter Property="Background" Value="#D9534F"/>
<Setter Property="BorderBrush" Value="#B33E3B"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E36460"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Flat small badge button -->
<Style TargetType="Button" x:Key="SmallGhostButton" BasedOn="{StaticResource ToolbarButton}">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="12 6"/>
<Setter Property="MinHeight" Value="36"/>
<Setter Property="Background" Value="#2F3740"/>
</Style>
<!-- ProgressBar -->
<Style TargetType="ProgressBar">
<Setter Property="Height" Value="6"/>
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
<Setter Property="Background" Value="#313941"/>
<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="ProgressBar">
<Border Background="{TemplateBinding Background}" CornerRadius="3">
<Grid x:Name="PART_Track">
<Rectangle x:Name="PART_Indicator" Fill="{TemplateBinding Foreground}" RadiusX="3" RadiusY="3"/>
</Grid>
<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>
<!-- ScrollViewer styling for thin scrollbar -->
<Style TargetType="ScrollBar">
<Setter Property="Width" Value="10"/>
<Setter Property="Background" Value="#20252B"/>
<!-- 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>
@@ -117,132 +203,406 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- TOOLBAR -->
<Border Background="#242A31" Padding="16 12" BorderBrush="#303840" BorderThickness="0,0,0,1">
<DockPanel LastChildFill="False">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<Button x:Name="BrowseVideoButton" Style="{StaticResource AccentButton}" Content=" Aggiungi Video" Click="BrowseVideoButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ImportFolderButton" Style="{StaticResource ToolbarButton}" Content="📁 Importa Cartella" Click="ImportFolderButton_Click" Margin="0,0,10,0"/>
<Button x:Name="SelectOutputFolderButton" Style="{StaticResource ToolbarButton}" Content="🗂 Seleziona Cartella Output" Click="SelectOutputFolderButton_Click" Margin="0,0,10,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Button x:Name="ConfigureSelectedButton" Style="{StaticResource ToolbarButton}" Content="⚙ Configura Selezionati" Width="195" IsEnabled="False" Click="ConfigureSelectedButton_Click" Margin="0,0,10,0"/>
<Button x:Name="StartQueueButton" Style="{StaticResource AccentButton}" Content="▶ Avvia Coda" Width="150" Click="StartQueueButton_Click" Margin="0,0,10,0"/>
<Button x:Name="StopQueueButton" Style="{StaticResource DangerButton}" Content="⏹ Ferma" Width="110" IsEnabled="False" Click="StopQueueButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ClearCompletedButton" Style="{StaticResource SmallGhostButton}" Content="🧹 Pulisci Completati" Click="ClearCompletedButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ClearAllButton" Style="{StaticResource SmallGhostButton}" Content="🗑 Pulisci Tutto" Click="ClearAllButton_Click" Margin="0,0,10,0"/>
<Button x:Name="SettingsButton" Style="{StaticResource SmallGhostButton}" Content="⚙ Impostazioni" Click="SettingsButton_Click"/>
<!-- 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>
<!-- CONTENUTO PRINCIPALE -->
<!-- Main Content with Sidebar Navigation -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="370"/>
</Grid.ColumnDefinitions>
<!-- Coda -->
<Grid Margin="18 12 8 12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0 0 0 10" VerticalAlignment="Center">
<TextBlock Text="Coda Job" FontSize="18" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock x:Name="QueueCountText" Text="(0)" Foreground="{StaticResource TextSecondaryBrush}" Margin="8,4,0,0"/>
<!-- 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>
<Border Grid.Row="1" Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="4">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="QueueItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource PanelSubBrush}" Margin="6" Padding="10" CornerRadius="6" BorderBrush="#3A454F" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 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>
<CheckBox x:Name="JobCheckBox" Grid.Row="0" Grid.Column="0" Margin="0,0,10,0" VerticalAlignment="Center" Tag="{Binding}" Checked="JobCheckBox_CheckedChanged" Unchecked="JobCheckBox_CheckedChanged"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding VideoName}" Foreground="White" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" Margin="0,0,8,0"/>
<TextBlock Text="{Binding Progress, StringFormat={}{0:0}%}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11"/>
</StackPanel>
<Button Grid.Row="0" Grid.Column="2" Content="✕" Width="30" Height="26" Style="{StaticResource SmallGhostButton}" Tag="{Binding}" Click="RemoveQueueItem_Click" ToolTip="Rimuovi"/>
<!-- Actions Bar -->
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ProgressBar Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,0" Value="{Binding Progress}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding StatusMessage}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11" Margin="0,6,0,0" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" FontSize="10" Foreground="#87939F" Margin="0,8,0,0" TextWrapping="Wrap">
<TextBlock.Text>
<MultiBinding StringFormat="{}📁 {0} 📐 {1} 🔄 {2} 🏷 {3} 🎯 {4}">
<Binding Path="OutputFolderDisplay"/>
<Binding Path="FrameSizeDisplay"/>
<Binding Path="OverwriteModeDisplay"/>
<Binding Path="NamingPatternDisplay"/>
<Binding Path="ExtractionModeDisplay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Grid>
<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>
<!-- Pannello destro -->
<StackPanel Grid.Column="1" Margin="8 12 18 12">
<Border Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="14">
<StackPanel>
<TextBlock Text="Impostazioni Globali" FontSize="16" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="Cartella Output" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" Margin="0,10,0,2"/>
<DockPanel LastChildFill="True">
<TextBox x:Name="GlobalOutputFolderTextBox" Height="34" Margin="0,0,10,0" IsReadOnly="True" Background="#2C333B" BorderBrush="#3A434C" Foreground="White" BorderThickness="1"/>
<Button Content="Sfoglia" Width="80" Style="{StaticResource SmallGhostButton}" Click="SelectOutputFolderButton_Click"/>
</DockPanel>
<TextBlock Text="Anteprime (Thumbnails)" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" Margin="0,14,0,4"/>
<Border Background="{StaticResource PanelSubBrush}" BorderBrush="#3A454F" BorderThickness="1" CornerRadius="6" Padding="6" Height="260">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ThumbnailsPanel">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<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 Margin="4" BorderBrush="#3D4853" BorderThickness="1" CornerRadius="4">
<Image Source="{Binding}" Width="90" Height="52" Stretch="UniformToFill"/>
<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>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
<!-- BARRA STATO -->
<Border Grid.Row="2" Background="#242A31" BorderBrush="#303840" BorderThickness="1,1,1,0" Padding="16 6" CornerRadius="6 6 0 0" Margin="14 0 14 14">
<!-- Footer Status Bar -->
<Border Grid.Row="2"
Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="0,1,0,0"
Padding="24,12">
<DockPanel>
<TextBlock x:Name="StatusText" Foreground="{StaticResource TextSecondaryBrush}" FontSize="13" VerticalAlignment="Center" Text="Pronto"/>
<TextBlock Text=" | " Foreground="#55606B" Margin="6,0"/>
<TextBlock Text="Job:" Foreground="#77818B" Margin="0,0,4,0"/>
<TextBlock x:Name="JobsSummaryText" Foreground="#4F5962" FontSize="11" VerticalAlignment="Center"/>
<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>

View File

@@ -8,7 +8,6 @@ using System.Windows.Media.Imaging;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using FFMpegCore;
using Ganimede.Properties;
using Ganimede.Services;
using Ganimede.Models;
@@ -30,7 +29,6 @@ namespace Ganimede
{
InitializeComponent();
InitializeUI();
ConfigureFFMpeg();
}
private void InitializeUI()
@@ -51,9 +49,67 @@ namespace Ganimede
_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(() =>
@@ -71,7 +127,7 @@ namespace Ganimede
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
Dispatcher.Invoke(() =>
{
JobsSummaryText.Text = $"In attesa: {pending} | In corso: {processing} | Completati: {completed} | Falliti: {failed}";
JobsSummaryText.Text = $"In Attesa: {pending} | In Corso: {processing} | Completati: {completed} | Falliti: {failed}";
});
}
@@ -81,7 +137,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true;
StatusText.Text = "Elaborazione coda...";
StatusText.Text = "Elaborazione coda in corso...";
UpdateJobsSummary();
});
}
@@ -139,112 +195,6 @@ namespace Ganimede
}
}
private void ConfigureFFMpeg()
{
var ffmpegBin = Settings.Default.FFmpegBinFolder;
if (!string.IsNullOrEmpty(ffmpegBin) && ValidateFFMpegBinaries(ffmpegBin))
FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
else if (TryUseSystemFFMpeg()) { }
else if (TryFixMissingFFMpeg(ffmpegBin))
FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
}
private bool ValidateFFMpegBinaries(string binFolder) =>
Directory.Exists(binFolder) &&
File.Exists(Path.Combine(binFolder, "ffmpeg.exe")) &&
File.Exists(Path.Combine(binFolder, "ffprobe.exe"));
private bool TryUseSystemFFMpeg()
{
try
{
var psi = new ProcessStartInfo { FileName = "ffmpeg", Arguments = "-version", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
using var p = Process.Start(psi);
return p != null && p.WaitForExit(4000) && p.ExitCode == 0;
}
catch { return false; }
}
private bool TryFixMissingFFMpeg(string binFolder)
{
if (string.IsNullOrEmpty(binFolder) || !Directory.Exists(binFolder)) return false;
var ffmpegPath = Path.Combine(binFolder, "ffmpeg.exe");
var ffprobePath = Path.Combine(binFolder, "ffprobe.exe");
if (!File.Exists(ffmpegPath) && File.Exists(ffprobePath))
{
try { File.Copy(ffprobePath, ffmpegPath, true); return File.Exists(ffmpegPath); } catch { return false; }
}
return false;
}
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new WpfOpenFileDialog { Filter = "Video (*.mp4;*.avi;*.mov;*.mkv;*.wmv)|*.mp4;*.avi;*.mov;*.mkv;*.wmv|Tutti i file (*.*)|*.*", 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 con 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 (In attesa)";
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 SettingsButton_Click(object sender, RoutedEventArgs e)
{
var win = new SettingsWindow { Owner = this };
if (win.ShowDialog() == true)
{
ConfigureFFMpeg();
StatusText.Text = "Impostazioni aggiornate";
}
}
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
{
if (_processingService.JobQueue.Count == 0)
@@ -254,7 +204,7 @@ namespace Ganimede
}
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
{
WpfMessageBox.Show("Nessun job in stato In attesa.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
WpfMessageBox.Show("Nessun job in attesa nella coda.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
await _processingService.StartProcessingAsync();
@@ -281,15 +231,23 @@ namespace Ganimede
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";
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
if (!string.IsNullOrEmpty(outputFolder))
{
var createSub = Settings.Default.CreateSubfolder;
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame ? outputFolder : (createSub ? Path.Combine(outputFolder, job.VideoName) : 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();
}
}
@@ -317,7 +275,7 @@ namespace Ganimede
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing);
if (processing)
{
var res = WpfMessageBox.Show("Ci sono job in elaborazione.\n\nSi: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
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)
{
@@ -334,7 +292,7 @@ namespace Ganimede
}
else
{
if (WpfMessageBox.Show("Rimuovere tutti i job?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
if (WpfMessageBox.Show("Rimuovere tutti i job dalla coda?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
_processingService.JobQueue.Clear();
thumbnails.Clear();
@@ -344,6 +302,97 @@ namespace Ganimede
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();
@@ -360,5 +409,32 @@ namespace Ganimede
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;
}
}
}
}

View File

@@ -1,4 +0,0 @@
namespace Ganimede.Models
{
// Modelli futuri (es. VideoInfo, FrameInfo)
}

View File

@@ -47,18 +47,6 @@ namespace Ganimede.Properties {
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("C:\\Users\\balbo\\source\\repos\\Ganimede\\Ganimede\\Ganimede\\FFMpeg")]
public string FFmpegBinFolder {
get {
return ((string)(this["FFmpegBinFolder"]));
}
set {
this["FFmpegBinFolder"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]

View File

@@ -3,11 +3,11 @@ using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using FFMpegCore;
using System.IO;
using Ganimede.Models;
using Ganimede.Properties;
using Ganimede.Helpers;
using Ganimede.VideoProcessing;
namespace Ganimede.Services
{
@@ -146,11 +146,10 @@ namespace Ganimede.Services
{
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
// (Do not decide folder change yet; need analysis for Auto mode)
var mediaInfo = await FFProbe.AnalyseAsync(job.VideoPath);
int frameRate = (int)(mediaInfo.PrimaryVideoStream?.FrameRate ?? 24);
int totalFrames = (int)(mediaInfo.Duration.TotalSeconds * frameRate);
// 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
@@ -161,11 +160,10 @@ namespace Ganimede.Services
suggestSingleFrame = true;
else if (mediaInfo.Duration.TotalSeconds >= 3 && mediaInfo.Duration.TotalSeconds <= 45)
{
var primary = mediaInfo.PrimaryVideoStream;
if (primary != null && primary.BitRate > 0 && primary.Width > 0 && primary.Height > 0)
if (mediaInfo.BitRate > 0 && mediaInfo.Width > 0 && mediaInfo.Height > 0)
{
double pixels = primary.Width * primary.Height;
if (primary.BitRate < pixels * 0.3)
double pixels = mediaInfo.Width * mediaInfo.Height;
if (mediaInfo.BitRate < pixels * 0.3)
suggestSingleFrame = true;
}
}
@@ -224,7 +222,7 @@ namespace Ganimede.Services
job.StatusMessage = "Frame already exists (skipped)";
else
{
await ExtractFrameAsync(job, targetIndex, frameRate, frameSize, framePath);
await ExtractFrameAsync(job, frameTime, frameSize, framePath, cancellationToken);
job.StatusMessage = "Single frame extracted";
}
job.Progress = 100;
@@ -234,32 +232,47 @@ namespace Ganimede.Services
return;
}
// Full extraction loop (unchanged)
// Full extraction using FrameExtractor
int processedFrames = 0;
int skippedFrames = 0;
for (int i = 0; i < totalFrames; i++)
await Task.Run(() =>
{
if (cancellationToken.IsCancellationRequested)
{
job.Status = JobStatus.Cancelled;
job.StatusMessage = "Cancelled by user";
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
return;
}
var frameTime = TimeSpan.FromSeconds((double)i / frameRate);
var fileName = NamingHelper.GenerateFileName(namingPattern, job, i, frameTime, customPrefix);
string framePath = Path.Combine(job.OutputFolder, fileName);
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
skippedFrames++;
else
{
await ExtractFrameAsync(job, i, frameRate, frameSize, framePath);
processedFrames++;
}
job.Progress = (double)(i + 1) / totalFrames * 100;
job.StatusMessage = $"Processed {processedFrames}/{totalFrames} frames ({job.Progress:F1}%)" + (skippedFrames > 0 ? $" - Skipped {skippedFrames}" : "");
if (i % 10 == 0) await Task.Delay(1, cancellationToken);
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;
@@ -327,62 +340,25 @@ namespace Ganimede.Services
return Settings.Default.DefaultCustomPrefix ?? "custom";
}
private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath)
private async Task ExtractFrameAsync(VideoJob job, TimeSpan frameTime, (int width, int height) frameSize, string framePath, CancellationToken cancellationToken)
{
var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate);
try
{
if (frameSize.width == -1 && frameSize.height == -1)
await Task.Run(() =>
{
try
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.WithVideoCodec("png"))
.ProcessAsynchronously();
return;
}
catch
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1))
.ProcessAsynchronously();
return;
}
}
try
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.WithVideoCodec("png")
.Resize(frameSize.width, frameSize.height))
.ProcessAsynchronously();
}
catch
{
await FFMpegArguments
.FromFileInput(job.VideoPath)
.OutputToFile(framePath, true, options => options
.Seek(frameTime)
.WithFrameOutputCount(1)
.Resize(frameSize.width, frameSize.height))
.ProcessAsynchronously();
}
FrameExtractor.ExtractFrame(
job.VideoPath,
frameTime,
framePath,
frameSize.width,
frameSize.height
);
}, cancellationToken);
}
catch (Exception ex)
{
Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}");
Debug.WriteLine($"[ERROR] Failed to extract frame from {job.VideoName}: {ex.Message}");
throw;
}
}
}

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

View File

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

View File

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

View 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
}

View File

@@ -1,4 +0,0 @@
namespace Ganimede.ViewModels
{
// ViewModel principale e futuri ViewModel
}

View File

@@ -1,4 +0,0 @@
namespace Ganimede.Views
{
// Views aggiuntive se necessario
}

View File

@@ -93,13 +93,21 @@ namespace Ganimede.Windows
if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0;
if (CustomNamingComboBox.SelectedItem == null) CustomNamingComboBox.SelectedIndex = 0;
UpdateJobNamingPreview();
// 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))
@@ -114,9 +122,13 @@ namespace Ganimede.Windows
JobNamingPreviewText.Text = "Video1_000001.png (default)";
}
}
catch
catch (Exception ex)
{
JobNamingPreviewText.Text = "Video1_000001.png";
System.Diagnostics.Debug.WriteLine($"[ERROR] UpdateJobNamingPreview: {ex.Message}");
if (JobNamingPreviewText != null)
{
JobNamingPreviewText.Text = "Video1_000001.png";
}
}
}

View File

@@ -4,7 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Impostazioni" Height="600" Width="640"
Title="Impostazioni" Height="550" Width="640"
Background="#1E2228" WindowStartupLocation="CenterOwner">
<Grid Margin="22">
<Grid.RowDefinitions>
@@ -19,24 +19,6 @@
<!-- Settings Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- FFmpeg Settings -->
<GroupBox Header="Configurazione FFmpeg" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
<StackPanel Margin="12">
<TextBlock Text="Cartella Binari FFmpeg:" Foreground="#CCC" Margin="0,0,0,6"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="FFmpegPathTextBox" Grid.Column="0" Height="34" VerticalContentAlignment="Center"
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
<Button x:Name="BrowseFFmpegButton" Grid.Column="1" Content="Sfoglia" Width="90" Height="34"
Click="BrowseFFmpegButton_Click"/>
</Grid>
<TextBlock x:Name="FFmpegStatusText" Foreground="#AAA" FontSize="12" Margin="0,6,0,0"/>
</StackPanel>
</GroupBox>
<!-- Output Settings -->
<GroupBox Header="Output" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
<StackPanel Margin="12">

View File

@@ -20,7 +20,6 @@ namespace Ganimede.Windows
private void LoadSettings()
{
FFmpegPathTextBox.Text = Settings.Default.FFmpegBinFolder;
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
@@ -55,8 +54,6 @@ namespace Ganimede.Windows
default:
GetDefaultModeRadio("DefaultModeFullRadio")!.IsChecked = true; break;
}
UpdateFFmpegStatus();
}
private string GetSelectedDefaultExtractionMode()
@@ -68,43 +65,6 @@ namespace Ganimede.Windows
private bool GetSingleFrameUseSubfolder() => GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
private void UpdateFFmpegStatus()
{
var path = FFmpegPathTextBox.Text;
if (string.IsNullOrEmpty(path))
{
FFmpegStatusText.Text = "Nessun percorso specificato";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Orange);
}
else if (ValidateFFMpegPath(path))
{
FFmpegStatusText.Text = "? FFmpeg valido";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGreen);
}
else
{
FFmpegStatusText.Text = "? Binari FFmpeg non trovati";
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
}
}
private bool ValidateFFMpegPath(string path)
{
if (!Directory.Exists(path)) return false;
return File.Exists(Path.Combine(path, "ffmpeg.exe")) && File.Exists(Path.Combine(path, "ffprobe.exe"));
}
private void BrowseFFmpegButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella binari FFmpeg", ShowNewFolderButton = false };
if (!string.IsNullOrEmpty(FFmpegPathTextBox.Text)) dialog.SelectedPath = FFmpegPathTextBox.Text;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
FFmpegPathTextBox.Text = dialog.SelectedPath;
UpdateFFmpegStatus();
}
}
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella output predefinita", ShowNewFolderButton = true };
@@ -119,7 +79,6 @@ namespace Ganimede.Windows
{
try
{
Settings.Default.FFmpegBinFolder = FFmpegPathTextBox.Text;
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;