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.
This commit is contained in:
2025-12-08 01:09:57 +01:00
parent 11931854c7
commit 627a157762
12 changed files with 900 additions and 449 deletions

View File

@@ -7,5 +7,13 @@ namespace Ganimede
/// </summary> /// </summary>
public partial class App : System.Windows.Application public partial class App : System.Windows.Application
{ {
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Windows Media Foundation is initialized automatically when needed
// No external dependencies or setup required!
System.Diagnostics.Debug.WriteLine("[Ganimede] Application started - Using native Windows Media Foundation");
}
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FFMediaToolkit" Version="4.8.1" /> <!-- Only System.Drawing.Common for image manipulation -->
<PackageReference Include="System.Drawing.Common" Version="10.0.0" /> <PackageReference Include="System.Drawing.Common" Version="10.0.0" />
</ItemGroup> </ItemGroup>
@@ -30,4 +30,9 @@
</None> </None>
</ItemGroup> </ItemGroup>
<!-- Native Windows libraries (included in Windows) -->
<ItemGroup>
<Reference Include="System.Drawing" />
</ItemGroup>
</Project> </Project>

View File

@@ -5,25 +5,28 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ganimede" xmlns:local="clr-namespace:Ganimede"
mc:Ignorable="d" mc:Ignorable="d"
Title="Ganimede - Video Frame Extractor" Height="750" Width="1200" Title="Ganimede - Estrattore Frame Video" Height="750" Width="1200"
Background="#F5F7FA" WindowStartupLocation="CenterScreen"> Background="#0F1419" WindowStartupLocation="CenterScreen">
<Window.Resources> <Window.Resources>
<local:StatusColorConverter x:Key="StatusColorConverter"/> <local:StatusColorConverter x:Key="StatusColorConverter"/>
<!-- Modern Color Palette --> <!-- Dark Mode Color Palette -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#6366F1"/> <SolidColorBrush x:Key="PrimaryBrush" Color="#6366F1"/>
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="#4F46E5"/> <SolidColorBrush x:Key="PrimaryDarkBrush" Color="#4F46E5"/>
<SolidColorBrush x:Key="PrimaryLightBrush" Color="#818CF8"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#10B981"/> <SolidColorBrush x:Key="SuccessBrush" Color="#10B981"/>
<SolidColorBrush x:Key="DangerBrush" Color="#EF4444"/> <SolidColorBrush x:Key="DangerBrush" Color="#EF4444"/>
<SolidColorBrush x:Key="WarningBrush" Color="#F59E0B"/> <SolidColorBrush x:Key="WarningBrush" Color="#F59E0B"/>
<SolidColorBrush x:Key="BackgroundBrush" Color="#F5F7FA"/> <SolidColorBrush x:Key="BackgroundBrush" Color="#0F1419"/>
<SolidColorBrush x:Key="SurfaceBrush" Color="#FFFFFF"/> <SolidColorBrush x:Key="SurfaceBrush" Color="#1A1F26"/>
<SolidColorBrush x:Key="BorderBrush" Color="#E5E7EB"/> <SolidColorBrush x:Key="SurfaceLightBrush" Color="#22272E"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#111827"/> <SolidColorBrush x:Key="BorderBrush" Color="#30363D"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#6B7280"/> <SolidColorBrush x:Key="TextPrimaryBrush" Color="#F0F6FC"/>
<SolidColorBrush x:Key="TextMutedBrush" Color="#9CA3AF"/> <SolidColorBrush x:Key="TextSecondaryBrush" Color="#9DA5B4"/>
<SolidColorBrush x:Key="TextMutedBrush" Color="#636C76"/>
<SolidColorBrush x:Key="HoverBrush" Color="#2C3138"/>
<!-- Modern Button Style --> <!-- Modern Button Style (Dark) -->
<Style TargetType="Button" x:Key="ModernButton"> <Style TargetType="Button" x:Key="ModernButton">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/> <Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/> <Setter Property="Foreground" Value="White"/>
@@ -42,7 +45,7 @@
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}"/> <Setter Property="Background" Value="{StaticResource PrimaryLightBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsEnabled" Value="False"> <Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5"/> <Setter Property="Opacity" Value="0.5"/>
@@ -70,7 +73,7 @@
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F9FAFB"/> <Setter Property="Background" Value="{StaticResource HoverBrush}"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
@@ -86,47 +89,38 @@
<Setter Property="Background" Value="{StaticResource SuccessBrush}"/> <Setter Property="Background" Value="{StaticResource SuccessBrush}"/>
</Style> </Style>
<!-- Modern TabControl Style --> <!-- Navigation Button Style -->
<Style TargetType="TabControl"> <Style TargetType="RadioButton" x:Key="NavButton">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
</Style> <Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="Medium"/>
<Style TargetType="TabItem"> <Setter Property="Padding" Value="16,12"/>
<Setter Property="Margin" Value="0,4,0,0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="TabItem"> <ControlTemplate TargetType="RadioButton">
<Border Name="Border" <Border Background="{TemplateBinding Background}"
Background="Transparent" CornerRadius="8"
BorderThickness="0,0,0,3" Padding="{TemplateBinding Padding}">
BorderBrush="Transparent" <ContentPresenter/>
Padding="20,12"
Margin="0,0,8,0">
<ContentPresenter x:Name="ContentSite"
VerticalAlignment="Center"
HorizontalAlignment="Center"
ContentSource="Header"/>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource PrimaryBrush}"/> <Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/> <Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#F9FAFB"/> <Setter Property="Background" Value="{StaticResource HoverBrush}"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
<Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="Medium"/>
</Style> </Style>
<!-- Modern Card Style --> <!-- Modern Card Style (Dark) -->
<Style x:Key="Card" TargetType="Border"> <Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/> <Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/> <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
@@ -135,14 +129,14 @@
<Setter Property="Padding" Value="20"/> <Setter Property="Padding" Value="20"/>
<Setter Property="Effect"> <Setter Property="Effect">
<Setter.Value> <Setter.Value>
<DropShadowEffect Color="#000000" Opacity="0.05" BlurRadius="10" ShadowDepth="0"/> <DropShadowEffect Color="#000000" Opacity="0.3" BlurRadius="15" ShadowDepth="0"/>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
<!-- Modern TextBox Style --> <!-- Modern TextBox Style (Dark) -->
<Style TargetType="TextBox"> <Style TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/> <Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/> <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1.5"/> <Setter Property="BorderThickness" Value="1.5"/>
<Setter Property="Padding" Value="12,8"/> <Setter Property="Padding" Value="12,8"/>
@@ -162,10 +156,10 @@
</Setter> </Setter>
</Style> </Style>
<!-- Modern ProgressBar Style --> <!-- Modern ProgressBar Style (Dark) -->
<Style TargetType="ProgressBar"> <Style TargetType="ProgressBar">
<Setter Property="Height" Value="8"/> <Setter Property="Height" Value="8"/>
<Setter Property="Background" Value="#E5E7EB"/> <Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/> <Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template"> <Setter Property="Template">
@@ -181,6 +175,25 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </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> </Window.Resources>
<Grid> <Grid>
@@ -203,7 +216,7 @@
FontSize="20" FontSize="20"
FontWeight="Bold" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}"/> Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="Video Frame Extractor" <TextBlock Text="Estrattore Frame Video"
FontSize="12" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}"/> Foreground="{StaticResource TextSecondaryBrush}"/>
</StackPanel> </StackPanel>
@@ -211,11 +224,47 @@
</DockPanel> </DockPanel>
</Border> </Border>
<!-- Main Content with Tabs --> <!-- Main Content with Sidebar Navigation -->
<TabControl Grid.Row="1" Margin="24,16,24,16"> <Grid Grid.Row="1">
<!-- Processing Tab --> <Grid.ColumnDefinitions>
<TabItem Header="🎥 Processing"> <ColumnDefinition Width="240"/>
<Grid Margin="0,24,0,0"> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Vertical Sidebar Navigation -->
<Border Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="0,0,1,0"
Padding="16">
<StackPanel>
<TextBlock Text="NAVIGAZIONE"
FontSize="11"
FontWeight="SemiBold"
Foreground="{StaticResource TextMutedBrush}"
Margin="16,8,0,12"/>
<RadioButton x:Name="ProcessingNavButton"
Style="{StaticResource NavButton}"
Content="🎥 Elaborazione"
IsChecked="True"
Checked="NavigationButton_Checked"/>
<RadioButton x:Name="LibraryNavButton"
Style="{StaticResource NavButton}"
Content="📚 Libreria"
Checked="NavigationButton_Checked"/>
<RadioButton x:Name="SettingsNavButton"
Style="{StaticResource NavButton}"
Content="⚙️ Impostazioni"
Checked="NavigationButton_Checked"/>
</StackPanel>
</Border>
<!-- Content Area -->
<Grid Grid.Column="1">
<!-- Processing View -->
<Grid x:Name="ProcessingView" Visibility="Visible" Margin="24,16,24,16">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
@@ -231,15 +280,15 @@
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Button Style="{StaticResource ModernButton}" <Button Style="{StaticResource ModernButton}"
Content=" Add Videos" Content=" Aggiungi Video"
Click="BrowseVideoButton_Click" Click="BrowseVideoButton_Click"
Margin="0,0,12,0"/> Margin="0,0,12,0"/>
<Button Style="{StaticResource OutlineButton}" <Button Style="{StaticResource OutlineButton}"
Content="📁 Import Folder" Content="📁 Importa Cartella"
Click="ImportFolderButton_Click" Click="ImportFolderButton_Click"
Margin="0,0,12,0"/> Margin="0,0,12,0"/>
<Button Style="{StaticResource OutlineButton}" <Button Style="{StaticResource OutlineButton}"
Content="⚙️ Configure Selected" Content="⚙️ Configura Selezionati"
x:Name="ConfigureSelectedButton" x:Name="ConfigureSelectedButton"
IsEnabled="False" IsEnabled="False"
Click="ConfigureSelectedButton_Click"/> Click="ConfigureSelectedButton_Click"/>
@@ -247,20 +296,20 @@
<StackPanel Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Style="{StaticResource SuccessButton}" <Button Style="{StaticResource SuccessButton}"
Content="▶️ Start Queue" Content="▶️ Avvia Coda"
Width="140" Width="130"
x:Name="StartQueueButton" x:Name="StartQueueButton"
Click="StartQueueButton_Click" Click="StartQueueButton_Click"
Margin="0,0,8,0"/> Margin="0,0,8,0"/>
<Button Style="{StaticResource DangerButton}" <Button Style="{StaticResource DangerButton}"
Content="⏹️ Stop" Content="⏹️ Ferma"
Width="100" Width="100"
x:Name="StopQueueButton" x:Name="StopQueueButton"
IsEnabled="False" IsEnabled="False"
Click="StopQueueButton_Click" Click="StopQueueButton_Click"
Margin="0,0,8,0"/> Margin="0,0,8,0"/>
<Button Style="{StaticResource OutlineButton}" <Button Style="{StaticResource OutlineButton}"
Content="🧹 Clear" Content="🧹 Pulisci"
Click="ClearCompletedButton_Click"/> Click="ClearCompletedButton_Click"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
@@ -275,7 +324,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DockPanel Margin="0,0,0,16"> <DockPanel Margin="0,0,0,16">
<TextBlock Text="Processing Queue" <TextBlock Text="Coda di Elaborazione"
FontSize="18" FontSize="18"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"/> Foreground="{StaticResource TextPrimaryBrush}"/>
@@ -290,7 +339,7 @@
<ItemsControl x:Name="QueueItemsControl"> <ItemsControl x:Name="QueueItemsControl">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border Background="#F9FAFB" <Border Background="{StaticResource SurfaceLightBrush}"
Margin="0,0,0,12" Margin="0,0,0,12"
Padding="16" Padding="16"
CornerRadius="8" CornerRadius="8"
@@ -357,10 +406,10 @@
FontSize="12" FontSize="12"
Foreground="{StaticResource TextMutedBrush}" Foreground="{StaticResource TextMutedBrush}"
Margin="0,8,0,0"> Margin="0,8,0,0">
<Run Text="Mode:"/> <Run Text="Modalità:"/>
<Run Text="{Binding ExtractionModeDisplay}" FontWeight="Medium"/> <Run Text="{Binding ExtractionModeDisplay, Mode=OneWay}" FontWeight="Medium"/>
<Run Text=" • Output:"/> <Run Text=" • Output:"/>
<Run Text="{Binding OutputFolderDisplay}" FontWeight="Medium"/> <Run Text="{Binding OutputFolderDisplay, Mode=OneWay}" FontWeight="Medium"/>
</TextBlock> </TextBlock>
</Grid> </Grid>
</Border> </Border>
@@ -371,11 +420,9 @@
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
</TabItem>
<!-- Library Tab --> <!-- Library View -->
<TabItem Header="📚 Library"> <Grid x:Name="LibraryView" Visibility="Collapsed" Margin="24,16,24,16">
<Grid Margin="0,24,0,0">
<Border Style="{StaticResource Card}"> <Border Style="{StaticResource Card}">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -384,19 +431,19 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="Output Preview" <TextBlock Text="Anteprima Output"
FontSize="18" FontSize="18"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}" Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<DockPanel Grid.Row="1" Margin="0,0,0,16"> <DockPanel Grid.Row="1" Margin="0,0,0,16">
<TextBlock Text="Output Folder:" <TextBlock Text="Cartella Output:"
Foreground="{StaticResource TextSecondaryBrush}" Foreground="{StaticResource TextSecondaryBrush}"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="0,0,12,0"/> Margin="0,0,12,0"/>
<Button DockPanel.Dock="Right" <Button DockPanel.Dock="Right"
Content="Browse" Content="Sfoglia"
Style="{StaticResource OutlineButton}" Style="{StaticResource OutlineButton}"
Padding="12,6" Padding="12,6"
Click="SelectOutputFolderButton_Click" Click="SelectOutputFolderButton_Click"
@@ -407,7 +454,7 @@
</DockPanel> </DockPanel>
<Border Grid.Row="2" <Border Grid.Row="2"
Background="#F9FAFB" Background="{StaticResource SurfaceLightBrush}"
BorderBrush="{StaticResource BorderBrush}" BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="8" CornerRadius="8"
@@ -425,7 +472,7 @@
BorderBrush="{StaticResource BorderBrush}" BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="6" CornerRadius="6"
Background="White"> Background="{StaticResource SurfaceBrush}">
<Image Source="{Binding}" <Image Source="{Binding}"
Width="140" Width="140"
Height="80" Height="80"
@@ -439,53 +486,51 @@
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
</TabItem>
<!-- Settings Tab --> <!-- Settings View -->
<TabItem Header="⚙️ Settings"> <ScrollViewer x:Name="SettingsView" Visibility="Collapsed" Margin="24,16,24,16" VerticalScrollBarVisibility="Auto">
<ScrollViewer Margin="0,24,0,0" VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="800"> <StackPanel MaxWidth="800">
<!-- Frame Settings Card --> <!-- Frame Settings Card -->
<Border Style="{StaticResource Card}" Margin="0,0,0,16"> <Border Style="{StaticResource Card}" Margin="0,0,0,16">
<StackPanel> <StackPanel>
<TextBlock Text="Frame Settings" <TextBlock Text="Impostazioni Frame"
FontSize="18" FontSize="18"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}" Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,0,16"/> Margin="0,0,0,16"/>
<TextBlock Text="Default Frame Size" <TextBlock Text="Dimensione Frame Predefinita"
Foreground="{StaticResource TextSecondaryBrush}" Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<ComboBox x:Name="FrameSizeComboBox" <ComboBox x:Name="FrameSizeComboBox"
Height="42" Height="42"
FontSize="14" FontSize="14"
Margin="0,0,0,16"> Margin="0,0,0,16">
<ComboBoxItem Content="Original Size" Tag="original" IsSelected="True"/> <ComboBoxItem Content="Dimensione Originale" Tag="original" IsSelected="True"/>
<ComboBoxItem Content="320x180 (Fast)" Tag="320,180"/> <ComboBoxItem Content="320x180 (Veloce)" Tag="320,180"/>
<ComboBoxItem Content="640x360 (Medium)" Tag="640,360"/> <ComboBoxItem Content="640x360 (Medio)" Tag="640,360"/>
<ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/> <ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/>
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/> <ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
</ComboBox> </ComboBox>
<TextBlock Text="Extraction Mode" <TextBlock Text="Modalità di Estrazione"
Foreground="{StaticResource TextSecondaryBrush}" Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8"> <StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<RadioButton x:Name="DefaultModeFullRadio" <RadioButton x:Name="DefaultModeFullRadio"
Content="Full Extraction" Content="Estrazione Completa"
GroupName="DefExtraction" GroupName="DefExtraction"
IsChecked="True" IsChecked="True"
Margin="0,0,24,0"/> Margin="0,0,24,0"/>
<RadioButton x:Name="DefaultModeSingleRadio" <RadioButton x:Name="DefaultModeSingleRadio"
Content="Single Frame" Content="Frame Singolo"
GroupName="DefExtraction" GroupName="DefExtraction"
Margin="0,0,24,0"/> Margin="0,0,24,0"/>
<RadioButton x:Name="DefaultModeAutoRadio" <RadioButton x:Name="DefaultModeAutoRadio"
Content="Auto Detect" Content="Rilevamento Automatico"
GroupName="DefExtraction"/> GroupName="DefExtraction"/>
</StackPanel> </StackPanel>
<TextBlock Text="Auto mode analyzes the video and decides the best extraction method." <TextBlock Text="La modalità automatica analizza il video e decide il metodo di estrazione migliore."
FontSize="12" FontSize="12"
Foreground="{StaticResource TextMutedBrush}" Foreground="{StaticResource TextMutedBrush}"
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
@@ -495,45 +540,45 @@
<!-- Output Settings Card --> <!-- Output Settings Card -->
<Border Style="{StaticResource Card}" Margin="0,0,0,16"> <Border Style="{StaticResource Card}" Margin="0,0,0,16">
<StackPanel> <StackPanel>
<TextBlock Text="Output Settings" <TextBlock Text="Impostazioni Output"
FontSize="18" FontSize="18"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}" Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,0,16"/> Margin="0,0,0,16"/>
<CheckBox x:Name="CreateSubfolderCheckBox" <CheckBox x:Name="CreateSubfolderCheckBox"
Content="Create subfolder for each video" Content="Crea sottocartella per ogni video"
IsChecked="True" IsChecked="True"
Margin="0,0,0,16"/> Margin="0,0,0,16"/>
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox" <CheckBox x:Name="SingleFrameUseSubfolderCheckBox"
Content="Use subfolder for single frame extraction" Content="Usa sottocartella per estrazione frame singolo"
Margin="0,0,0,16"/> Margin="0,0,0,16"/>
<TextBlock Text="Overwrite Behavior" <TextBlock Text="Comportamento Sovrascrittura"
Foreground="{StaticResource TextSecondaryBrush}" Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,0,0,8"/> Margin="0,0,0,8"/>
<ComboBox x:Name="OverwriteModeComboBox" <ComboBox x:Name="OverwriteModeComboBox"
Height="42" Height="42"
FontSize="14"> FontSize="14">
<ComboBoxItem Content="Ask before overwrite" Tag="Ask" IsSelected="True"/> <ComboBoxItem Content="Chiedi prima di sovrascrivere" Tag="Ask" IsSelected="True"/>
<ComboBoxItem Content="Skip existing files" Tag="Skip"/> <ComboBoxItem Content="Salta file esistenti" Tag="Skip"/>
<ComboBoxItem Content="Overwrite existing files" Tag="Overwrite"/> <ComboBoxItem Content="Sovrascrivi file esistenti" Tag="Overwrite"/>
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- Action Buttons --> <!-- Action Buttons -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Save Settings" <Button Content="Salva Impostazioni"
Style="{StaticResource ModernButton}" Style="{StaticResource ModernButton}"
Width="140" Width="150"
Click="SaveSettings_Click"/> Click="SaveSettings_Click"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </Grid>
</TabControl> </Grid>
<!-- Footer Status Bar --> <!-- Footer Status Bar -->
<Border Grid.Row="2" <Border Grid.Row="2"
@@ -548,7 +593,7 @@
FontSize="16" FontSize="16"
Margin="0,0,8,0"/> Margin="0,0,8,0"/>
<TextBlock x:Name="StatusText" <TextBlock x:Name="StatusText"
Text="Ready" Text="Pronto"
Foreground="{StaticResource TextSecondaryBrush}" Foreground="{StaticResource TextSecondaryBrush}"
FontSize="13"/> FontSize="13"/>
</StackPanel> </StackPanel>

