Migrazione a Windows Media Foundation

- Rimosse le dipendenze da FFmpeg e FFMediaToolkit.
- Implementata Windows Media Foundation per analisi video.
- Aggiunto tema scuro e navigazione laterale nell'interfaccia.
- Tradotti testi e notifiche dall'inglese all'italiano.
- Migliorata la gestione degli errori in JobConfigWindow.
- Aggiornato README per riflettere i cambiamenti.
- Eliminato lo script di download di FFmpeg.
This commit is contained in:
2025-12-08 01:09:57 +01:00
parent 11931854c7
commit 627a157762
12 changed files with 900 additions and 449 deletions

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@@ -127,7 +127,7 @@ namespace Ganimede
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
Dispatcher.Invoke(() =>
{
JobsSummaryText.Text = $"Pending: {pending} | Processing: {processing} | Completed: {completed} | Failed: {failed}";
JobsSummaryText.Text = $"In Attesa: {pending} | In Corso: {processing} | Completati: {completed} | Falliti: {failed}";
});
}
@@ -137,7 +137,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true;
StatusText.Text = "Processing queue...";
StatusText.Text = "Elaborazione coda in corso...";
UpdateJobsSummary();
});
}
@@ -148,7 +148,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = true;
StopQueueButton.IsEnabled = false;
StatusText.Text = "Queue stopped";
StatusText.Text = "Coda fermata";
UpdateJobsSummary();
});
}
@@ -157,7 +157,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✓ Completed: {job.VideoName}";
StatusText.Text = $"✓ Completato: {job.VideoName}";
LoadThumbnailsFromFolder(job.OutputFolder);
UpdateJobsSummary();
});
@@ -167,7 +167,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
StatusText.Text = $"✗ Failed: {job.VideoName}";
StatusText.Text = $"✗ Fallito: {job.VideoName}";
UpdateJobsSummary();
});
}
@@ -199,12 +199,12 @@ namespace Ganimede
{
if (_processingService.JobQueue.Count == 0)
{
WpfMessageBox.Show("No videos in queue.", "Empty Queue", MessageBoxButton.OK, MessageBoxImage.Information);
WpfMessageBox.Show("Nessun video in coda.", "Coda Vuota", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
{
WpfMessageBox.Show("No pending jobs in queue.", "No Jobs", MessageBoxButton.OK, MessageBoxImage.Information);
WpfMessageBox.Show("Nessun job in attesa nella coda.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
await _processingService.StartProcessingAsync();
@@ -214,7 +214,7 @@ namespace Ganimede
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
{
_processingService.StopProcessing();
StatusText.Text = "Stopping...";
StatusText.Text = "Arresto in corso...";
UpdateJobsSummary();
}
@@ -235,9 +235,8 @@ namespace Ganimede
var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this };
if (cfg.ShowDialog() == true)
{
StatusText.Text = $"Configuration applied to {_selectedJobs.Count} job(s)";
StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
// Update output folders only if outputFolder is set
if (!string.IsNullOrEmpty(outputFolder))
{
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
@@ -267,7 +266,7 @@ namespace Ganimede
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
{
_processingService.RemoveCompletedJobs();
StatusText.Text = "Completed jobs removed";
StatusText.Text = "Job completati rimossi";
UpdateQueueCount();
}
@@ -276,7 +275,7 @@ namespace Ganimede
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing);
if (processing)
{
var res = WpfMessageBox.Show("There are jobs being processed.\n\nYes: Stop and clear queue\nNo: Remove only non-processing jobs\nCancel: Cancel operation", "Confirm", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
var res = WpfMessageBox.Show("Ci sono job in elaborazione.\n\nSì: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla operazione", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
if (res == MessageBoxResult.Cancel) return;
if (res == MessageBoxResult.Yes)
{
@@ -293,13 +292,13 @@ namespace Ganimede
}
else
{
if (WpfMessageBox.Show("Remove all jobs from queue?", "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
if (WpfMessageBox.Show("Rimuovere tutti i job dalla coda?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
_processingService.JobQueue.Clear();
thumbnails.Clear();
}
}
StatusText.Text = "Queue updated";
StatusText.Text = "Coda aggiornata";
UpdateQueueCount();
}
@@ -311,7 +310,7 @@ namespace Ganimede
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Select folder containing videos", ShowNewFolderButton = false };
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona la cartella contenente i video", ShowNewFolderButton = false };
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
try
@@ -319,15 +318,15 @@ namespace Ganimede
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
if (files.Length == 0)
{
WpfMessageBox.Show("No valid video files found.", "Import Folder", MessageBoxButton.OK, MessageBoxImage.Information);
WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
AddVideosToQueue(files);
StatusText.Text = $"Imported {files.Length} video(s)";
StatusText.Text = $"Importati {files.Length} video";
}
catch (Exception ex)
{
WpfMessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
@@ -336,12 +335,12 @@ namespace Ganimede
{
if (string.IsNullOrEmpty(outputFolder))
{
WpfMessageBox.Show("Please select an output folder first.", "Output Folder Required", MessageBoxButton.OK, MessageBoxImage.Warning);
WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var createSub = Settings.Default.CreateSubfolder;
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
StatusText.Text = $"Added {paths.Length} video(s)";
StatusText.Text = $"Aggiunti {paths.Length} video";
Settings.Default.LastVideoPath = paths.FirstOrDefault();
Settings.Default.Save();
UpdateQueueCount();
@@ -355,7 +354,7 @@ namespace Ganimede
{
outputFolder = dialog.SelectedPath;
GlobalOutputFolderTextBox.Text = outputFolder;
StatusText.Text = "Output folder updated";
StatusText.Text = "Cartella output aggiornata";
Settings.Default.LastOutputFolder = outputFolder;
Settings.Default.Save();
}
@@ -383,14 +382,14 @@ namespace Ganimede
Settings.Default.Save();
StatusText.Text = "✓ Settings saved successfully";
Debug.WriteLine("[SETTINGS] Settings saved from inline tab");
StatusText.Text = "✓ Impostazioni salvate con successo";
Debug.WriteLine("[SETTINGS] Impostazioni salvate");
}
catch (Exception ex)
{
StatusText.Text = "✗ Failed to save settings";
StatusText.Text = "✗ Impossibile salvare le impostazioni";
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
WpfMessageBox.Show($"Error saving settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
WpfMessageBox.Show($"Errore nel salvataggio: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -410,5 +409,32 @@ namespace Ganimede
foreach (var c in FindVisualChildren<T>(child)) yield return c;
}
}
private void NavigationButton_Checked(object sender, RoutedEventArgs e)
{
if (sender is System.Windows.Controls.RadioButton rb)
{
// Find views by name
var processingView = FindName("ProcessingView") as Grid;
var libraryView = FindName("LibraryView") as Grid;
var settingsView = FindName("SettingsView") as ScrollViewer;
if (processingView == null || libraryView == null || settingsView == null)
return;
// Hide all views
processingView.Visibility = Visibility.Collapsed;
libraryView.Visibility = Visibility.Collapsed;
settingsView.Visibility = Visibility.Collapsed;
// Show selected view
if (rb.Name == "ProcessingNavButton")
processingView.Visibility = Visibility.Visible;
else if (rb.Name == "LibraryNavButton")
libraryView.Visibility = Visibility.Visible;
else if (rb.Name == "SettingsNavButton")
settingsView.Visibility = Visibility.Visible;
}
}
}
}

View File

@@ -1,150 +0,0 @@
# Ganimede - Video Frame Extractor
## ?? Overview
Ganimede is a modern .NET 8 WPF application for extracting frames from video files. The application features a clean, Material Design-inspired interface with tabbed navigation.
## ? Key Features
### Video Processing
- **Multiple video formats supported**: MP4, AVI, MOV, MKV, WMV, FLV, WebM
- **Batch processing**: Add multiple videos to queue
- **Folder import**: Import entire folders of videos
- **Three extraction modes**:
- **Full**: Extract all frames from video
- **Single Frame**: Extract one representative frame
- **Auto**: Automatically determine best extraction method
### Modern UI
- **Tab-based navigation**:
- ?? **Processing**: Manage video queue and processing
- ?? **Library**: Preview extracted frames
- ?? **Settings**: Configure application preferences
- **Clean Material Design** aesthetic
- **Real-time progress tracking**
- **Thumbnail preview** of extracted frames
### Settings
- **Frame size options**: Original, 320x180, 640x360, 1280x720, 1920x1080
- **Overwrite behavior**: Ask, Skip, Overwrite
- **Subfolder creation** options
- **Extraction mode** defaults
## ??? Technology Stack
### Core Technologies
- **.NET 8** (Windows)
- **WPF** (Windows Presentation Foundation)
- **XAML** for UI design
### Video Processing
- **FFMediaToolkit 4.8.1** - Native .NET video processing
- **FFmpeg.AutoGen 7.1.1** - FFmpeg bindings
- **System.Drawing.Common 10.0.0** - Image manipulation
### Architecture
- **MVVM-inspired** pattern
- **Async/await** for responsive UI
- **ObservableCollection** for data binding
- **Custom wrapper classes** for video operations:
- `VideoAnalyzer` - Video metadata extraction
- `FrameExtractor` - Frame extraction operations
- `VideoProcessingService` - Queue management
## ?? Project Structure
```
Ganimede/
??? VideoProcessing/
? ??? VideoAnalyzer.cs # Video analysis wrapper
? ??? FrameExtractor.cs # Frame extraction wrapper
??? Services/
? ??? VideoProcessingService.cs # Processing queue management
??? Models/
? ??? VideoJob.cs # Job data model
??? Windows/
? ??? JobConfigWindow.xaml # Job configuration dialog
? ??? SettingsWindow.xaml # Legacy settings window (unused)
??? Helpers/
? ??? NamingHelper.cs # File naming utilities
??? Converters/
? ??? StatusColorConverter.cs # Status-to-color converter
??? MainWindow.xaml # Main application window
```
## ?? Recent Updates
### UI Redesign (v2.0)
- Complete UI overhaul with modern Material Design
- Tab-based navigation replacing sidebar layout
- Integrated settings (no separate window needed)
- Light theme with clean aesthetics
- Improved color palette (Indigo primary)
- Enhanced card-based layouts
- Refined typography and spacing
### Video Processing Refactor (v2.0)
- **Replaced FFMpegCore** with FFMediaToolkit
- **No external FFmpeg binaries required**
- Custom wrapper architecture for video operations
- Improved performance with direct memory access
- Simplified configuration (no FFmpeg path needed)
## ?? Requirements
- **Windows** operating system
- **.NET 8 Runtime** (or SDK for development)
- **FFmpeg libraries** (automatically included via FFMediaToolkit)
## ?? Color Palette
- **Primary**: #6366F1 (Indigo)
- **Success**: #10B981 (Green)
- **Danger**: #EF4444 (Red)
- **Warning**: #F59E0B (Amber)
- **Background**: #F5F7FA (Light Gray)
- **Surface**: #FFFFFF (White)
- **Border**: #E5E7EB (Gray)
## ?? Usage
1. **Add Videos**: Click "Add Videos" or "Import Folder"
2. **Select Output**: Choose output folder for extracted frames
3. **Configure** (Optional): Configure individual jobs or use default settings
4. **Start Queue**: Process all pending videos
5. **View Results**: Switch to Library tab to preview extracted frames
## ?? Development
### Building
```bash
dotnet build
```
### Running
```bash
dotnet run --project Ganimede\Ganimede.csproj
```
### Configuration
Settings are stored in `Settings.settings` and persisted between sessions:
- Default output folder
- Frame size preferences
- Extraction mode defaults
- Overwrite behavior
- Subfolder creation options
## ?? License
[Your License Here]
## ?? Author
[Your Name/Organization]
## ?? Known Issues
None currently reported.
## ?? Support
[Your Support Contact]

View File

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

View File

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

View File

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

View File

@@ -93,13 +93,21 @@ namespace Ganimede.Windows
if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0;
if (CustomNamingComboBox.SelectedItem == null) CustomNamingComboBox.SelectedIndex = 0;
UpdateJobNamingPreview();
// Defer preview update until window is fully loaded
Dispatcher.InvokeAsync(() => UpdateJobNamingPreview(), System.Windows.Threading.DispatcherPriority.Loaded);
}
private void UpdateJobNamingPreview()
{
try
{
// Check if controls are initialized
if (UseCustomNamingCheckBox == null || CustomNamingComboBox == null ||
CustomNamingPrefixTextBox == null || JobNamingPreviewText == null)
{
return;
}
if (UseCustomNamingCheckBox.IsChecked == true &&
CustomNamingComboBox.SelectedItem is ComboBoxItem selectedItem &&
Enum.TryParse<NamingPattern>(selectedItem.Tag?.ToString(), out var pattern))
@@ -114,11 +122,15 @@ namespace Ganimede.Windows
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";
}
}
}
private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }
private void UseCustomFrameSizeCheckBox_CheckedChanged(object sender, RoutedEventArgs e) { }