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>
|
/// </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.
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) { }
|
||||||
|
|||||||
Reference in New Issue
Block a user