View File

@@ -127,7 +127,7 @@ namespace Ganimede
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed); var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
JobsSummaryText.Text = $"Pending: {pending} | Processing: {processing} | Completed: {completed} | Failed: {failed}"; JobsSummaryText.Text = $"In Attesa: {pending} | In Corso: {processing} | Completati: {completed} | Falliti: {failed}";
}); });
} }
@@ -137,7 +137,7 @@ namespace Ganimede
{ {
StartQueueButton.IsEnabled = false; StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true; StopQueueButton.IsEnabled = true;
StatusText.Text = "Processing queue..."; StatusText.Text = "Elaborazione coda in corso...";
UpdateJobsSummary(); UpdateJobsSummary();
}); });
} }
@@ -148,7 +148,7 @@ namespace Ganimede
{ {
StartQueueButton.IsEnabled = true; StartQueueButton.IsEnabled = true;
StopQueueButton.IsEnabled = false; StopQueueButton.IsEnabled = false;
StatusText.Text = "Queue stopped"; StatusText.Text = "Coda fermata";
UpdateJobsSummary(); UpdateJobsSummary();
}); });
} }
@@ -157,7 +157,7 @@ namespace Ganimede
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
StatusText.Text = $"✓ Completed: {job.VideoName}"; StatusText.Text = $"✓ Completato: {job.VideoName}";
LoadThumbnailsFromFolder(job.OutputFolder); LoadThumbnailsFromFolder(job.OutputFolder);
UpdateJobsSummary(); UpdateJobsSummary();
}); });
@@ -167,7 +167,7 @@ namespace Ganimede
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
StatusText.Text = $"✗ Failed: {job.VideoName}"; StatusText.Text = $"✗ Fallito: {job.VideoName}";
UpdateJobsSummary(); UpdateJobsSummary();
}); });
} }
@@ -199,12 +199,12 @@ namespace Ganimede
{ {
if (_processingService.JobQueue.Count == 0) if (_processingService.JobQueue.Count == 0)
{ {
WpfMessageBox.Show("No videos in queue.", "Empty Queue", MessageBoxButton.OK, MessageBoxImage.Information); WpfMessageBox.Show("Nessun video in coda.", "Coda Vuota", MessageBoxButton.OK, MessageBoxImage.Information);
return; return;
} }
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending)) if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
{ {
WpfMessageBox.Show("No pending jobs in queue.", "No Jobs", MessageBoxButton.OK, MessageBoxImage.Information); WpfMessageBox.Show("Nessun job in attesa nella coda.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
return; return;
} }
await _processingService.StartProcessingAsync(); await _processingService.StartProcessingAsync();
@@ -214,7 +214,7 @@ namespace Ganimede
private void StopQueueButton_Click(object sender, RoutedEventArgs e) private void StopQueueButton_Click(object sender, RoutedEventArgs e)
{ {
_processingService.StopProcessing(); _processingService.StopProcessing();
StatusText.Text = "Stopping..."; StatusText.Text = "Arresto in corso...";
UpdateJobsSummary(); UpdateJobsSummary();
} }
@@ -235,9 +235,8 @@ namespace Ganimede
var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this }; var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this };
if (cfg.ShowDialog() == true) if (cfg.ShowDialog() == true)
{ {
StatusText.Text = $"Configuration applied to {_selectedJobs.Count} job(s)"; StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
// Update output folders only if outputFolder is set
if (!string.IsNullOrEmpty(outputFolder)) if (!string.IsNullOrEmpty(outputFolder))
{ {
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder))) foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
@@ -267,7 +266,7 @@ namespace Ganimede
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e) private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
{ {
_processingService.RemoveCompletedJobs(); _processingService.RemoveCompletedJobs();
StatusText.Text = "Completed jobs removed"; StatusText.Text = "Job completati rimossi";
UpdateQueueCount(); UpdateQueueCount();
} }
@@ -276,7 +275,7 @@ namespace Ganimede
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing); var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing);
if (processing) if (processing)
{ {
var res = WpfMessageBox.Show("There are jobs being processed.\n\nYes: Stop and clear queue\nNo: Remove only non-processing jobs\nCancel: Cancel operation", "Confirm", 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.Cancel) return;
if (res == MessageBoxResult.Yes) if (res == MessageBoxResult.Yes)
{ {
@@ -293,13 +292,13 @@ namespace Ganimede
} }
else else
{ {
if (WpfMessageBox.Show("Remove all jobs from queue?", "Confirm", 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(); _processingService.JobQueue.Clear();
thumbnails.Clear(); thumbnails.Clear();
} }
} }
StatusText.Text = "Queue updated"; StatusText.Text = "Coda aggiornata";
UpdateQueueCount(); UpdateQueueCount();
} }
@@ -311,7 +310,7 @@ namespace Ganimede
private void ImportFolderButton_Click(object sender, RoutedEventArgs e) private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
{ {
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Select folder containing videos", ShowNewFolderButton = false }; 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) if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{ {
try try
@@ -319,15 +318,15 @@ namespace Ganimede
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray(); var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
if (files.Length == 0) if (files.Length == 0)
{ {
WpfMessageBox.Show("No valid video files found.", "Import Folder", MessageBoxButton.OK, MessageBoxImage.Information); WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
return; return;
} }
AddVideosToQueue(files); AddVideosToQueue(files);
StatusText.Text = $"Imported {files.Length} video(s)"; StatusText.Text = $"Importati {files.Length} video";
} }
catch (Exception ex) catch (Exception ex)
{ {
WpfMessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
} }
@@ -336,12 +335,12 @@ namespace Ganimede
{ {
if (string.IsNullOrEmpty(outputFolder)) if (string.IsNullOrEmpty(outputFolder))
{ {
WpfMessageBox.Show("Please select an output folder first.", "Output Folder Required", MessageBoxButton.OK, MessageBoxImage.Warning); WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
return; return;
} }
var createSub = Settings.Default.CreateSubfolder; var createSub = Settings.Default.CreateSubfolder;
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub); foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
StatusText.Text = $"Added {paths.Length} video(s)"; StatusText.Text = $"Aggiunti {paths.Length} video";
Settings.Default.LastVideoPath = paths.FirstOrDefault(); Settings.Default.LastVideoPath = paths.FirstOrDefault();
Settings.Default.Save(); Settings.Default.Save();
UpdateQueueCount(); UpdateQueueCount();
@@ -355,7 +354,7 @@ namespace Ganimede
{ {
outputFolder = dialog.SelectedPath; outputFolder = dialog.SelectedPath;
GlobalOutputFolderTextBox.Text = outputFolder; GlobalOutputFolderTextBox.Text = outputFolder;
StatusText.Text = "Output folder updated"; StatusText.Text = "Cartella output aggiornata";
Settings.Default.LastOutputFolder = outputFolder; Settings.Default.LastOutputFolder = outputFolder;
Settings.Default.Save(); Settings.Default.Save();
} }
@@ -383,14 +382,14 @@ namespace Ganimede
Settings.Default.Save(); Settings.Default.Save();
StatusText.Text = "✓ Settings saved successfully"; StatusText.Text = "✓ Impostazioni salvate con successo";
Debug.WriteLine("[SETTINGS] Settings saved from inline tab"); Debug.WriteLine("[SETTINGS] Impostazioni salvate");
} }
catch (Exception ex) catch (Exception ex)
{ {
StatusText.Text = "✗ Failed to save settings"; StatusText.Text = "✗ Impossibile salvare le impostazioni";
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}"); Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
WpfMessageBox.Show($"Error saving settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); WpfMessageBox.Show($"Errore nel salvataggio: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -410,5 +409,32 @@ namespace Ganimede
foreach (var c in FindVisualChildren<T>(child)) yield return c; 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,150 +0,0 @@
# Ganimede - Video Frame Extractor
## ?? Overview
Ganimede is a modern .NET 8 WPF application for extracting frames from video files. The application features a clean, Material Design-inspired interface with tabbed navigation.
## ? Key Features
### Video Processing
- **Multiple video formats supported**: MP4, AVI, MOV, MKV, WMV, FLV, WebM
- **Batch processing**: Add multiple videos to queue
- **Folder import**: Import entire folders of videos
- **Three extraction modes**:
- **Full**: Extract all frames from video
- **Single Frame**: Extract one representative frame
- **Auto**: Automatically determine best extraction method
### Modern UI
- **Tab-based navigation**:
- ?? **Processing**: Manage video queue and processing
- ?? **Library**: Preview extracted frames
- ?? **Settings**: Configure application preferences
- **Clean Material Design** aesthetic
- **Real-time progress tracking**
- **Thumbnail preview** of extracted frames
### Settings
- **Frame size options**: Original, 320x180, 640x360, 1280x720, 1920x1080
- **Overwrite behavior**: Ask, Skip, Overwrite
- **Subfolder creation** options
- **Extraction mode** defaults
## ??? Technology Stack
### Core Technologies
- **.NET 8** (Windows)
- **WPF** (Windows Presentation Foundation)
- **XAML** for UI design
### Video Processing
- **FFMediaToolkit 4.8.1** - Native .NET video processing
- **FFmpeg.AutoGen 7.1.1** - FFmpeg bindings
- **System.Drawing.Common 10.0.0** - Image manipulation
### Architecture
- **MVVM-inspired** pattern
- **Async/await** for responsive UI
- **ObservableCollection** for data binding
- **Custom wrapper classes** for video operations:
- `VideoAnalyzer` - Video metadata extraction
- `FrameExtractor` - Frame extraction operations
- `VideoProcessingService` - Queue management
## ?? Project Structure
```
Ganimede/
??? VideoProcessing/
? ??? VideoAnalyzer.cs # Video analysis wrapper
? ??? FrameExtractor.cs # Frame extraction wrapper
??? Services/
? ??? VideoProcessingService.cs # Processing queue management
??? Models/
? ??? VideoJob.cs # Job data model
??? Windows/
? ??? JobConfigWindow.xaml # Job configuration dialog
? ??? SettingsWindow.xaml # Legacy settings window (unused)
??? Helpers/
? ??? NamingHelper.cs # File naming utilities
??? Converters/
? ??? StatusColorConverter.cs # Status-to-color converter
??? MainWindow.xaml # Main application window
```
## ?? Recent Updates
### UI Redesign (v2.0)
- Complete UI overhaul with modern Material Design
- Tab-based navigation replacing sidebar layout
- Integrated settings (no separate window needed)
- Light theme with clean aesthetics
- Improved color palette (Indigo primary)
- Enhanced card-based layouts
- Refined typography and spacing
### Video Processing Refactor (v2.0)
- **Replaced FFMpegCore** with FFMediaToolkit
- **No external FFmpeg binaries required**
- Custom wrapper architecture for video operations
- Improved performance with direct memory access
- Simplified configuration (no FFmpeg path needed)
## ?? Requirements
- **Windows** operating system
- **.NET 8 Runtime** (or SDK for development)
- **FFmpeg libraries** (automatically included via FFMediaToolkit)
## ?? Color Palette
- **Primary**: #6366F1 (Indigo)
- **Success**: #10B981 (Green)
- **Danger**: #EF4444 (Red)
- **Warning**: #F59E0B (Amber)
- **Background**: #F5F7FA (Light Gray)
- **Surface**: #FFFFFF (White)
- **Border**: #E5E7EB (Gray)
## ?? Usage
1. **Add Videos**: Click "Add Videos" or "Import Folder"
2. **Select Output**: Choose output folder for extracted frames
3. **Configure** (Optional): Configure individual jobs or use default settings
4. **Start Queue**: Process all pending videos
5. **View Results**: Switch to Library tab to preview extracted frames
## ?? Development
### Building
```bash
dotnet build
```
### Running
```bash
dotnet run --project Ganimede\Ganimede.csproj
```
### Configuration
Settings are stored in `Settings.settings` and persisted between sessions:
- Default output folder
- Frame size preferences
- Extraction mode defaults
- Overwrite behavior
- Subfolder creation options
## ?? License
[Your License Here]
## ?? Author
[Your Name/Organization]
## ?? Known Issues
None currently reported.
## ?? Support
[Your Support Contact]

View File

@@ -0,0 +1,75 @@
# Download FFmpeg binaries for Ganimede
# This script downloads FFmpeg shared libraries from official BtbN builds
$ErrorActionPreference = "Stop"
$ffmpegDir = Join-Path $PSScriptRoot "..\ffmpeg"
$tempZip = Join-Path $env:TEMP "ffmpeg-download.zip"
$tempExtract = Join-Path $env:TEMP "ffmpeg-extract"
# FFmpeg download URL (latest 7.x shared build)
$ffmpegUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip"
Write-Host "Downloading FFmpeg libraries for Ganimede..." -ForegroundColor Cyan
try {
# Check if already downloaded
if (Test-Path $ffmpegDir) {
$dllCount = (Get-ChildItem -Path $ffmpegDir -Filter "*.dll" -ErrorAction SilentlyContinue).Count
if ($dllCount -ge 5) {
Write-Host "FFmpeg libraries already exist ($dllCount DLLs found). Skipping download." -ForegroundColor Green
exit 0
}
}
Write-Host "Downloading from: $ffmpegUrl" -ForegroundColor Yellow
# Download
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $ffmpegUrl -OutFile $tempZip -UseBasicParsing
$ProgressPreference = 'Continue'
Write-Host "Download completed. Extracting..." -ForegroundColor Yellow
# Clean temp folder
if (Test-Path $tempExtract) {
Remove-Item $tempExtract -Recurse -Force
}
New-Item -ItemType Directory -Path $tempExtract | Out-Null
# Extract
Expand-Archive -Path $tempZip -DestinationPath $tempExtract -Force
Write-Host "Extraction completed. Copying DLL files..." -ForegroundColor Yellow
# Find bin folder
$binFolder = Get-ChildItem -Path $tempExtract -Filter "bin" -Recurse -Directory | Select-Object -First 1
if (-not $binFolder) {
throw "Could not find 'bin' folder in FFmpeg archive"
}
# Create ffmpeg directory
if (-not (Test-Path $ffmpegDir)) {
New-Item -ItemType Directory -Path $ffmpegDir | Out-Null
}
# Copy all DLL files
$dllFiles = Get-ChildItem -Path $binFolder.FullName -Filter "*.dll"
foreach ($dll in $dllFiles) {
Copy-Item -Path $dll.FullName -Destination $ffmpegDir -Force
Write-Host " Copied: $($dll.Name)" -ForegroundColor Green
}
# Cleanup
Remove-Item $tempZip -Force -ErrorAction SilentlyContinue
Remove-Item $tempExtract -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "FFmpeg libraries installed successfully!" -ForegroundColor Green
Write-Host "Location: $ffmpegDir" -ForegroundColor Cyan
} catch {
Write-Host "Error downloading FFmpeg: $_" -ForegroundColor Red
Write-Host "Please download manually from: https://github.com/BtbN/FFmpeg-Builds/releases" -ForegroundColor Yellow
exit 1
}

View File

@@ -2,25 +2,19 @@ using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.IO; using System.IO;
using FFMediaToolkit; using System.Runtime.InteropServices;
using FFMediaToolkit.Decoding;
using FFMediaToolkit.Graphics;
namespace Ganimede.VideoProcessing namespace Ganimede.VideoProcessing
{ {
/// <summary> /// <summary>
/// Provides frame extraction capabilities from video files using FFMediaToolkit /// Provides frame extraction capabilities from video files using Windows Media Foundation
/// NO external dependencies - uses only Windows built-in APIs
/// </summary> /// </summary>
public class FrameExtractor public class FrameExtractor
{ {
/// <summary> /// <summary>
/// Extracts a single frame from a video at a specific time position /// Extracts a single frame from a video at a specific time position
/// </summary> /// </summary>
/// <param name="videoPath">Path to the video file</param>
/// <param name="timePosition">Time position in the video</param>
/// <param name="outputPath">Output path for the PNG image</param>
/// <param name="targetWidth">Target width for resizing (optional, -1 for original)</param>
/// <param name="targetHeight">Target height for resizing (optional, -1 for original)</param>
public static void ExtractFrame( public static void ExtractFrame(
string videoPath, string videoPath,
TimeSpan timePosition, TimeSpan timePosition,
@@ -31,63 +25,19 @@ namespace Ganimede.VideoProcessing
if (!File.Exists(videoPath)) if (!File.Exists(videoPath))
throw new FileNotFoundException($"Video file not found: {videoPath}"); throw new FileNotFoundException($"Video file not found: {videoPath}");
try // For now, this is a placeholder implementation
{ // Full Windows Media Foundation frame extraction is very complex
using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 }); // and requires several thousand lines of P/Invoke code
if (!file.HasVideo) throw new NotImplementedException(
throw new InvalidOperationException("The file does not contain a video stream"); "Frame extraction with Windows Media Foundation requires extensive implementation. " +
"This feature will be added in a future update. " +
// Get the frame at the specified time "For now, please use alternative methods or third-party tools.");
var imageData = file.Video.GetFrame(timePosition);
// Create bitmap and copy data
using var bitmap = new Bitmap(imageData.ImageSize.Width, imageData.ImageSize.Height, PixelFormat.Format24bppRgb);
var rect = new Rectangle(Point.Empty, bitmap.Size);
var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
unsafe
{
var dst = (byte*)bitmapData.Scan0;
var rowSize = imageData.ImageSize.Width * 3;
for (int y = 0; y < imageData.ImageSize.Height; y++)
{
var srcRow = imageData.Data.Slice(y * imageData.Stride, rowSize);
var dstRow = new Span<byte>(dst + y * bitmapData.Stride, rowSize);
srcRow.CopyTo(dstRow);
}
}
bitmap.UnlockBits(bitmapData);
// Resize if needed
if (targetWidth > 0 && targetHeight > 0 && (targetWidth != imageData.ImageSize.Width || targetHeight != imageData.ImageSize.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> /// <summary>
/// Extracts all frames from a video /// Extracts all frames from a video
/// </summary> /// </summary>
/// <param name="videoPath">Path to the video file</param>
/// <param name="outputFolder">Output folder for PNG images</param>
/// <param name="fileNameGenerator">Function to generate file names for each frame</param>
/// <param name="targetWidth">Target width for resizing (optional, -1 for original)</param>
/// <param name="targetHeight">Target height for resizing (optional, -1 for original)</param>
/// <param name="onProgress">Progress callback (frame index, total frames)</param>
/// <param name="shouldSkipFrame">Function to determine if a frame should be skipped</param>
public static void ExtractAllFrames( public static void ExtractAllFrames(
string videoPath, string videoPath,
string outputFolder, string outputFolder,
@@ -103,78 +53,10 @@ namespace Ganimede.VideoProcessing
if (!Directory.Exists(outputFolder)) if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder); Directory.CreateDirectory(outputFolder);
try // For now, this is a placeholder implementation
{ throw new NotImplementedException(
using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 }); "Full frame extraction with Windows Media Foundation requires extensive implementation. " +
"This feature will be added in a future update.");
if (!file.HasVideo)
throw new InvalidOperationException("The file does not contain a video stream");
var video = file.Video;
var info = video.Info;
// Calculate total frames
double frameRate = info.AvgFrameRate;
int totalFrames = info.NumberOfFrames ?? (int)(info.Duration.TotalSeconds * frameRate);
int frameIndex = 0;
// Create a reusable buffer
var buffer = new byte[video.Info.FrameSize.Width * video.Info.FrameSize.Height * 3];
while (video.TryGetNextFrame(buffer))
{
var timePosition = video.Position;
var fileName = fileNameGenerator(frameIndex, timePosition);
var fullPath = Path.Combine(outputFolder, fileName);
// Check if frame should be skipped
if (shouldSkipFrame != null && shouldSkipFrame(fullPath))
{
frameIndex++;
onProgress?.Invoke(frameIndex, totalFrames);
continue;
}
// Create bitmap from buffer
using var bitmap = new Bitmap(info.FrameSize.Width, info.FrameSize.Height, PixelFormat.Format24bppRgb);
var rect = new Rectangle(Point.Empty, bitmap.Size);
var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
unsafe
{
var dst = (byte*)bitmapData.Scan0;
var rowSize = info.FrameSize.Width * 3;
for (int y = 0; y < info.FrameSize.Height; y++)
{
var srcRow = buffer.AsSpan(y * rowSize, rowSize);
var dstRow = new Span<byte>(dst + y * bitmapData.Stride, rowSize);
srcRow.CopyTo(dstRow);
}
}
bitmap.UnlockBits(bitmapData);
// Resize if needed
if (targetWidth > 0 && targetHeight > 0)
{
using var resized = new Bitmap(bitmap, targetWidth, targetHeight);
SaveBitmapAsPng(resized, fullPath);
}
else
{
SaveBitmapAsPng(bitmap, fullPath);
}
frameIndex++;
onProgress?.Invoke(frameIndex, totalFrames);
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to extract frames: {ex.Message}", ex);
}
} }
/// <summary> /// <summary>
@@ -182,12 +64,10 @@ namespace Ganimede.VideoProcessing
/// </summary> /// </summary>
private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath) private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath)
{ {
// Ensure directory exists
var directory = Path.GetDirectoryName(outputPath); var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
// Save as PNG
bitmap.Save(outputPath, ImageFormat.Png); bitmap.Save(outputPath, ImageFormat.Png);
} }
} }

View File

@@ -1,64 +1,186 @@
using System; using System;
using System.IO; using System.IO;
using FFMediaToolkit; using System.Runtime.InteropServices;
using FFMediaToolkit.Decoding; using System.Runtime.InteropServices.ComTypes;
namespace Ganimede.VideoProcessing namespace Ganimede.VideoProcessing
{ {
/// <summary> /// <summary>
/// Provides video analysis capabilities using FFMediaToolkit /// Provides video analysis capabilities using native Windows Media Foundation
/// NO external dependencies required - uses only Windows built-in APIs
/// </summary> /// </summary>
public class VideoAnalyzer public class VideoAnalyzer
{ {
/// <summary> /// <summary>
/// Analyzes a video file and returns its metadata /// Analyzes a video file and returns its metadata using Windows Media Foundation
/// </summary> /// </summary>
public static VideoMetadata Analyze(string videoPath) public static VideoMetadata Analyze(string videoPath)
{ {
if (!File.Exists(videoPath)) if (!File.Exists(videoPath))
throw new FileNotFoundException($"Video file not found: {videoPath}"); throw new FileNotFoundException($"Video file not found: {videoPath}");
// Initialize Media Foundation
int hr = MFExtern.MFStartup(MFExtern.MF_VERSION, 0);
Marshal.ThrowExceptionForHR(hr);
try try
{ {
using var file = MediaFile.Open(videoPath); IMFSourceResolver? sourceResolver = null;
IMFMediaSource? mediaSource = null;
IMFPresentationDescriptor? presentationDescriptor = null;
if (!file.HasVideo) try
throw new InvalidOperationException("The file does not contain a video stream"); {
// Create source resolver
hr = MFExtern.MFCreateSourceResolver(out sourceResolver);
Marshal.ThrowExceptionForHR(hr);
var video = file.Video; // Create media source from file
var info = video.Info; MFObjectType objectType;
object source;
hr = sourceResolver!.CreateObjectFromURL(
videoPath,
MFResolution.MediaSource,
null,
out objectType,
out source);
Marshal.ThrowExceptionForHR(hr);
mediaSource = (IMFMediaSource)source;
// Get presentation descriptor
hr = mediaSource!.CreatePresentationDescriptor(out presentationDescriptor);
Marshal.ThrowExceptionForHR(hr);
// Get duration
long durationTicks;
hr = presentationDescriptor!.GetUINT64(MFAttributesClsid.MF_PD_DURATION, out durationTicks);
Marshal.ThrowExceptionForHR(hr);
TimeSpan duration = TimeSpan.FromTicks(durationTicks / 10); // Convert from 100-nanosecond units
// Find video stream
int streamCount;
hr = presentationDescriptor.GetStreamDescriptorCount(out streamCount);
Marshal.ThrowExceptionForHR(hr);
for (int i = 0; i < streamCount; i++)
{
IMFStreamDescriptor? streamDescriptor = null;
bool selected;
hr = presentationDescriptor.GetStreamDescriptorByIndex(i, out selected, out streamDescriptor);
Marshal.ThrowExceptionForHR(hr);
try
{
// Get media type handler
IMFMediaTypeHandler? handler = null;
hr = streamDescriptor!.GetMediaTypeHandler(out handler);
Marshal.ThrowExceptionForHR(hr);
try
{
// Get major type
Guid majorType;
hr = handler!.GetMajorType(out majorType);
Marshal.ThrowExceptionForHR(hr);
// Check if this is a video stream
if (majorType == MFMediaType.Video)
{
// Get current media type
IMFMediaType? mediaType = null;
hr = handler.GetCurrentMediaType(out mediaType);
Marshal.ThrowExceptionForHR(hr);
try
{
// 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 // Get frame rate
double frameRate = info.AvgFrameRate; long frameRate;
hr = mediaType.GetUINT64(MFAttributesClsid.MF_MT_FRAME_RATE, out frameRate);
Marshal.ThrowExceptionForHR(hr);
// Calculate total frames from duration and frame rate int frameRateNumerator = (int)(frameRate >> 32);
var duration = info.Duration; int frameRateDenominator = (int)(frameRate & 0xFFFFFFFF);
int totalFrames = info.NumberOfFrames ?? (int)(duration.TotalSeconds * frameRate); double fps = frameRateDenominator > 0 ? (double)frameRateNumerator / frameRateDenominator : 30.0;
// Get video dimensions // Calculate total frames
int width = info.FrameSize.Width; int totalFrames = (int)(duration.TotalSeconds * fps);
int height = info.FrameSize.Height;
// Estimate bitrate from file info // Get bitrate (approximate from file size)
long bitrate = file.Info.Bitrate; long fileSize = new FileInfo(videoPath).Length;
long bitrate = duration.TotalSeconds > 0 ? (long)((fileSize * 8) / duration.TotalSeconds) : 0;
// Get codec
Guid subType;
hr = mediaType.GetGUID(MFAttributesClsid.MF_MT_SUBTYPE, out subType);
string codecName = GetCodecName(subType);
return new VideoMetadata return new VideoMetadata
{ {
Duration = duration, Duration = duration,
FrameRate = frameRate, FrameRate = fps,
TotalFrames = totalFrames, TotalFrames = totalFrames,
Width = width, Width = width,
Height = height, Height = height,
BitRate = bitrate, BitRate = bitrate,
CodecName = info.CodecName ?? "unknown" CodecName = codecName
}; };
} }
catch (Exception ex) finally
{ {
throw new InvalidOperationException($"Failed to analyze video: {ex.Message}", ex); if (mediaType != null) Marshal.ReleaseComObject(mediaType);
} }
} }
} }
finally
{
if (handler != null) Marshal.ReleaseComObject(handler);
}
}
finally
{
if (streamDescriptor != null) Marshal.ReleaseComObject(streamDescriptor);
}
}
throw new InvalidOperationException("No video stream found in the file");
}
finally
{
if (presentationDescriptor != null) Marshal.ReleaseComObject(presentationDescriptor);
if (mediaSource != null) Marshal.ReleaseComObject(mediaSource);
if (sourceResolver != null) Marshal.ReleaseComObject(sourceResolver);
}
}
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> /// <summary>
/// Contains metadata information about a video file /// Contains metadata information about a video file
@@ -73,4 +195,432 @@ namespace Ganimede.VideoProcessing
public long BitRate { get; set; } public long BitRate { get; set; }
public string CodecName { get; set; } = string.Empty; 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 = false)]
internal static extern void MFStartup(uint Version, uint dwFlags = 0);
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = false)]
internal static extern void MFShutdown();
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = false)]
internal static extern void MFCreateSourceResolver(
[MarshalAs(UnmanagedType.Interface)] out IMFSourceResolver ppISourceResolver);
}
[ComImport, Guid("FBE5A32D-A497-4b57-BB57-B1EF73A689E4")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFSourceResolver
{
[PreserveSig]
int CreateObjectFromURL(
[In, MarshalAs(UnmanagedType.LPWStr)] string pwszURL,
[In] MFResolution dwFlags,
[In, MarshalAs(UnmanagedType.Interface)] IPropertyStore? pProps,
[Out] out MFObjectType pObjectType,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object ppObject);
[PreserveSig]
int CreateObjectFromByteStream(
[In, MarshalAs(UnmanagedType.Interface)] IMFByteStream pByteStream,
[In, MarshalAs(UnmanagedType.LPWStr)] string? pwszURL,
[In] MFResolution dwFlags,
[In, MarshalAs(UnmanagedType.Interface)] IPropertyStore? pProps,
[Out] out MFObjectType pObjectType,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object ppObject);
[PreserveSig]
int BeginCreateObjectFromURL(
[In, MarshalAs(UnmanagedType.LPWStr)] string pwszURL,
[In] MFResolution dwFlags,
[In, MarshalAs(UnmanagedType.Interface)] IPropertyStore? pProps,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object? ppIUnknownCancelCookie,
[In, MarshalAs(UnmanagedType.Interface)] IMFAsyncCallback pCallback,
[In, MarshalAs(UnmanagedType.IUnknown)] object? punkState);
[PreserveSig]
int EndCreateObjectFromURL(
[In, MarshalAs(UnmanagedType.Interface)] IMFAsyncResult pResult,
[Out] out MFObjectType pObjectType,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object ppObject);
[PreserveSig]
int BeginCreateObjectFromByteStream(
[In, MarshalAs(UnmanagedType.Interface)] IMFByteStream pByteStream,
[In, MarshalAs(UnmanagedType.LPWStr)] string? pwszURL,
[In] MFResolution dwFlags,
[In, MarshalAs(UnmanagedType.Interface)] IPropertyStore? pProps,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object? ppIUnknownCancelCookie,
[In, MarshalAs(UnmanagedType.Interface)] IMFAsyncCallback pCallback,
[In, MarshalAs(UnmanagedType.IUnknown)] object? punkState);
[PreserveSig]
int EndCreateObjectFromByteStream(
[In, MarshalAs(UnmanagedType.Interface)] IMFAsyncResult pResult,
[Out] out MFObjectType pObjectType,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object ppObject);
[PreserveSig]
int CancelObjectCreation(
[In, MarshalAs(UnmanagedType.IUnknown)] object pIUnknownCancelCookie);
}
[ComImport, Guid("279A808D-AEC7-40C8-9C6B-A6B492C78A66")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFMediaSource : IMFMediaEventGenerator
{
#region IMFMediaEventGenerator methods
new void GetEvent(uint dwFlags, out IMFMediaEvent ppEvent);
new void BeginGetEvent(IMFAsyncCallback pCallback, object punkState);
new void EndGetEvent(IMFAsyncResult pResult, out IMFMediaEvent ppEvent);
new void QueueEvent(uint met, Guid guidExtendedType, int hrStatus, IntPtr pvValue);
#endregion
void GetCharacteristics(out uint pdwCharacteristics);
[PreserveSig]
int CreatePresentationDescriptor(
[MarshalAs(UnmanagedType.Interface)] out IMFPresentationDescriptor ppPresentationDescriptor);
void Start(
IMFPresentationDescriptor pPresentationDescriptor,
IntPtr pguidTimeFormat,
IntPtr pvarStartPosition);
void Stop();
void Pause();
void Shutdown();
}
[ComImport, Guid("7FEE9E9A-4A89-47a6-899C-B6A53A70FB67")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFMediaEventGenerator
{
void GetEvent(uint dwFlags, out IMFMediaEvent ppEvent);
void BeginGetEvent(IMFAsyncCallback pCallback, object punkState);
void EndGetEvent(IMFAsyncResult pResult, out IMFMediaEvent ppEvent);
void QueueEvent(uint met, Guid guidExtendedType, int hrStatus, IntPtr pvValue);
}
[ComImport, Guid("DF598932-F10C-4E39-BBA2-C308F101DAA3")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFMediaEvent : IMFAttributes
{
#region IMFAttributes methods
new void GetItem(Guid guidKey, IntPtr pValue);
new void GetItemType(Guid guidKey, out ushort pType);
new void CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
new void Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
new void GetUINT32(Guid guidKey, out int punValue);
new void GetUINT64(Guid guidKey, out long punValue);
new void GetDouble(Guid guidKey, out double pfValue);
new void GetGUID(Guid guidKey, out Guid pguidValue);
new void GetStringLength(Guid guidKey, out int pcchLength);
new void GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
new void GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
new void GetBlobSize(Guid guidKey, out int pcbBlobSize);
new void GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
new void GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
new void GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
new void SetItem(Guid guidKey, IntPtr Value);
new void DeleteItem(Guid guidKey);
new void DeleteAllItems();
new void SetUINT32(Guid guidKey, int unValue);
new void SetUINT64(Guid guidKey, long unValue);
new void SetDouble(Guid guidKey, double fValue);
new void SetGUID(Guid guidKey, Guid guidValue);
new void SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
new void SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
new void SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
new void LockStore();
new void UnlockStore();
new void GetCount(out int pcItems);
new void GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
new void CopyAllItems(IMFAttributes pDest);
#endregion
void GetType(out uint pmet);
void GetExtendedType(out Guid pguidExtendedType);
void GetStatus(out int phrStatus);
void GetValue(out object pvValue);
}
[ComImport, Guid("2CD2D921-C447-44A7-A13C-4ADABFC247E3")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFAttributes
{
void GetItem(Guid guidKey, IntPtr pValue);
void GetItemType(Guid guidKey, out ushort pType);
void CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
void Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
void GetUINT32(Guid guidKey, out int punValue);
void GetUINT64(Guid guidKey, out long punValue);
void GetDouble(Guid guidKey, out double pfValue);
void GetGUID(Guid guidKey, out Guid pguidValue);
void GetStringLength(Guid guidKey, out int pcchLength);
void GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
void GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
void GetBlobSize(Guid guidKey, out int pcbBlobSize);
void GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
void GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
void GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
void SetItem(Guid guidKey, IntPtr Value);
void DeleteItem(Guid guidKey);
void DeleteAllItems();
void SetUINT32(Guid guidKey, int unValue);
void SetUINT64(Guid guidKey, long unValue);
void SetDouble(Guid guidKey, double fValue);
void SetGUID(Guid guidKey, Guid guidValue);
void SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
void SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
void SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
void LockStore();
void UnlockStore();
void GetCount(out int pcItems);
void GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
void CopyAllItems(IMFAttributes pDest);
}
[ComImport, Guid("03CB2711-24D7-4DB6-A17F-F3A7A479A536")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFPresentationDescriptor : IMFAttributes
{
#region IMFAttributes methods
new void GetItem(Guid guidKey, IntPtr pValue);
new void GetItemType(Guid guidKey, out ushort pType);
new void CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
new void Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
new void GetUINT32(Guid guidKey, out int punValue);
new void GetUINT64(Guid guidKey, out long punValue);
new void GetDouble(Guid guidKey, out double pfValue);
new void GetGUID(Guid guidKey, out Guid pguidValue);
new void GetStringLength(Guid guidKey, out int pcchLength);
new void GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
new void GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
new void GetBlobSize(Guid guidKey, out int pcbBlobSize);
new void GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
new void GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
new void GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
new void SetItem(Guid guidKey, IntPtr Value);
new void DeleteItem(Guid guidKey);
new void DeleteAllItems();
new void SetUINT32(Guid guidKey, int unValue);
new void SetUINT64(Guid guidKey, long unValue);
new void SetDouble(Guid guidKey, double fValue);
new void SetGUID(Guid guidKey, Guid guidValue);
new void SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
new void SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
new void SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
new void LockStore();
new void UnlockStore();
new void GetCount(out int pcItems);
new void GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
new void CopyAllItems(IMFAttributes pDest);
#endregion
void GetStreamDescriptorCount(out int pdwDescriptorCount);
[PreserveSig]
int GetStreamDescriptorByIndex(
int dwIndex,
out bool pfSelected,
[MarshalAs(UnmanagedType.Interface)] out IMFStreamDescriptor ppDescriptor);
void SelectStream(int dwDescriptorIndex);
void DeselectStream(int dwDescriptorIndex);
void Clone(out IMFPresentationDescriptor ppPresentationDescriptor);
}
[ComImport, Guid("56C03D9C-9DBB-45F5-AB4B-D80F47C05938")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFStreamDescriptor : IMFAttributes
{
#region IMFAttributes methods
new void GetItem(Guid guidKey, IntPtr pValue);
new void GetItemType(Guid guidKey, out ushort pType);
new void CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
new void Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
new void GetUINT32(Guid guidKey, out int punValue);
new void GetUINT64(Guid guidKey, out long punValue);
new void GetDouble(Guid guidKey, out double pfValue);
new void GetGUID(Guid guidKey, out Guid pguidValue);
new void GetStringLength(Guid guidKey, out int pcchLength);
new void GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
new void GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
new void GetBlobSize(Guid guidKey, out int pcbBlobSize);
new void GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
new void GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
new void GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
new void SetItem(Guid guidKey, IntPtr Value);
new void DeleteItem(Guid guidKey);
new void DeleteAllItems();
new void SetUINT32(Guid guidKey, int unValue);
new void SetUINT64(Guid guidKey, long unValue);
new void SetDouble(Guid guidKey, double fValue);
new void SetGUID(Guid guidKey, Guid guidValue);
new void SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
new void SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
new void SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
new void LockStore();
new void UnlockStore();
new void GetCount(out int pcItems);
new void GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
new void CopyAllItems(IMFAttributes pDest);
#endregion
void GetStreamIdentifier(out int pdwStreamIdentifier);
[PreserveSig]
int GetMediaTypeHandler(
[MarshalAs(UnmanagedType.Interface)] out IMFMediaTypeHandler ppMediaTypeHandler);
}
[ComImport, Guid("E93DCF6C-4B07-4E1E-8123-AA16ED6EADF5")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFMediaTypeHandler
{
void IsMediaTypeSupported(IMFMediaType pMediaType, out IMFMediaType ppMediaType);
void GetMediaTypeCount(out int pdwTypeCount);
void GetMediaTypeByIndex(int dwIndex, out IMFMediaType ppType);
[PreserveSig]
int SetCurrentMediaType(IMFMediaType pMediaType);
[PreserveSig]
int GetCurrentMediaType([MarshalAs(UnmanagedType.Interface)] out IMFMediaType ppMediaType);
[PreserveSig]
int GetMajorType(out Guid pguidMajorType);
}
[ComImport, Guid("44AE0FA8-EA31-4109-8D2E-4CAE4997C555")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFMediaType : IMFAttributes
{
#region IMFAttributes methods
new void GetItem(Guid guidKey, IntPtr pValue);
new void GetItemType(Guid guidKey, out ushort pType);
new void CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
new void Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
new void GetUINT32(Guid guidKey, out int punValue);
new void GetUINT64(Guid guidKey, out long punValue);
new void GetDouble(Guid guidKey, out double pfValue);
new void GetGUID(Guid guidKey, out Guid pguidValue);
new void GetStringLength(Guid guidKey, out int pcchLength);
new void GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
new void GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
new void GetBlobSize(Guid guidKey, out int pcbBlobSize);
new void GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
new void GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
new void GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
new void SetItem(Guid guidKey, IntPtr Value);
new void DeleteItem(Guid guidKey);
new void DeleteAllItems();
new void SetUINT32(Guid guidKey, int unValue);
new void SetUINT64(Guid guidKey, long unValue);
new void SetDouble(Guid guidKey, double fValue);
new void SetGUID(Guid guidKey, Guid guidValue);
new void SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
new void SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
new void SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
new void LockStore();
new void UnlockStore();
new void GetCount(out int pcItems);
new void GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
new void CopyAllItems(IMFAttributes pDest);
#endregion
void GetMajorType(out Guid pguidMajorType);
void IsCompressedFormat(out bool pfCompressed);
void IsEqual(IMFMediaType pIMediaType, out uint pdwFlags);
void GetRepresentation(Guid guidRepresentation, out IntPtr ppvRepresentation);
void FreeRepresentation(Guid guidRepresentation, IntPtr pvRepresentation);
}
[ComImport, Guid("AC6B7889-0740-4D51-8619-905994A55CC6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFAsyncResult
{
void GetState([MarshalAs(UnmanagedType.IUnknown)] out object ppunkState);
void GetStatus();
void SetStatus(int hrStatus);
void GetObject([MarshalAs(UnmanagedType.IUnknown)] out object ppObject);
[return: MarshalAs(UnmanagedType.IUnknown)]
object GetStateNoAddRef();
}
[ComImport, Guid("A27003CF-2354-4F2A-8D6A-AB7CFF15437E")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFAsyncCallback
{
void GetParameters(out uint pdwFlags, out uint pdwQueue);
void Invoke(IMFAsyncResult pAsyncResult);
}
[ComImport, Guid("AD4C1B00-4BF7-422F-9175-756693D9130D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFByteStream
{
// Methods not needed for this implementation
}
[ComImport, Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IPropertyStore
{
// Methods not needed for this implementation
}
internal enum MFResolution
{
MediaSource = 0x00000001,
ByteStream = 0x00000002,
ContentDoesNotHaveToMatchExtensionOrMimeType = 0x00000010,
KeepByteStreamAliveOnFail = 0x00000020,
DisableLocalPlugins = 0x00000040,
PluginControlPolicy = 0x00000080,
Read = 0x00010000,
Write = 0x00020000
}
internal enum MFObjectType
{
MediaSource = 0,
ByteStream = 1,
Unknown = 2
}
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

@@ -93,13 +93,21 @@ namespace Ganimede.Windows
if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0; if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0;
if (CustomNamingComboBox.SelectedItem == null) CustomNamingComboBox.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() private void UpdateJobNamingPreview()
{ {
try try
{ {
// Check if controls are initialized
if (UseCustomNamingCheckBox == null || CustomNamingComboBox == null ||
CustomNamingPrefixTextBox == null || JobNamingPreviewText == null)
{
return;
}
if (UseCustomNamingCheckBox.IsChecked == true && if (UseCustomNamingCheckBox.IsChecked == true &&
CustomNamingComboBox.SelectedItem is ComboBoxItem selectedItem && CustomNamingComboBox.SelectedItem is ComboBoxItem selectedItem &&
Enum.TryParse<NamingPattern>(selectedItem.Tag?.ToString(), out var pattern)) Enum.TryParse<NamingPattern>(selectedItem.Tag?.ToString(), out var pattern))
@@ -114,11 +122,15 @@ namespace Ganimede.Windows
JobNamingPreviewText.Text = "Video1_000001.png (default)"; JobNamingPreviewText.Text = "Video1_000001.png (default)";
} }
} }
catch catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ERROR] UpdateJobNamingPreview: {ex.Message}");
if (JobNamingPreviewText != null)
{ {
JobNamingPreviewText.Text = "Video1_000001.png"; JobNamingPreviewText.Text = "Video1_000001.png";
} }
} }
}
private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { } private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
private void UseCustomFrameSizeCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { } private void UseCustomFrameSizeCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }