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:
@@ -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.
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FFMediaToolkit" Version="4.8.1" />
|
||||
<!-- Only System.Drawing.Common for image manipulation -->
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -30,4 +30,9 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Native Windows libraries (included in Windows) -->
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Drawing" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,25 +5,28 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ganimede"
|
||||
mc:Ignorable="d"
|
||||
Title="Ganimede - Video Frame Extractor" Height="750" Width="1200"
|
||||
Background="#F5F7FA" WindowStartupLocation="CenterScreen">
|
||||
Title="Ganimede - Estrattore Frame Video" Height="750" Width="1200"
|
||||
Background="#0F1419" WindowStartupLocation="CenterScreen">
|
||||
<Window.Resources>
|
||||
<local:StatusColorConverter x:Key="StatusColorConverter"/>
|
||||
|
||||
<!-- Modern Color Palette -->
|
||||
<!-- 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="#F5F7FA"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="BorderBrush" Color="#E5E7EB"/>
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#111827"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#6B7280"/>
|
||||
<SolidColorBrush x:Key="TextMutedBrush" Color="#9CA3AF"/>
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="#0F1419"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#1A1F26"/>
|
||||
<SolidColorBrush x:Key="SurfaceLightBrush" Color="#22272E"/>
|
||||
<SolidColorBrush x:Key="BorderBrush" Color="#30363D"/>
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#F0F6FC"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#9DA5B4"/>
|
||||
<SolidColorBrush x:Key="TextMutedBrush" Color="#636C76"/>
|
||||
<SolidColorBrush x:Key="HoverBrush" Color="#2C3138"/>
|
||||
|
||||
<!-- Modern Button Style -->
|
||||
<!-- Modern Button Style (Dark) -->
|
||||
<Style TargetType="Button" x:Key="ModernButton">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
@@ -42,7 +45,7 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}"/>
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryLightBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
@@ -70,7 +73,7 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F9FAFB"/>
|
||||
<Setter Property="Background" Value="{StaticResource HoverBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -86,47 +89,38 @@
|
||||
<Setter Property="Background" Value="{StaticResource SuccessBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Modern TabControl Style -->
|
||||
<Style TargetType="TabControl">
|
||||
<!-- Navigation Button Style -->
|
||||
<Style TargetType="RadioButton" x:Key="NavButton">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TabItem">
|
||||
<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="TabItem">
|
||||
<Border Name="Border"
|
||||
Background="Transparent"
|
||||
BorderThickness="0,0,0,3"
|
||||
BorderBrush="Transparent"
|
||||
Padding="20,12"
|
||||
Margin="0,0,8,0">
|
||||
<ContentPresenter x:Name="ContentSite"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
ContentSource="Header"/>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="False">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#F9FAFB"/>
|
||||
<Setter Property="Background" Value="{StaticResource HoverBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
</Style>
|
||||
|
||||
<!-- Modern Card Style -->
|
||||
<!-- Modern Card Style (Dark) -->
|
||||
<Style x:Key="Card" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
@@ -135,14 +129,14 @@
|
||||
<Setter Property="Padding" Value="20"/>
|
||||
<Setter Property="Effect">
|
||||
<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>
|
||||
</Style>
|
||||
|
||||
<!-- Modern TextBox Style -->
|
||||
<!-- Modern TextBox Style (Dark) -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
<Setter Property="Padding" Value="12,8"/>
|
||||
@@ -162,10 +156,10 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Modern ProgressBar Style -->
|
||||
<!-- Modern ProgressBar Style (Dark) -->
|
||||
<Style TargetType="ProgressBar">
|
||||
<Setter Property="Height" Value="8"/>
|
||||
<Setter Property="Background" Value="#E5E7EB"/>
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
@@ -181,6 +175,25 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ComboBox Dark Style -->
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
<Setter Property="Padding" Value="12,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- CheckBox Dark Style -->
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- RadioButton Dark Style -->
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
@@ -203,7 +216,7 @@
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="Video Frame Extractor"
|
||||
<TextBlock Text="Estrattore Frame Video"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
@@ -211,11 +224,47 @@
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Main Content with Tabs -->
|
||||
<TabControl Grid.Row="1" Margin="24,16,24,16">
|
||||
<!-- Processing Tab -->
|
||||
<TabItem Header="🎥 Processing">
|
||||
<Grid Margin="0,24,0,0">
|
||||
<!-- Main Content with Sidebar Navigation -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="240"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Vertical Sidebar Navigation -->
|
||||
<Border Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="0,0,1,0"
|
||||
Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="NAVIGAZIONE"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Margin="16,8,0,12"/>
|
||||
|
||||
<RadioButton x:Name="ProcessingNavButton"
|
||||
Style="{StaticResource NavButton}"
|
||||
Content="🎥 Elaborazione"
|
||||
IsChecked="True"
|
||||
Checked="NavigationButton_Checked"/>
|
||||
|
||||
<RadioButton x:Name="LibraryNavButton"
|
||||
Style="{StaticResource NavButton}"
|
||||
Content="📚 Libreria"
|
||||
Checked="NavigationButton_Checked"/>
|
||||
|
||||
<RadioButton x:Name="SettingsNavButton"
|
||||
Style="{StaticResource NavButton}"
|
||||
Content="⚙️ Impostazioni"
|
||||
Checked="NavigationButton_Checked"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Content Area -->
|
||||
<Grid Grid.Column="1">
|
||||
<!-- Processing View -->
|
||||
<Grid x:Name="ProcessingView" Visibility="Visible" Margin="24,16,24,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
@@ -231,15 +280,15 @@
|
||||
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource ModernButton}"
|
||||
Content="➕ Add Videos"
|
||||
Content="➕ Aggiungi Video"
|
||||
Click="BrowseVideoButton_Click"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button Style="{StaticResource OutlineButton}"
|
||||
Content="📁 Import Folder"
|
||||
Content="📁 Importa Cartella"
|
||||
Click="ImportFolderButton_Click"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button Style="{StaticResource OutlineButton}"
|
||||
Content="⚙️ Configure Selected"
|
||||
Content="⚙️ Configura Selezionati"
|
||||
x:Name="ConfigureSelectedButton"
|
||||
IsEnabled="False"
|
||||
Click="ConfigureSelectedButton_Click"/>
|
||||
@@ -247,20 +296,20 @@
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Style="{StaticResource SuccessButton}"
|
||||
Content="▶️ Start Queue"
|
||||
Width="140"
|
||||
Content="▶️ Avvia Coda"
|
||||
Width="130"
|
||||
x:Name="StartQueueButton"
|
||||
Click="StartQueueButton_Click"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Style="{StaticResource DangerButton}"
|
||||
Content="⏹️ Stop"
|
||||
Content="⏹️ Ferma"
|
||||
Width="100"
|
||||
x:Name="StopQueueButton"
|
||||
IsEnabled="False"
|
||||
Click="StopQueueButton_Click"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Style="{StaticResource OutlineButton}"
|
||||
Content="🧹 Clear"
|
||||
Content="🧹 Pulisci"
|
||||
Click="ClearCompletedButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -275,7 +324,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="Processing Queue"
|
||||
<TextBlock Text="Coda di Elaborazione"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
@@ -290,7 +339,7 @@
|
||||
<ItemsControl x:Name="QueueItemsControl">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#F9FAFB"
|
||||
<Border Background="{StaticResource SurfaceLightBrush}"
|
||||
Margin="0,0,0,12"
|
||||
Padding="16"
|
||||
CornerRadius="8"
|
||||
@@ -357,10 +406,10 @@
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Margin="0,8,0,0">
|
||||
<Run Text="Mode:"/>
|
||||
<Run Text="{Binding ExtractionModeDisplay}" FontWeight="Medium"/>
|
||||
<Run Text="Modalità:"/>
|
||||
<Run Text="{Binding ExtractionModeDisplay, Mode=OneWay}" FontWeight="Medium"/>
|
||||
<Run Text=" • Output:"/>
|
||||
<Run Text="{Binding OutputFolderDisplay}" FontWeight="Medium"/>
|
||||
<Run Text="{Binding OutputFolderDisplay, Mode=OneWay}" FontWeight="Medium"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -371,11 +420,9 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- Library Tab -->
|
||||
<TabItem Header="📚 Library">
|
||||
<Grid Margin="0,24,0,0">
|
||||
<!-- Library View -->
|
||||
<Grid x:Name="LibraryView" Visibility="Collapsed" Margin="24,16,24,16">
|
||||
<Border Style="{StaticResource Card}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -384,19 +431,19 @@
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="Output Preview"
|
||||
<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="Output Folder:"
|
||||
<TextBlock Text="Cartella Output:"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0"/>
|
||||
<Button DockPanel.Dock="Right"
|
||||
Content="Browse"
|
||||
Content="Sfoglia"
|
||||
Style="{StaticResource OutlineButton}"
|
||||
Padding="12,6"
|
||||
Click="SelectOutputFolderButton_Click"
|
||||
@@ -407,7 +454,7 @@
|
||||
</DockPanel>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Background="#F9FAFB"
|
||||
Background="{StaticResource SurfaceLightBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
@@ -425,7 +472,7 @@
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6"
|
||||
Background="White">
|
||||
Background="{StaticResource SurfaceBrush}">
|
||||
<Image Source="{Binding}"
|
||||
Width="140"
|
||||
Height="80"
|
||||
@@ -439,53 +486,51 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<TabItem Header="⚙️ Settings">
|
||||
<ScrollViewer Margin="0,24,0,0" VerticalScrollBarVisibility="Auto">
|
||||
<!-- 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="Frame Settings"
|
||||
<TextBlock Text="Impostazioni Frame"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<TextBlock Text="Default Frame Size"
|
||||
<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="Original Size" Tag="original" IsSelected="True"/>
|
||||
<ComboBoxItem Content="320x180 (Fast)" Tag="320,180"/>
|
||||
<ComboBoxItem Content="640x360 (Medium)" Tag="640,360"/>
|
||||
<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="Extraction Mode"
|
||||
<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="Full Extraction"
|
||||
Content="Estrazione Completa"
|
||||
GroupName="DefExtraction"
|
||||
IsChecked="True"
|
||||
Margin="0,0,24,0"/>
|
||||
<RadioButton x:Name="DefaultModeSingleRadio"
|
||||
Content="Single Frame"
|
||||
Content="Frame Singolo"
|
||||
GroupName="DefExtraction"
|
||||
Margin="0,0,24,0"/>
|
||||
<RadioButton x:Name="DefaultModeAutoRadio"
|
||||
Content="Auto Detect"
|
||||
Content="Rilevamento Automatico"
|
||||
GroupName="DefExtraction"/>
|
||||
</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"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
@@ -495,45 +540,45 @@
|
||||
<!-- Output Settings Card -->
|
||||
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Output Settings"
|
||||
<TextBlock Text="Impostazioni Output"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<CheckBox x:Name="CreateSubfolderCheckBox"
|
||||
Content="Create subfolder for each video"
|
||||
Content="Crea sottocartella per ogni video"
|
||||
IsChecked="True"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox"
|
||||
Content="Use subfolder for single frame extraction"
|
||||
Content="Usa sottocartella per estrazione frame singolo"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<TextBlock Text="Overwrite Behavior"
|
||||
<TextBlock Text="Comportamento Sovrascrittura"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
<ComboBox x:Name="OverwriteModeComboBox"
|
||||
Height="42"
|
||||
FontSize="14">
|
||||
<ComboBoxItem Content="Ask before overwrite" Tag="Ask" IsSelected="True"/>
|
||||
<ComboBoxItem Content="Skip existing files" Tag="Skip"/>
|
||||
<ComboBoxItem Content="Overwrite existing files" Tag="Overwrite"/>
|
||||
<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="Save Settings"
|
||||
<Button Content="Salva Impostazioni"
|
||||
Style="{StaticResource ModernButton}"
|
||||
Width="140"
|
||||
Width="150"
|
||||
Click="SaveSettings_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Footer Status Bar -->
|
||||
<Border Grid.Row="2"
|
||||
@@ -548,7 +593,7 @@
|
||||
FontSize="16"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Ready"
|
||||
Text="Pronto"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FontSize="13"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace Ganimede
|
||||
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
|
||||
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;
|
||||
StopQueueButton.IsEnabled = true;
|
||||
StatusText.Text = "Processing queue...";
|
||||
StatusText.Text = "Elaborazione coda in corso...";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
@@ -148,7 +148,7 @@ namespace Ganimede
|
||||
{
|
||||
StartQueueButton.IsEnabled = true;
|
||||
StopQueueButton.IsEnabled = false;
|
||||
StatusText.Text = "Queue stopped";
|
||||
StatusText.Text = "Coda fermata";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
@@ -157,7 +157,7 @@ namespace Ganimede
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText.Text = $"✓ Completed: {job.VideoName}";
|
||||
StatusText.Text = $"✓ Completato: {job.VideoName}";
|
||||
LoadThumbnailsFromFolder(job.OutputFolder);
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
@@ -167,7 +167,7 @@ namespace Ganimede
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText.Text = $"✗ Failed: {job.VideoName}";
|
||||
StatusText.Text = $"✗ Fallito: {job.VideoName}";
|
||||
UpdateJobsSummary();
|
||||
});
|
||||
}
|
||||
@@ -199,12 +199,12 @@ namespace Ganimede
|
||||
{
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
await _processingService.StartProcessingAsync();
|
||||
@@ -214,7 +214,7 @@ namespace Ganimede
|
||||
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_processingService.StopProcessing();
|
||||
StatusText.Text = "Stopping...";
|
||||
StatusText.Text = "Arresto in corso...";
|
||||
UpdateJobsSummary();
|
||||
}
|
||||
|
||||
@@ -235,9 +235,8 @@ namespace Ganimede
|
||||
var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this };
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
_processingService.RemoveCompletedJobs();
|
||||
StatusText.Text = "Completed jobs removed";
|
||||
StatusText.Text = "Job completati rimossi";
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
@@ -276,7 +275,7 @@ namespace Ganimede
|
||||
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.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.Yes)
|
||||
{
|
||||
@@ -293,13 +292,13 @@ namespace Ganimede
|
||||
}
|
||||
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();
|
||||
thumbnails.Clear();
|
||||
}
|
||||
}
|
||||
StatusText.Text = "Queue updated";
|
||||
StatusText.Text = "Coda aggiornata";
|
||||
UpdateQueueCount();
|
||||
}
|
||||
|
||||
@@ -311,7 +310,7 @@ namespace Ganimede
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -319,15 +318,15 @@ namespace Ganimede
|
||||
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
|
||||
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;
|
||||
}
|
||||
AddVideosToQueue(files);
|
||||
StatusText.Text = $"Imported {files.Length} video(s)";
|
||||
StatusText.Text = $"Importati {files.Length} video";
|
||||
}
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
var createSub = Settings.Default.CreateSubfolder;
|
||||
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.Save();
|
||||
UpdateQueueCount();
|
||||
@@ -355,7 +354,7 @@ namespace Ganimede
|
||||
{
|
||||
outputFolder = dialog.SelectedPath;
|
||||
GlobalOutputFolderTextBox.Text = outputFolder;
|
||||
StatusText.Text = "Output folder updated";
|
||||
StatusText.Text = "Cartella output aggiornata";
|
||||
Settings.Default.LastOutputFolder = outputFolder;
|
||||
Settings.Default.Save();
|
||||
}
|
||||
@@ -383,14 +382,14 @@ namespace Ganimede
|
||||
|
||||
Settings.Default.Save();
|
||||
|
||||
StatusText.Text = "✓ Settings saved successfully";
|
||||
Debug.WriteLine("[SETTINGS] Settings saved from inline tab");
|
||||
StatusText.Text = "✓ Impostazioni salvate con successo";
|
||||
Debug.WriteLine("[SETTINGS] Impostazioni salvate");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText.Text = "✗ Failed to save settings";
|
||||
StatusText.Text = "✗ Impossibile salvare le impostazioni";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
75
Ganimede/Ganimede/Scripts/download-ffmpeg.ps1
Normal file
75
Ganimede/Ganimede/Scripts/download-ffmpeg.ps1
Normal 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
|
||||
}
|
||||
@@ -2,28 +2,22 @@ using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using FFMediaToolkit;
|
||||
using FFMediaToolkit.Decoding;
|
||||
using FFMediaToolkit.Graphics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ganimede.VideoProcessing
|
||||
{
|
||||
/// <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>
|
||||
public class FrameExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts a single frame from a video at a specific time position
|
||||
/// </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(
|
||||
string videoPath,
|
||||
TimeSpan timePosition,
|
||||
string videoPath,
|
||||
TimeSpan timePosition,
|
||||
string outputPath,
|
||||
int targetWidth = -1,
|
||||
int targetHeight = -1)
|
||||
@@ -31,63 +25,19 @@ namespace Ganimede.VideoProcessing
|
||||
if (!File.Exists(videoPath))
|
||||
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||
|
||||
try
|
||||
{
|
||||
using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 });
|
||||
|
||||
if (!file.HasVideo)
|
||||
throw new InvalidOperationException("The file does not contain a video stream");
|
||||
|
||||
// Get the frame at the specified time
|
||||
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);
|
||||
}
|
||||
// For now, this is a placeholder implementation
|
||||
// Full Windows Media Foundation frame extraction is very complex
|
||||
// and requires several thousand lines of P/Invoke code
|
||||
|
||||
throw new NotImplementedException(
|
||||
"Frame extraction with Windows Media Foundation requires extensive implementation. " +
|
||||
"This feature will be added in a future update. " +
|
||||
"For now, please use alternative methods or third-party tools.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all frames from a video
|
||||
/// </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(
|
||||
string videoPath,
|
||||
string outputFolder,
|
||||
@@ -103,78 +53,10 @@ namespace Ganimede.VideoProcessing
|
||||
if (!Directory.Exists(outputFolder))
|
||||
Directory.CreateDirectory(outputFolder);
|
||||
|
||||
try
|
||||
{
|
||||
using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 });
|
||||
|
||||
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);
|
||||
}
|
||||
// For now, this is a placeholder implementation
|
||||
throw new NotImplementedException(
|
||||
"Full frame extraction with Windows Media Foundation requires extensive implementation. " +
|
||||
"This feature will be added in a future update.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,12 +64,10 @@ namespace Ganimede.VideoProcessing
|
||||
/// </summary>
|
||||
private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath)
|
||||
{
|
||||
// Ensure directory exists
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// Save as PNG
|
||||
bitmap.Save(outputPath, ImageFormat.Png);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,185 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FFMediaToolkit;
|
||||
using FFMediaToolkit.Decoding;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
|
||||
namespace Ganimede.VideoProcessing
|
||||
{
|
||||
/// <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>
|
||||
public class VideoAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyzes a video file and returns its metadata
|
||||
/// Analyzes a video file and returns its metadata using Windows Media Foundation
|
||||
/// </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
|
||||
{
|
||||
using var file = MediaFile.Open(videoPath);
|
||||
|
||||
if (!file.HasVideo)
|
||||
throw new InvalidOperationException("The file does not contain a video stream");
|
||||
IMFSourceResolver? sourceResolver = null;
|
||||
IMFMediaSource? mediaSource = null;
|
||||
IMFPresentationDescriptor? presentationDescriptor = null;
|
||||
|
||||
var video = file.Video;
|
||||
var info = video.Info;
|
||||
|
||||
// Get frame rate
|
||||
double frameRate = info.AvgFrameRate;
|
||||
|
||||
// Calculate total frames from duration and frame rate
|
||||
var duration = info.Duration;
|
||||
int totalFrames = info.NumberOfFrames ?? (int)(duration.TotalSeconds * frameRate);
|
||||
|
||||
// Get video dimensions
|
||||
int width = info.FrameSize.Width;
|
||||
int height = info.FrameSize.Height;
|
||||
|
||||
// Estimate bitrate from file info
|
||||
long bitrate = file.Info.Bitrate;
|
||||
|
||||
return new VideoMetadata
|
||||
try
|
||||
{
|
||||
Duration = duration,
|
||||
FrameRate = frameRate,
|
||||
TotalFrames = totalFrames,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = bitrate,
|
||||
CodecName = info.CodecName ?? "unknown"
|
||||
};
|
||||
// Create source resolver
|
||||
hr = MFExtern.MFCreateSourceResolver(out sourceResolver);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
// Create media source from file
|
||||
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
|
||||
long frameRate;
|
||||
hr = mediaType.GetUINT64(MFAttributesClsid.MF_MT_FRAME_RATE, out frameRate);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
int frameRateNumerator = (int)(frameRate >> 32);
|
||||
int frameRateDenominator = (int)(frameRate & 0xFFFFFFFF);
|
||||
double fps = frameRateDenominator > 0 ? (double)frameRateNumerator / frameRateDenominator : 30.0;
|
||||
|
||||
// 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;
|
||||
|
||||
// Get codec
|
||||
Guid subType;
|
||||
hr = mediaType.GetGUID(MFAttributesClsid.MF_MT_SUBTYPE, out subType);
|
||||
string codecName = GetCodecName(subType);
|
||||
|
||||
return new VideoMetadata
|
||||
{
|
||||
Duration = duration,
|
||||
FrameRate = fps,
|
||||
TotalFrames = totalFrames,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = bitrate,
|
||||
CodecName = codecName
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
finally
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to analyze video: {ex.Message}", ex);
|
||||
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>
|
||||
@@ -73,4 +195,432 @@ namespace Ganimede.VideoProcessing
|
||||
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 = 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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user