Compare commits
3 Commits
959fdad037
...
fdf540a69b
| Author | SHA1 | Date | |
|---|---|---|---|
| fdf540a69b | |||
| 627a157762 | |||
| 11931854c7 |
@@ -13,9 +13,6 @@
|
|||||||
<setting name="LastVideoPath" serializeAs="String">
|
<setting name="LastVideoPath" serializeAs="String">
|
||||||
<value />
|
<value />
|
||||||
</setting>
|
</setting>
|
||||||
<setting name="FFmpegBinFolder" serializeAs="String">
|
|
||||||
<value>C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg</value>
|
|
||||||
</setting>
|
|
||||||
</Ganimede.Properties.Settings>
|
</Ganimede.Properties.Settings>
|
||||||
</userSettings>
|
</userSettings>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -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.
@@ -7,10 +7,12 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
<!-- Only System.Drawing.Common for image manipulation -->
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,4 +30,6 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Native Windows libraries (included in Windows) -->
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,48 +5,50 @@
|
|||||||
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="Estrattore Frame Video" Height="800" Width="1250"
|
Title="Ganimede - Estrattore Frame Video" Height="750" Width="1200"
|
||||||
Background="#1E2228" WindowStartupLocation="CenterScreen">
|
Background="#0F1419" WindowStartupLocation="CenterScreen">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<local:StatusColorConverter x:Key="StatusColorConverter"/>
|
<local:StatusColorConverter x:Key="StatusColorConverter"/>
|
||||||
|
|
||||||
<!-- Color resources -->
|
<!-- Dark Mode Color Palette -->
|
||||||
<Color x:Key="AccentColor">#268BFF</Color>
|
<SolidColorBrush x:Key="PrimaryBrush" Color="#6366F1"/>
|
||||||
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
|
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="#4F46E5"/>
|
||||||
<SolidColorBrush x:Key="AccentBrushLight" Color="#39A3FF"/>
|
<SolidColorBrush x:Key="PrimaryLightBrush" Color="#818CF8"/>
|
||||||
<SolidColorBrush x:Key="BaseBrush" Color="#1E2228"/>
|
<SolidColorBrush x:Key="SuccessBrush" Color="#10B981"/>
|
||||||
<SolidColorBrush x:Key="PanelBrush" Color="#242A31"/>
|
<SolidColorBrush x:Key="DangerBrush" Color="#EF4444"/>
|
||||||
<SolidColorBrush x:Key="PanelSubBrush" Color="#2C333B"/>
|
<SolidColorBrush x:Key="WarningBrush" Color="#F59E0B"/>
|
||||||
<SolidColorBrush x:Key="BorderBrushColor" Color="#38424D"/>
|
<SolidColorBrush x:Key="BackgroundBrush" Color="#0F1419"/>
|
||||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#FFFFFF"/>
|
<SolidColorBrush x:Key="SurfaceBrush" Color="#1A1F26"/>
|
||||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#B5BDC7"/>
|
<SolidColorBrush x:Key="SurfaceLightBrush" Color="#22272E"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="#30363D"/>
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#F0F6FC"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#9DA5B4"/>
|
||||||
|
<SolidColorBrush x:Key="TextMutedBrush" Color="#636C76"/>
|
||||||
|
<SolidColorBrush x:Key="HoverBrush" Color="#2C3138"/>
|
||||||
|
|
||||||
<!-- Button Style -->
|
<!-- Modern Button Style (Dark) -->
|
||||||
<Style TargetType="Button" x:Key="ToolbarButton">
|
<Style TargetType="Button" x:Key="ModernButton">
|
||||||
|
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||||
<Setter Property="Foreground" Value="White"/>
|
<Setter Property="Foreground" Value="White"/>
|
||||||
<Setter Property="Background" Value="#2F3740"/>
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
<Setter Property="BorderBrush" Value="#3F4A55"/>
|
<Setter Property="Padding" Value="16,10"/>
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="Padding" Value="14 8"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
<Setter Property="FontSize" Value="14"/>
|
||||||
<Setter Property="MinHeight" Value="40"/>
|
<Setter Property="FontWeight" Value="Medium"/>
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="Button">
|
<ControlTemplate TargetType="Button">
|
||||||
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="8" SnapsToDevicePixels="True">
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter Property="Background" Value="#39444F"/>
|
<Setter Property="Background" Value="{StaticResource PrimaryLightBrush}"/>
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsPressed" Value="True">
|
|
||||||
<Setter Property="Background" Value="#46525E"/>
|
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter Property="Opacity" Value="0.4"/>
|
<Setter Property="Opacity" Value="0.5"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
@@ -54,59 +56,143 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Accent Button -->
|
<Style TargetType="Button" x:Key="OutlineButton" BasedOn="{StaticResource ModernButton}">
|
||||||
<Style TargetType="Button" x:Key="AccentButton" BasedOn="{StaticResource ToolbarButton}">
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
<Setter Property="BorderBrush" Value="#1673D5"/>
|
<Setter Property="BorderThickness" Value="1.5"/>
|
||||||
<Setter Property="Foreground" Value="White"/>
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Background" Value="{StaticResource AccentBrushLight}"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Danger Button -->
|
|
||||||
<Style TargetType="Button" x:Key="DangerButton" BasedOn="{StaticResource ToolbarButton}">
|
|
||||||
<Setter Property="Background" Value="#D9534F"/>
|
|
||||||
<Setter Property="BorderBrush" Value="#B33E3B"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Background" Value="#E36460"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Flat small badge button -->
|
|
||||||
<Style TargetType="Button" x:Key="SmallGhostButton" BasedOn="{StaticResource ToolbarButton}">
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="Padding" Value="12 6"/>
|
|
||||||
<Setter Property="MinHeight" Value="36"/>
|
|
||||||
<Setter Property="Background" Value="#2F3740"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- ProgressBar -->
|
|
||||||
<Style TargetType="ProgressBar">
|
|
||||||
<Setter Property="Height" Value="6"/>
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
|
|
||||||
<Setter Property="Background" Value="#313941"/>
|
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="ProgressBar">
|
<ControlTemplate TargetType="Button">
|
||||||
<Border Background="{TemplateBinding Background}" CornerRadius="3">
|
<Border Background="{TemplateBinding Background}"
|
||||||
<Grid x:Name="PART_Track">
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
<Rectangle x:Name="PART_Indicator" Fill="{TemplateBinding Foreground}" RadiusX="3" RadiusY="3"/>
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
</Grid>
|
CornerRadius="8"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="{StaticResource HoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Button" x:Key="DangerButton" BasedOn="{StaticResource ModernButton}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource DangerBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Button" x:Key="SuccessButton" BasedOn="{StaticResource ModernButton}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SuccessBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Navigation Button Style -->
|
||||||
|
<Style TargetType="RadioButton" x:Key="NavButton">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="15"/>
|
||||||
|
<Setter Property="FontWeight" Value="Medium"/>
|
||||||
|
<Setter Property="Padding" Value="16,12"/>
|
||||||
|
<Setter Property="Margin" Value="0,4,0,0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="{StaticResource HoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Modern Card Style (Dark) -->
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="12"/>
|
||||||
|
<Setter Property="Padding" Value="20"/>
|
||||||
|
<Setter Property="Effect">
|
||||||
|
<Setter.Value>
|
||||||
|
<DropShadowEffect Color="#000000" Opacity="0.3" BlurRadius="15" ShadowDepth="0"/>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Modern TextBox Style (Dark) -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1.5"/>
|
||||||
|
<Setter Property="Padding" Value="12,8"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
|
||||||
</Border>
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- ScrollViewer styling for thin scrollbar -->
|
<!-- Modern ProgressBar Style (Dark) -->
|
||||||
<Style TargetType="ScrollBar">
|
<Style TargetType="ProgressBar">
|
||||||
<Setter Property="Width" Value="10"/>
|
<Setter Property="Height" Value="8"/>
|
||||||
<Setter Property="Background" Value="#20252B"/>
|
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ProgressBar">
|
||||||
|
<Border Background="{TemplateBinding Background}" CornerRadius="4">
|
||||||
|
<Rectangle Name="PART_Indicator"
|
||||||
|
Fill="{TemplateBinding Foreground}"
|
||||||
|
RadiusX="4" RadiusY="4"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ComboBox Dark Style -->
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceLightBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1.5"/>
|
||||||
|
<Setter Property="Padding" Value="12,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- CheckBox Dark Style -->
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- RadioButton Dark Style -->
|
||||||
|
<Style TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
</Style>
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
@@ -117,132 +203,406 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- TOOLBAR -->
|
<!-- Header -->
|
||||||
<Border Background="#242A31" Padding="16 12" BorderBrush="#303840" BorderThickness="0,0,0,1">
|
<Border Background="{StaticResource SurfaceBrush}"
|
||||||
<DockPanel LastChildFill="False">
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
|
BorderThickness="0,0,0,1"
|
||||||
<Button x:Name="BrowseVideoButton" Style="{StaticResource AccentButton}" Content="➕ Aggiungi Video" Click="BrowseVideoButton_Click" Margin="0,0,10,0"/>
|
Padding="24,16">
|
||||||
<Button x:Name="ImportFolderButton" Style="{StaticResource ToolbarButton}" Content="📁 Importa Cartella" Click="ImportFolderButton_Click" Margin="0,0,10,0"/>
|
<DockPanel>
|
||||||
<Button x:Name="SelectOutputFolderButton" Style="{StaticResource ToolbarButton}" Content="🗂 Seleziona Cartella Output" Click="SelectOutputFolderButton_Click" Margin="0,0,10,0"/>
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
</StackPanel>
|
<TextBlock Text="🎬" FontSize="28" Margin="0,0,12,0"/>
|
||||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
|
<StackPanel>
|
||||||
<Button x:Name="ConfigureSelectedButton" Style="{StaticResource ToolbarButton}" Content="⚙ Configura Selezionati" Width="195" IsEnabled="False" Click="ConfigureSelectedButton_Click" Margin="0,0,10,0"/>
|
<TextBlock Text="Ganimede"
|
||||||
<Button x:Name="StartQueueButton" Style="{StaticResource AccentButton}" Content="▶ Avvia Coda" Width="150" Click="StartQueueButton_Click" Margin="0,0,10,0"/>
|
FontSize="20"
|
||||||
<Button x:Name="StopQueueButton" Style="{StaticResource DangerButton}" Content="⏹ Ferma" Width="110" IsEnabled="False" Click="StopQueueButton_Click" Margin="0,0,10,0"/>
|
FontWeight="Bold"
|
||||||
<Button x:Name="ClearCompletedButton" Style="{StaticResource SmallGhostButton}" Content="🧹 Pulisci Completati" Click="ClearCompletedButton_Click" Margin="0,0,10,0"/>
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
<Button x:Name="ClearAllButton" Style="{StaticResource SmallGhostButton}" Content="🗑 Pulisci Tutto" Click="ClearAllButton_Click" Margin="0,0,10,0"/>
|
<TextBlock Text="Estrattore Frame Video"
|
||||||
<Button x:Name="SettingsButton" Style="{StaticResource SmallGhostButton}" Content="⚙ Impostazioni" Click="SettingsButton_Click"/>
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- CONTENUTO PRINCIPALE -->
|
<!-- Main Content with Sidebar Navigation -->
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="240"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="370"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Coda -->
|
<!-- Vertical Sidebar Navigation -->
|
||||||
<Grid Margin="18 12 8 12">
|
<Border Background="{StaticResource SurfaceBrush}"
|
||||||
<Grid.RowDefinitions>
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
<RowDefinition Height="Auto"/>
|
BorderThickness="0,0,1,0"
|
||||||
<RowDefinition Height="*"/>
|
Padding="16">
|
||||||
</Grid.RowDefinitions>
|
<StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0 0 0 10" VerticalAlignment="Center">
|
<TextBlock Text="NAVIGAZIONE"
|
||||||
<TextBlock Text="Coda Job" FontSize="18" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
FontSize="11"
|
||||||
<TextBlock x:Name="QueueCountText" Text="(0)" Foreground="{StaticResource TextSecondaryBrush}" Margin="8,4,0,0"/>
|
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>
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="1" Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="4">
|
<!-- Content Area -->
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<Grid Grid.Column="1">
|
||||||
<ItemsControl x:Name="QueueItemsControl">
|
<!-- Processing View -->
|
||||||
<ItemsControl.ItemTemplate>
|
<Grid x:Name="ProcessingView" Visibility="Visible" Margin="24,16,24,16">
|
||||||
<DataTemplate>
|
<Grid.RowDefinitions>
|
||||||
<Border Background="{StaticResource PanelSubBrush}" Margin="6" Padding="10" CornerRadius="6" BorderBrush="#3A454F" BorderThickness="1">
|
<RowDefinition Height="Auto"/>
|
||||||
<Grid>
|
<RowDefinition Height="*"/>
|
||||||
<Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<CheckBox x:Name="JobCheckBox" Grid.Row="0" Grid.Column="0" Margin="0,0,10,0" VerticalAlignment="Center" Tag="{Binding}" Checked="JobCheckBox_CheckedChanged" Unchecked="JobCheckBox_CheckedChanged"/>
|
<!-- Actions Bar -->
|
||||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||||
<TextBlock Text="{Binding VideoName}" Foreground="White" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" Margin="0,0,8,0"/>
|
<Grid>
|
||||||
<TextBlock Text="{Binding Progress, StringFormat={}{0:0}%}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11"/>
|
<Grid.ColumnDefinitions>
|
||||||
</StackPanel>
|
<ColumnDefinition Width="*"/>
|
||||||
<Button Grid.Row="0" Grid.Column="2" Content="✕" Width="30" Height="26" Style="{StaticResource SmallGhostButton}" Tag="{Binding}" Click="RemoveQueueItem_Click" ToolTip="Rimuovi"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ProgressBar Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,0" Value="{Binding Progress}"/>
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding StatusMessage}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11" Margin="0,6,0,0" TextTrimming="CharacterEllipsis"/>
|
<Button Style="{StaticResource ModernButton}"
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" FontSize="10" Foreground="#87939F" Margin="0,8,0,0" TextWrapping="Wrap">
|
Content="➕ Aggiungi Video"
|
||||||
<TextBlock.Text>
|
Click="BrowseVideoButton_Click"
|
||||||
<MultiBinding StringFormat="{}📁 {0} 📐 {1} 🔄 {2} 🏷 {3} 🎯 {4}">
|
Margin="0,0,12,0"/>
|
||||||
<Binding Path="OutputFolderDisplay"/>
|
<Button Style="{StaticResource OutlineButton}"
|
||||||
<Binding Path="FrameSizeDisplay"/>
|
Content="📁 Importa Cartella"
|
||||||
<Binding Path="OverwriteModeDisplay"/>
|
Click="ImportFolderButton_Click"
|
||||||
<Binding Path="NamingPatternDisplay"/>
|
Margin="0,0,12,0"/>
|
||||||
<Binding Path="ExtractionModeDisplay"/>
|
<Button Style="{StaticResource OutlineButton}"
|
||||||
</MultiBinding>
|
Content="⚙️ Configura Selezionati"
|
||||||
</TextBlock.Text>
|
x:Name="ConfigureSelectedButton"
|
||||||
</TextBlock>
|
IsEnabled="False"
|
||||||
</Grid>
|
Click="ConfigureSelectedButton_Click"/>
|
||||||
</Border>
|
</StackPanel>
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Pannello destro -->
|
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||||
<StackPanel Grid.Column="1" Margin="8 12 18 12">
|
<Button Style="{StaticResource SuccessButton}"
|
||||||
<Border Background="{StaticResource PanelBrush}" BorderBrush="{StaticResource BorderBrushColor}" BorderThickness="1" CornerRadius="8" Padding="14">
|
Content="▶️ Avvia Coda"
|
||||||
<StackPanel>
|
Width="130"
|
||||||
<TextBlock Text="Impostazioni Globali" FontSize="16" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
x:Name="StartQueueButton"
|
||||||
<TextBlock Text="Cartella Output" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" Margin="0,10,0,2"/>
|
Click="StartQueueButton_Click"
|
||||||
<DockPanel LastChildFill="True">
|
Margin="0,0,8,0"/>
|
||||||
<TextBox x:Name="GlobalOutputFolderTextBox" Height="34" Margin="0,0,10,0" IsReadOnly="True" Background="#2C333B" BorderBrush="#3A434C" Foreground="White" BorderThickness="1"/>
|
<Button Style="{StaticResource DangerButton}"
|
||||||
<Button Content="Sfoglia" Width="80" Style="{StaticResource SmallGhostButton}" Click="SelectOutputFolderButton_Click"/>
|
Content="⏹️ Ferma"
|
||||||
</DockPanel>
|
Width="100"
|
||||||
<TextBlock Text="Anteprime (Thumbnails)" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" Margin="0,14,0,4"/>
|
x:Name="StopQueueButton"
|
||||||
<Border Background="{StaticResource PanelSubBrush}" BorderBrush="#3A454F" BorderThickness="1" CornerRadius="6" Padding="6" Height="260">
|
IsEnabled="False"
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
Click="StopQueueButton_Click"
|
||||||
<ItemsControl x:Name="ThumbnailsPanel">
|
Margin="0,0,8,0"/>
|
||||||
<ItemsControl.ItemsPanel>
|
<Button Style="{StaticResource OutlineButton}"
|
||||||
<ItemsPanelTemplate>
|
Content="🧹 Pulisci"
|
||||||
<WrapPanel IsItemsHost="True"/>
|
Click="ClearCompletedButton_Click"/>
|
||||||
</ItemsPanelTemplate>
|
</StackPanel>
|
||||||
</ItemsControl.ItemsPanel>
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Queue List -->
|
||||||
|
<Border Grid.Row="1" Style="{StaticResource Card}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<DockPanel Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="Coda di Elaborazione"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<TextBlock x:Name="QueueCountText"
|
||||||
|
Text="(0)"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource TextMutedBrush}"
|
||||||
|
Margin="8,2,0,0"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsControl x:Name="QueueItemsControl">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border Margin="4" BorderBrush="#3D4853" BorderThickness="1" CornerRadius="4">
|
<Border Background="{StaticResource SurfaceLightBrush}"
|
||||||
<Image Source="{Binding}" Width="90" Height="52" Stretch="UniformToFill"/>
|
Margin="0,0,0,12"
|
||||||
|
Padding="16"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<CheckBox x:Name="JobCheckBox"
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Tag="{Binding}"
|
||||||
|
Checked="JobCheckBox_CheckedChanged"
|
||||||
|
Unchecked="JobCheckBox_CheckedChanged"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding VideoName}"
|
||||||
|
FontWeight="Medium"
|
||||||
|
FontSize="15"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<TextBlock Text="{Binding StatusMessage}"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
Text="{Binding Progress, StringFormat={}{0:0}%}"
|
||||||
|
FontSize="15"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryBrush}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,16,0"/>
|
||||||
|
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Content="✕"
|
||||||
|
Width="32"
|
||||||
|
Height="32"
|
||||||
|
FontSize="16"
|
||||||
|
Style="{StaticResource OutlineButton}"
|
||||||
|
Padding="0"
|
||||||
|
Tag="{Binding}"
|
||||||
|
Click="RemoveQueueItem_Click"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ProgressBar Grid.Row="1"
|
||||||
|
Value="{Binding Progress}"
|
||||||
|
Margin="0,12,0,0"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource TextMutedBrush}"
|
||||||
|
Margin="0,8,0,0">
|
||||||
|
<Run Text="Modalità:"/>
|
||||||
|
<Run Text="{Binding ExtractionModeDisplay, Mode=OneWay}" FontWeight="Medium"/>
|
||||||
|
<Run Text=" • Output:"/>
|
||||||
|
<Run Text="{Binding OutputFolderDisplay, Mode=OneWay}" FontWeight="Medium"/>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Library View -->
|
||||||
|
<Grid x:Name="LibraryView" Visibility="Collapsed" Margin="24,16,24,16">
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="Anteprima Output"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<DockPanel Grid.Row="1" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="Cartella Output:"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,12,0"/>
|
||||||
|
<Button DockPanel.Dock="Right"
|
||||||
|
Content="Sfoglia"
|
||||||
|
Style="{StaticResource OutlineButton}"
|
||||||
|
Padding="12,6"
|
||||||
|
Click="SelectOutputFolderButton_Click"
|
||||||
|
Margin="12,0,0,0"/>
|
||||||
|
<TextBox x:Name="GlobalOutputFolderTextBox"
|
||||||
|
IsReadOnly="True"
|
||||||
|
VerticalContentAlignment="Center"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Background="{StaticResource SurfaceLightBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="12">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsControl x:Name="ThumbnailsPanel">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Margin="6"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="6"
|
||||||
|
Background="{StaticResource SurfaceBrush}">
|
||||||
|
<Image Source="{Binding}"
|
||||||
|
Width="140"
|
||||||
|
Height="80"
|
||||||
|
Stretch="UniformToFill"/>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Settings View -->
|
||||||
|
<ScrollViewer x:Name="SettingsView" Visibility="Collapsed" Margin="24,16,24,16" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel MaxWidth="800">
|
||||||
|
<!-- Frame Settings Card -->
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Impostazioni Frame"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Dimensione Frame Predefinita"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
<ComboBox x:Name="FrameSizeComboBox"
|
||||||
|
Height="42"
|
||||||
|
FontSize="14"
|
||||||
|
Margin="0,0,0,16">
|
||||||
|
<ComboBoxItem Content="Dimensione Originale" Tag="original" IsSelected="True"/>
|
||||||
|
<ComboBoxItem Content="320x180 (Veloce)" Tag="320,180"/>
|
||||||
|
<ComboBoxItem Content="640x360 (Medio)" Tag="640,360"/>
|
||||||
|
<ComboBoxItem Content="1280x720 (HD)" Tag="1280,720"/>
|
||||||
|
<ComboBoxItem Content="1920x1080 (Full HD)" Tag="1920,1080"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Text="Modalità di Estrazione"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
|
<RadioButton x:Name="DefaultModeFullRadio"
|
||||||
|
Content="Estrazione Completa"
|
||||||
|
GroupName="DefExtraction"
|
||||||
|
IsChecked="True"
|
||||||
|
Margin="0,0,24,0"/>
|
||||||
|
<RadioButton x:Name="DefaultModeSingleRadio"
|
||||||
|
Content="Frame Singolo"
|
||||||
|
GroupName="DefExtraction"
|
||||||
|
Margin="0,0,24,0"/>
|
||||||
|
<RadioButton x:Name="DefaultModeAutoRadio"
|
||||||
|
Content="Rilevamento Automatico"
|
||||||
|
GroupName="DefExtraction"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="La modalità automatica analizza il video e decide il metodo di estrazione migliore."
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource TextMutedBrush}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Output Settings Card -->
|
||||||
|
<Border Style="{StaticResource Card}" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Impostazioni Output"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="CreateSubfolderCheckBox"
|
||||||
|
Content="Crea sottocartella per ogni video"
|
||||||
|
IsChecked="True"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="SingleFrameUseSubfolderCheckBox"
|
||||||
|
Content="Usa sottocartella per estrazione frame singolo"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Comportamento Sovrascrittura"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
<ComboBox x:Name="OverwriteModeComboBox"
|
||||||
|
Height="42"
|
||||||
|
FontSize="14">
|
||||||
|
<ComboBoxItem Content="Chiedi prima di sovrascrivere" Tag="Ask" IsSelected="True"/>
|
||||||
|
<ComboBoxItem Content="Salta file esistenti" Tag="Skip"/>
|
||||||
|
<ComboBoxItem Content="Sovrascrivi file esistenti" Tag="Overwrite"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="Salva Impostazioni"
|
||||||
|
Style="{StaticResource ModernButton}"
|
||||||
|
Width="150"
|
||||||
|
Click="SaveSettings_Click"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</ScrollViewer>
|
||||||
</StackPanel>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- BARRA STATO -->
|
<!-- Footer Status Bar -->
|
||||||
<Border Grid.Row="2" Background="#242A31" BorderBrush="#303840" BorderThickness="1,1,1,0" Padding="16 6" CornerRadius="6 6 0 0" Margin="14 0 14 14">
|
<Border Grid.Row="2"
|
||||||
|
Background="{StaticResource SurfaceBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
Padding="24,12">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock x:Name="StatusText" Foreground="{StaticResource TextSecondaryBrush}" FontSize="13" VerticalAlignment="Center" Text="Pronto"/>
|
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
|
||||||
<TextBlock Text=" | " Foreground="#55606B" Margin="6,0"/>
|
<TextBlock Text="●"
|
||||||
<TextBlock Text="Job:" Foreground="#77818B" Margin="0,0,4,0"/>
|
Foreground="{StaticResource SuccessBrush}"
|
||||||
<TextBlock x:Name="JobsSummaryText" Foreground="#4F5962" FontSize="11" VerticalAlignment="Center"/>
|
FontSize="16"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<TextBlock x:Name="StatusText"
|
||||||
|
Text="Pronto"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock x:Name="JobsSummaryText"
|
||||||
|
DockPanel.Dock="Right"
|
||||||
|
Foreground="{StaticResource TextMutedBrush}"
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalAlignment="Right"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using System.Windows.Media.Imaging;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FFMpegCore;
|
|
||||||
using Ganimede.Properties;
|
using Ganimede.Properties;
|
||||||
using Ganimede.Services;
|
using Ganimede.Services;
|
||||||
using Ganimede.Models;
|
using Ganimede.Models;
|
||||||
@@ -30,7 +29,6 @@ namespace Ganimede
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
InitializeUI();
|
InitializeUI();
|
||||||
ConfigureFFMpeg();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeUI()
|
private void InitializeUI()
|
||||||
@@ -51,9 +49,67 @@ namespace Ganimede
|
|||||||
_processingService.ProcessingStopped += OnProcessingStopped;
|
_processingService.ProcessingStopped += OnProcessingStopped;
|
||||||
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
|
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
|
||||||
|
|
||||||
|
// Initialize settings controls
|
||||||
|
LoadSettingsControls();
|
||||||
|
|
||||||
UpdateQueueCount();
|
UpdateQueueCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadSettingsControls()
|
||||||
|
{
|
||||||
|
// Controls are loaded after InitializeComponent, so we can't access them in constructor
|
||||||
|
// We'll load settings when the Settings tab is first accessed instead
|
||||||
|
Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load frame size
|
||||||
|
var frameSize = Settings.Default.FrameSize;
|
||||||
|
foreach (ComboBoxItem item in FrameSizeComboBox.Items)
|
||||||
|
{
|
||||||
|
if (item.Tag?.ToString() == frameSize)
|
||||||
|
{
|
||||||
|
FrameSizeComboBox.SelectedItem = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load overwrite mode
|
||||||
|
var overwriteMode = Settings.Default.DefaultOverwriteMode;
|
||||||
|
foreach (ComboBoxItem item in OverwriteModeComboBox.Items)
|
||||||
|
{
|
||||||
|
if (item.Tag?.ToString() == overwriteMode)
|
||||||
|
{
|
||||||
|
OverwriteModeComboBox.SelectedItem = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load extraction mode
|
||||||
|
switch (Settings.Default.DefaultExtractionMode)
|
||||||
|
{
|
||||||
|
case "SingleFrame":
|
||||||
|
DefaultModeSingleRadio.IsChecked = true;
|
||||||
|
break;
|
||||||
|
case "Auto":
|
||||||
|
DefaultModeAutoRadio.IsChecked = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
DefaultModeFullRadio.IsChecked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load folder settings
|
||||||
|
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
|
||||||
|
SingleFrameUseSubfolderCheckBox.IsChecked = Settings.Default.SingleFrameUseSubfolder;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[ERROR] Failed to load settings controls: {ex.Message}");
|
||||||
|
}
|
||||||
|
}, System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateQueueCount()
|
private void UpdateQueueCount()
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
@@ -71,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 = $"In attesa: {pending} | In corso: {processing} | Completati: {completed} | Falliti: {failed}";
|
JobsSummaryText.Text = $"In Attesa: {pending} | In Corso: {processing} | Completati: {completed} | Falliti: {failed}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +137,7 @@ namespace Ganimede
|
|||||||
{
|
{
|
||||||
StartQueueButton.IsEnabled = false;
|
StartQueueButton.IsEnabled = false;
|
||||||
StopQueueButton.IsEnabled = true;
|
StopQueueButton.IsEnabled = true;
|
||||||
StatusText.Text = "Elaborazione coda...";
|
StatusText.Text = "Elaborazione coda in corso...";
|
||||||
UpdateJobsSummary();
|
UpdateJobsSummary();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -139,112 +195,6 @@ namespace Ganimede
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureFFMpeg()
|
|
||||||
{
|
|
||||||
var ffmpegBin = Settings.Default.FFmpegBinFolder;
|
|
||||||
if (!string.IsNullOrEmpty(ffmpegBin) && ValidateFFMpegBinaries(ffmpegBin))
|
|
||||||
FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
|
|
||||||
else if (TryUseSystemFFMpeg()) { }
|
|
||||||
else if (TryFixMissingFFMpeg(ffmpegBin))
|
|
||||||
FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateFFMpegBinaries(string binFolder) =>
|
|
||||||
Directory.Exists(binFolder) &&
|
|
||||||
File.Exists(Path.Combine(binFolder, "ffmpeg.exe")) &&
|
|
||||||
File.Exists(Path.Combine(binFolder, "ffprobe.exe"));
|
|
||||||
|
|
||||||
private bool TryUseSystemFFMpeg()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var psi = new ProcessStartInfo { FileName = "ffmpeg", Arguments = "-version", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
|
|
||||||
using var p = Process.Start(psi);
|
|
||||||
return p != null && p.WaitForExit(4000) && p.ExitCode == 0;
|
|
||||||
}
|
|
||||||
catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryFixMissingFFMpeg(string binFolder)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(binFolder) || !Directory.Exists(binFolder)) return false;
|
|
||||||
var ffmpegPath = Path.Combine(binFolder, "ffmpeg.exe");
|
|
||||||
var ffprobePath = Path.Combine(binFolder, "ffprobe.exe");
|
|
||||||
if (!File.Exists(ffmpegPath) && File.Exists(ffprobePath))
|
|
||||||
{
|
|
||||||
try { File.Copy(ffprobePath, ffmpegPath, true); return File.Exists(ffmpegPath); } catch { return false; }
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var dialog = new WpfOpenFileDialog { Filter = "Video (*.mp4;*.avi;*.mov;*.mkv;*.wmv)|*.mp4;*.avi;*.mov;*.mkv;*.wmv|Tutti i file (*.*)|*.*", Multiselect = true };
|
|
||||||
if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona la cartella con i video", ShowNewFolderButton = false };
|
|
||||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
|
|
||||||
if (files.Length == 0)
|
|
||||||
{
|
|
||||||
WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AddVideosToQueue(files);
|
|
||||||
StatusText.Text = $"Importati {files.Length} video.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddVideosToQueue(string[] paths)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(outputFolder))
|
|
||||||
{
|
|
||||||
WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var createSub = Settings.Default.CreateSubfolder;
|
|
||||||
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
|
|
||||||
StatusText.Text = $"Aggiunti {paths.Length} video (In attesa)";
|
|
||||||
Settings.Default.LastVideoPath = paths.FirstOrDefault();
|
|
||||||
Settings.Default.Save();
|
|
||||||
UpdateQueueCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
|
||||||
if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
|
|
||||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
||||||
{
|
|
||||||
outputFolder = dialog.SelectedPath;
|
|
||||||
GlobalOutputFolderTextBox.Text = outputFolder;
|
|
||||||
StatusText.Text = "Cartella output aggiornata";
|
|
||||||
Settings.Default.LastOutputFolder = outputFolder;
|
|
||||||
Settings.Default.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SettingsButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var win = new SettingsWindow { Owner = this };
|
|
||||||
if (win.ShowDialog() == true)
|
|
||||||
{
|
|
||||||
ConfigureFFMpeg();
|
|
||||||
StatusText.Text = "Impostazioni aggiornate";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
|
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_processingService.JobQueue.Count == 0)
|
if (_processingService.JobQueue.Count == 0)
|
||||||
@@ -254,7 +204,7 @@ namespace Ganimede
|
|||||||
}
|
}
|
||||||
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
|
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
|
||||||
{
|
{
|
||||||
WpfMessageBox.Show("Nessun job in stato In attesa.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
|
WpfMessageBox.Show("Nessun job in attesa nella coda.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _processingService.StartProcessingAsync();
|
await _processingService.StartProcessingAsync();
|
||||||
@@ -281,15 +231,23 @@ namespace Ganimede
|
|||||||
private void ConfigureSelectedButton_Click(object sender, RoutedEventArgs e)
|
private void ConfigureSelectedButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_selectedJobs.Count == 0) return;
|
if (_selectedJobs.Count == 0) return;
|
||||||
|
|
||||||
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 = $"Configurazione applicata a {_selectedJobs.Count} job";
|
StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
|
||||||
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
|
|
||||||
|
if (!string.IsNullOrEmpty(outputFolder))
|
||||||
{
|
{
|
||||||
var createSub = Settings.Default.CreateSubfolder;
|
foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
|
||||||
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame ? outputFolder : (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
|
{
|
||||||
|
var createSub = Settings.Default.CreateSubfolder;
|
||||||
|
job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame
|
||||||
|
? outputFolder
|
||||||
|
: (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateJobsSummary();
|
UpdateJobsSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,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("Ci sono job in elaborazione.\n\nSi: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
var res = WpfMessageBox.Show("Ci sono job in elaborazione.\n\nSì: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla operazione", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
||||||
if (res == MessageBoxResult.Cancel) return;
|
if (res == MessageBoxResult.Cancel) return;
|
||||||
if (res == MessageBoxResult.Yes)
|
if (res == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
@@ -334,7 +292,7 @@ namespace Ganimede
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (WpfMessageBox.Show("Rimuovere tutti i job?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
if (WpfMessageBox.Show("Rimuovere tutti i job dalla coda?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
_processingService.JobQueue.Clear();
|
_processingService.JobQueue.Clear();
|
||||||
thumbnails.Clear();
|
thumbnails.Clear();
|
||||||
@@ -344,6 +302,97 @@ namespace Ganimede
|
|||||||
UpdateQueueCount();
|
UpdateQueueCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dialog = new WpfOpenFileDialog { Filter = "Video Files|*.mp4;*.avi;*.mov;*.mkv;*.wmv;*.flv;*.webm|All Files|*.*", Multiselect = true };
|
||||||
|
if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona la cartella contenente i video", ShowNewFolderButton = false };
|
||||||
|
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
|
||||||
|
if (files.Length == 0)
|
||||||
|
{
|
||||||
|
WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddVideosToQueue(files);
|
||||||
|
StatusText.Text = $"Importati {files.Length} video";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddVideosToQueue(string[] paths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(outputFolder))
|
||||||
|
{
|
||||||
|
WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var createSub = Settings.Default.CreateSubfolder;
|
||||||
|
foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
|
||||||
|
StatusText.Text = $"Aggiunti {paths.Length} video";
|
||||||
|
Settings.Default.LastVideoPath = paths.FirstOrDefault();
|
||||||
|
Settings.Default.Save();
|
||||||
|
UpdateQueueCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||||
|
if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
|
||||||
|
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||||
|
{
|
||||||
|
outputFolder = dialog.SelectedPath;
|
||||||
|
GlobalOutputFolderTextBox.Text = outputFolder;
|
||||||
|
StatusText.Text = "Cartella output aggiornata";
|
||||||
|
Settings.Default.LastOutputFolder = outputFolder;
|
||||||
|
Settings.Default.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveSettings_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var selectedFrameSize = FrameSizeComboBox.SelectedItem as ComboBoxItem;
|
||||||
|
Settings.Default.FrameSize = selectedFrameSize?.Tag?.ToString() ?? "original";
|
||||||
|
|
||||||
|
var selectedOverwrite = OverwriteModeComboBox.SelectedItem as ComboBoxItem;
|
||||||
|
Settings.Default.DefaultOverwriteMode = selectedOverwrite?.Tag?.ToString() ?? "Ask";
|
||||||
|
|
||||||
|
if (DefaultModeSingleRadio.IsChecked == true)
|
||||||
|
Settings.Default.DefaultExtractionMode = "SingleFrame";
|
||||||
|
else if (DefaultModeAutoRadio.IsChecked == true)
|
||||||
|
Settings.Default.DefaultExtractionMode = "Auto";
|
||||||
|
else
|
||||||
|
Settings.Default.DefaultExtractionMode = "Full";
|
||||||
|
|
||||||
|
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||||
|
Settings.Default.SingleFrameUseSubfolder = SingleFrameUseSubfolderCheckBox.IsChecked ?? false;
|
||||||
|
|
||||||
|
Settings.Default.Save();
|
||||||
|
|
||||||
|
StatusText.Text = "✓ Impostazioni salvate con successo";
|
||||||
|
Debug.WriteLine("[SETTINGS] Impostazioni salvate");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText.Text = "✗ Impossibile salvare le impostazioni";
|
||||||
|
Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
|
||||||
|
WpfMessageBox.Show($"Errore nel salvataggio: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsVideoFile(string path)
|
private static bool IsVideoFile(string path)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||||
@@ -360,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,4 +0,0 @@
|
|||||||
namespace Ganimede.Models
|
|
||||||
{
|
|
||||||
// Modelli futuri (es. VideoInfo, FrameInfo)
|
|
||||||
}
|
|
||||||
12
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
12
Ganimede/Ganimede/Properties/Settings.Designer.cs
generated
@@ -47,18 +47,6 @@ namespace Ganimede.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("C:\\Users\\balbo\\source\\repos\\Ganimede\\Ganimede\\Ganimede\\FFMpeg")]
|
|
||||||
public string FFmpegBinFolder {
|
|
||||||
get {
|
|
||||||
return ((string)(this["FFmpegBinFolder"]));
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
this["FFmpegBinFolder"] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("True")]
|
[global::System.Configuration.DefaultSettingValueAttribute("True")]
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using FFMpegCore;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Ganimede.Models;
|
using Ganimede.Models;
|
||||||
using Ganimede.Properties;
|
using Ganimede.Properties;
|
||||||
using Ganimede.Helpers;
|
using Ganimede.Helpers;
|
||||||
|
using Ganimede.VideoProcessing;
|
||||||
|
|
||||||
namespace Ganimede.Services
|
namespace Ganimede.Services
|
||||||
{
|
{
|
||||||
@@ -146,11 +146,10 @@ namespace Ganimede.Services
|
|||||||
{
|
{
|
||||||
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
|
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
|
||||||
|
|
||||||
// (Do not decide folder change yet; need analysis for Auto mode)
|
// Analyze video using VideoAnalyzer
|
||||||
|
var mediaInfo = await Task.Run(() => VideoAnalyzer.Analyze(job.VideoPath), cancellationToken);
|
||||||
var mediaInfo = await FFProbe.AnalyseAsync(job.VideoPath);
|
int frameRate = (int)mediaInfo.FrameRate;
|
||||||
int frameRate = (int)(mediaInfo.PrimaryVideoStream?.FrameRate ?? 24);
|
int totalFrames = mediaInfo.TotalFrames;
|
||||||
int totalFrames = (int)(mediaInfo.Duration.TotalSeconds * frameRate);
|
|
||||||
Debug.WriteLine($"[INFO] Video {job.VideoName}: {totalFrames} frames at {frameRate} fps, duration {mediaInfo.Duration}");
|
Debug.WriteLine($"[INFO] Video {job.VideoName}: {totalFrames} frames at {frameRate} fps, duration {mediaInfo.Duration}");
|
||||||
|
|
||||||
// Heuristic suggestion
|
// Heuristic suggestion
|
||||||
@@ -161,11 +160,10 @@ namespace Ganimede.Services
|
|||||||
suggestSingleFrame = true;
|
suggestSingleFrame = true;
|
||||||
else if (mediaInfo.Duration.TotalSeconds >= 3 && mediaInfo.Duration.TotalSeconds <= 45)
|
else if (mediaInfo.Duration.TotalSeconds >= 3 && mediaInfo.Duration.TotalSeconds <= 45)
|
||||||
{
|
{
|
||||||
var primary = mediaInfo.PrimaryVideoStream;
|
if (mediaInfo.BitRate > 0 && mediaInfo.Width > 0 && mediaInfo.Height > 0)
|
||||||
if (primary != null && primary.BitRate > 0 && primary.Width > 0 && primary.Height > 0)
|
|
||||||
{
|
{
|
||||||
double pixels = primary.Width * primary.Height;
|
double pixels = mediaInfo.Width * mediaInfo.Height;
|
||||||
if (primary.BitRate < pixels * 0.3)
|
if (mediaInfo.BitRate < pixels * 0.3)
|
||||||
suggestSingleFrame = true;
|
suggestSingleFrame = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,7 +222,7 @@ namespace Ganimede.Services
|
|||||||
job.StatusMessage = "Frame already exists (skipped)";
|
job.StatusMessage = "Frame already exists (skipped)";
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ExtractFrameAsync(job, targetIndex, frameRate, frameSize, framePath);
|
await ExtractFrameAsync(job, frameTime, frameSize, framePath, cancellationToken);
|
||||||
job.StatusMessage = "Single frame extracted";
|
job.StatusMessage = "Single frame extracted";
|
||||||
}
|
}
|
||||||
job.Progress = 100;
|
job.Progress = 100;
|
||||||
@@ -234,32 +232,47 @@ namespace Ganimede.Services
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full extraction loop (unchanged)
|
// Full extraction using FrameExtractor
|
||||||
int processedFrames = 0;
|
int processedFrames = 0;
|
||||||
int skippedFrames = 0;
|
int skippedFrames = 0;
|
||||||
for (int i = 0; i < totalFrames; i++)
|
|
||||||
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
FrameExtractor.ExtractAllFrames(
|
||||||
{
|
job.VideoPath,
|
||||||
job.Status = JobStatus.Cancelled;
|
job.OutputFolder,
|
||||||
job.StatusMessage = "Cancelled by user";
|
(frameIndex, timePosition) => NamingHelper.GenerateFileName(namingPattern, job, frameIndex, timePosition, customPrefix),
|
||||||
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
|
frameSize.width,
|
||||||
return;
|
frameSize.height,
|
||||||
}
|
(current, total) =>
|
||||||
var frameTime = TimeSpan.FromSeconds((double)i / frameRate);
|
{
|
||||||
var fileName = NamingHelper.GenerateFileName(namingPattern, job, i, frameTime, customPrefix);
|
if (cancellationToken.IsCancellationRequested)
|
||||||
string framePath = Path.Combine(job.OutputFolder, fileName);
|
return;
|
||||||
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
|
|
||||||
skippedFrames++;
|
job.Progress = (double)current / total * 100;
|
||||||
else
|
job.StatusMessage = $"Processed {current}/{total} frames ({job.Progress:F1}%)";
|
||||||
{
|
processedFrames = current;
|
||||||
await ExtractFrameAsync(job, i, frameRate, frameSize, framePath);
|
},
|
||||||
processedFrames++;
|
(framePath) =>
|
||||||
}
|
{
|
||||||
job.Progress = (double)(i + 1) / totalFrames * 100;
|
if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
|
||||||
job.StatusMessage = $"Processed {processedFrames}/{totalFrames} frames ({job.Progress:F1}%)" + (skippedFrames > 0 ? $" - Skipped {skippedFrames}" : "");
|
{
|
||||||
if (i % 10 == 0) await Task.Delay(1, cancellationToken);
|
skippedFrames++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
job.Status = JobStatus.Cancelled;
|
||||||
|
job.StatusMessage = "Cancelled by user";
|
||||||
|
Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
job.Status = JobStatus.Completed;
|
job.Status = JobStatus.Completed;
|
||||||
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
|
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
|
||||||
job.Progress = 100;
|
job.Progress = 100;
|
||||||
@@ -327,62 +340,25 @@ namespace Ganimede.Services
|
|||||||
return Settings.Default.DefaultCustomPrefix ?? "custom";
|
return Settings.Default.DefaultCustomPrefix ?? "custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath)
|
private async Task ExtractFrameAsync(VideoJob job, TimeSpan frameTime, (int width, int height) frameSize, string framePath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (frameSize.width == -1 && frameSize.height == -1)
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
try
|
FrameExtractor.ExtractFrame(
|
||||||
{
|
job.VideoPath,
|
||||||
await FFMpegArguments
|
frameTime,
|
||||||
.FromFileInput(job.VideoPath)
|
framePath,
|
||||||
.OutputToFile(framePath, true, options => options
|
frameSize.width,
|
||||||
.Seek(frameTime)
|
frameSize.height
|
||||||
.WithFrameOutputCount(1)
|
);
|
||||||
.WithVideoCodec("png"))
|
}, cancellationToken);
|
||||||
.ProcessAsynchronously();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await FFMpegArguments
|
|
||||||
.FromFileInput(job.VideoPath)
|
|
||||||
.OutputToFile(framePath, true, options => options
|
|
||||||
.Seek(frameTime)
|
|
||||||
.WithFrameOutputCount(1))
|
|
||||||
.ProcessAsynchronously();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await FFMpegArguments
|
|
||||||
.FromFileInput(job.VideoPath)
|
|
||||||
.OutputToFile(framePath, true, options => options
|
|
||||||
.Seek(frameTime)
|
|
||||||
.WithFrameOutputCount(1)
|
|
||||||
.WithVideoCodec("png")
|
|
||||||
.Resize(frameSize.width, frameSize.height))
|
|
||||||
.ProcessAsynchronously();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await FFMpegArguments
|
|
||||||
.FromFileInput(job.VideoPath)
|
|
||||||
.OutputToFile(framePath, true, options => options
|
|
||||||
.Seek(frameTime)
|
|
||||||
.WithFrameOutputCount(1)
|
|
||||||
.Resize(frameSize.width, frameSize.height))
|
|
||||||
.ProcessAsynchronously();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}");
|
Debug.WriteLine($"[ERROR] Failed to extract frame from {job.VideoName}: {ex.Message}");
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs
Normal file
147
Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using Ganimede.VideoProcessing.MediaFoundation;
|
||||||
|
|
||||||
|
namespace Ganimede.VideoProcessing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides frame extraction capabilities from video files using Windows Media Foundation
|
||||||
|
/// NO external dependencies - uses only Windows built-in APIs
|
||||||
|
/// </summary>
|
||||||
|
public class FrameExtractor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts a single frame from a video at a specific time position
|
||||||
|
/// </summary>
|
||||||
|
public static void ExtractFrame(
|
||||||
|
string videoPath,
|
||||||
|
TimeSpan timePosition,
|
||||||
|
string outputPath,
|
||||||
|
int targetWidth = -1,
|
||||||
|
int targetHeight = -1)
|
||||||
|
{
|
||||||
|
if (!File.Exists(videoPath))
|
||||||
|
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var reader = new MFVideoReader(videoPath))
|
||||||
|
{
|
||||||
|
// Read frame at specified time
|
||||||
|
using (var bitmap = reader.ReadFrameAtTime(timePosition))
|
||||||
|
{
|
||||||
|
if (bitmap == null)
|
||||||
|
throw new InvalidOperationException($"Could not extract frame at position {timePosition}");
|
||||||
|
|
||||||
|
// Resize if needed
|
||||||
|
if (targetWidth > 0 && targetHeight > 0 &&
|
||||||
|
(targetWidth != bitmap.Width || targetHeight != bitmap.Height))
|
||||||
|
{
|
||||||
|
using (var resized = new Bitmap(bitmap, targetWidth, targetHeight))
|
||||||
|
{
|
||||||
|
SaveBitmapAsPng(resized, outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SaveBitmapAsPng(bitmap, outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to extract frame: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts all frames from a video
|
||||||
|
/// </summary>
|
||||||
|
public static void ExtractAllFrames(
|
||||||
|
string videoPath,
|
||||||
|
string outputFolder,
|
||||||
|
Func<int, TimeSpan, string> fileNameGenerator,
|
||||||
|
int targetWidth = -1,
|
||||||
|
int targetHeight = -1,
|
||||||
|
Action<int, int>? onProgress = null,
|
||||||
|
Func<string, bool>? shouldSkipFrame = null)
|
||||||
|
{
|
||||||
|
if (!File.Exists(videoPath))
|
||||||
|
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||||
|
|
||||||
|
if (!Directory.Exists(outputFolder))
|
||||||
|
Directory.CreateDirectory(outputFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var reader = new MFVideoReader(videoPath))
|
||||||
|
{
|
||||||
|
// Calculate total frames
|
||||||
|
int totalFrames = (int)(reader.Duration.TotalSeconds * reader.FrameRate);
|
||||||
|
int frameIndex = 0;
|
||||||
|
TimeSpan currentTime = TimeSpan.Zero;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Read next frame
|
||||||
|
using (var bitmap = reader.ReadFrame())
|
||||||
|
{
|
||||||
|
if (bitmap == null)
|
||||||
|
break; // End of stream
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
var fileName = fileNameGenerator(frameIndex, currentTime);
|
||||||
|
var fullPath = Path.Combine(outputFolder, fileName);
|
||||||
|
|
||||||
|
// Check if frame should be skipped
|
||||||
|
if (shouldSkipFrame != null && shouldSkipFrame(fullPath))
|
||||||
|
{
|
||||||
|
frameIndex++;
|
||||||
|
currentTime = TimeSpan.FromSeconds(frameIndex / reader.FrameRate);
|
||||||
|
onProgress?.Invoke(frameIndex, totalFrames);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize if needed
|
||||||
|
if (targetWidth > 0 && targetHeight > 0 &&
|
||||||
|
(targetWidth != bitmap.Width || targetHeight != bitmap.Height))
|
||||||
|
{
|
||||||
|
using (var resized = new Bitmap(bitmap, targetWidth, targetHeight))
|
||||||
|
{
|
||||||
|
SaveBitmapAsPng(resized, fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SaveBitmapAsPng(bitmap, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
frameIndex++;
|
||||||
|
currentTime = TimeSpan.FromSeconds(frameIndex / reader.FrameRate);
|
||||||
|
onProgress?.Invoke(frameIndex, totalFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to extract frames: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a bitmap as PNG file
|
||||||
|
/// </summary>
|
||||||
|
private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(outputPath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
|
bitmap.Save(outputPath, ImageFormat.Png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ganimede.VideoProcessing.MediaFoundation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Additional Media Foundation COM interfaces for frame extraction
|
||||||
|
/// These interfaces extend the basic WMF functionality for video decoding
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
#region Source Reader Interfaces
|
||||||
|
|
||||||
|
[ComImport, Guid("70ae66f2-c809-4e4f-8915-bdcb406b7993")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFSourceReader
|
||||||
|
{
|
||||||
|
[PreserveSig]
|
||||||
|
int GetStreamSelection(
|
||||||
|
int dwStreamIndex,
|
||||||
|
out bool pfSelected);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetStreamSelection(
|
||||||
|
int dwStreamIndex,
|
||||||
|
bool fSelected);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetNativeMediaType(
|
||||||
|
int dwStreamIndex,
|
||||||
|
int dwMediaTypeIndex,
|
||||||
|
out IMFMediaType ppMediaType);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetCurrentMediaType(
|
||||||
|
int dwStreamIndex,
|
||||||
|
out IMFMediaType ppMediaType);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetCurrentMediaType(
|
||||||
|
int dwStreamIndex,
|
||||||
|
IntPtr pdwReserved,
|
||||||
|
IMFMediaType pMediaType);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetCurrentPosition(
|
||||||
|
Guid guidTimeFormat,
|
||||||
|
IntPtr varPosition);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int ReadSample(
|
||||||
|
int dwStreamIndex,
|
||||||
|
int dwControlFlags,
|
||||||
|
out int pdwActualStreamIndex,
|
||||||
|
out MF_SOURCE_READER_FLAG pdwStreamFlags,
|
||||||
|
out long pllTimestamp,
|
||||||
|
out IMFSample ppSample);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Flush(int dwStreamIndex);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetServiceForStream(
|
||||||
|
int dwStreamIndex,
|
||||||
|
Guid guidService,
|
||||||
|
Guid riid,
|
||||||
|
out IntPtr ppvObject);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetPresentationAttribute(
|
||||||
|
int dwStreamIndex,
|
||||||
|
Guid guidAttribute,
|
||||||
|
IntPtr pvarAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
internal enum MF_SOURCE_READER_FLAG
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Error = 0x00000001,
|
||||||
|
EndOfStream = 0x00000002,
|
||||||
|
NewStream = 0x00000004,
|
||||||
|
NativeMediaTypeChanged = 0x00000010,
|
||||||
|
CurrentMediaTypeChanged = 0x00000020,
|
||||||
|
StreamTick = 0x00000100,
|
||||||
|
AllEffectsRemoved = 0x00000200
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFSourceReaderIndex
|
||||||
|
{
|
||||||
|
public const int FirstVideoStream = unchecked((int)0xFFFFFFFC);
|
||||||
|
public const int FirstAudioStream = unchecked((int)0xFFFFFFFD);
|
||||||
|
public const int MediaSource = unchecked((int)0xFFFFFFFE);
|
||||||
|
public const int AnyStream = unchecked((int)0xFFFFFFFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Sample and Buffer Interfaces
|
||||||
|
|
||||||
|
[ComImport, Guid("c40a00f2-b93a-4d80-ae8c-5a1c634f58e4")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFSample : IMFAttributes
|
||||||
|
{
|
||||||
|
#region IMFAttributes methods
|
||||||
|
[PreserveSig] new int GetItem(Guid guidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] new int GetItemType(Guid guidKey, out ushort pType);
|
||||||
|
[PreserveSig] new int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||||
|
[PreserveSig] new int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||||
|
[PreserveSig] new int GetUINT32(Guid guidKey, out int punValue);
|
||||||
|
[PreserveSig] new int GetUINT64(Guid guidKey, out long punValue);
|
||||||
|
[PreserveSig] new int GetDouble(Guid guidKey, out double pfValue);
|
||||||
|
[PreserveSig] new int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||||
|
[PreserveSig] new int GetStringLength(Guid guidKey, out int pcchLength);
|
||||||
|
[PreserveSig] new int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||||
|
[PreserveSig] new int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||||
|
[PreserveSig] new int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||||
|
[PreserveSig] new int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||||
|
[PreserveSig] new int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||||
|
[PreserveSig] new int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||||
|
[PreserveSig] new int SetItem(Guid guidKey, IntPtr Value);
|
||||||
|
[PreserveSig] new int DeleteItem(Guid guidKey);
|
||||||
|
[PreserveSig] new int DeleteAllItems();
|
||||||
|
[PreserveSig] new int SetUINT32(Guid guidKey, int unValue);
|
||||||
|
[PreserveSig] new int SetUINT64(Guid guidKey, long unValue);
|
||||||
|
[PreserveSig] new int SetDouble(Guid guidKey, double fValue);
|
||||||
|
[PreserveSig] new int SetGUID(Guid guidKey, Guid guidValue);
|
||||||
|
[PreserveSig] new int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||||
|
[PreserveSig] new int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||||
|
[PreserveSig] new int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||||
|
[PreserveSig] new int LockStore();
|
||||||
|
[PreserveSig] new int UnlockStore();
|
||||||
|
[PreserveSig] new int GetCount(out int pcItems);
|
||||||
|
[PreserveSig] new int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] new int CopyAllItems(IMFAttributes pDest);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[PreserveSig] int GetSampleFlags(out int pdwSampleFlags);
|
||||||
|
[PreserveSig] int SetSampleFlags(int dwSampleFlags);
|
||||||
|
[PreserveSig] int GetSampleTime(out long phnsSampleTime);
|
||||||
|
[PreserveSig] int SetSampleTime(long hnsSampleTime);
|
||||||
|
[PreserveSig] int GetSampleDuration(out long phnsSampleDuration);
|
||||||
|
[PreserveSig] int SetSampleDuration(long hnsSampleDuration);
|
||||||
|
[PreserveSig] int GetBufferCount(out int pdwBufferCount);
|
||||||
|
[PreserveSig] int GetBufferByIndex(int dwIndex, out IMFMediaBuffer ppBuffer);
|
||||||
|
[PreserveSig] int ConvertToContiguousBuffer(out IMFMediaBuffer ppBuffer);
|
||||||
|
[PreserveSig] int AddBuffer(IMFMediaBuffer pBuffer);
|
||||||
|
[PreserveSig] int RemoveBufferByIndex(int dwIndex);
|
||||||
|
[PreserveSig] int RemoveAllBuffers();
|
||||||
|
[PreserveSig] int GetTotalLength(out int pcbTotalLength);
|
||||||
|
[PreserveSig] int CopyToBuffer(IMFMediaBuffer pBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("045FA593-8799-42b8-BC8D-8968C6453507")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFMediaBuffer
|
||||||
|
{
|
||||||
|
[PreserveSig] int Lock(out IntPtr ppbBuffer, out int pcbMaxLength, out int pcbCurrentLength);
|
||||||
|
[PreserveSig] int Unlock();
|
||||||
|
[PreserveSig] int GetCurrentLength(out int pcbCurrentLength);
|
||||||
|
[PreserveSig] int SetCurrentLength(int cbCurrentLength);
|
||||||
|
[PreserveSig] int GetMaxLength(out int pcbMaxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("7DC9D5F9-9ED9-44ec-9BBF-0600BB589FBB")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMF2DBuffer
|
||||||
|
{
|
||||||
|
[PreserveSig] int Lock2D(out IntPtr pbScanline0, out int plPitch);
|
||||||
|
[PreserveSig] int Unlock2D();
|
||||||
|
[PreserveSig] int GetScanline0AndPitch(out IntPtr pbScanline0, out int plPitch);
|
||||||
|
[PreserveSig] int IsContiguousFormat(out bool pfIsContiguous);
|
||||||
|
[PreserveSig] int GetContiguousLength(out int pcbLength);
|
||||||
|
[PreserveSig] int ContiguousCopyTo(IntPtr pbDestBuffer, int cbDestBuffer);
|
||||||
|
[PreserveSig] int ContiguousCopyFrom(IntPtr pbSrcBuffer, int cbSrcBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Functions and Constants
|
||||||
|
|
||||||
|
internal static class MFExternExtended
|
||||||
|
{
|
||||||
|
[DllImport("mfreadwrite.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFCreateSourceReaderFromURL(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string pwszURL,
|
||||||
|
IMFAttributes pAttributes,
|
||||||
|
out IMFSourceReader ppSourceReader);
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFCreateMediaType(out IMFMediaType ppMFType);
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFCreateAttributes(out IMFAttributes ppMFAttributes, int cInitialSize);
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFCreateMemoryBuffer(int cbMaxLength, out IMFMediaBuffer ppBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFVideoFormat
|
||||||
|
{
|
||||||
|
// Uncompressed RGB formats
|
||||||
|
public static readonly Guid RGB32 = new Guid("00000016-0000-0010-8000-00aa00389b71");
|
||||||
|
public static readonly Guid RGB24 = new Guid("00000014-0000-0010-8000-00aa00389b71");
|
||||||
|
public static readonly Guid RGB555 = new Guid("00000018-0000-0010-8000-00aa00389b71");
|
||||||
|
public static readonly Guid RGB565 = new Guid("00000017-0000-0010-8000-00aa00389b71");
|
||||||
|
|
||||||
|
// YUV formats
|
||||||
|
public static readonly Guid NV12 = new Guid("3231564E-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid YUY2 = new Guid("32595559-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid UYVY = new Guid("59565955-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid YV12 = new Guid("32315659-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid I420 = new Guid("30323449-0000-0010-8000-00AA00389B71");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFAttributesClsidExtended
|
||||||
|
{
|
||||||
|
// Source Reader attributes
|
||||||
|
public static readonly Guid MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING =
|
||||||
|
new Guid("fb394f3d-ccf1-42ee-bbb3-f9b845d5681d");
|
||||||
|
public static readonly Guid MF_SOURCE_READER_DISABLE_DXVA =
|
||||||
|
new Guid("aa456cfd-3943-4a1e-a77d-1838c0ea2e35");
|
||||||
|
public static readonly Guid MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING =
|
||||||
|
new Guid("0f81da2c-b537-4672-a8b2-a681b17307a3");
|
||||||
|
|
||||||
|
// Low latency mode
|
||||||
|
public static readonly Guid MF_LOW_LATENCY =
|
||||||
|
new Guid("9c27891a-ed7a-40e1-88e8-b22727a024ee");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Base Interfaces (referenced from VideoAnalyzer)
|
||||||
|
|
||||||
|
[ComImport, Guid("2CD2D921-C447-44A7-A13C-4ADABFC247E3")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFAttributes
|
||||||
|
{
|
||||||
|
[PreserveSig] int GetItem(Guid guidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] int GetItemType(Guid guidKey, out ushort pType);
|
||||||
|
[PreserveSig] int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||||
|
[PreserveSig] int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||||
|
[PreserveSig] int GetUINT32(Guid guidKey, out int punValue);
|
||||||
|
[PreserveSig] int GetUINT64(Guid guidKey, out long punValue);
|
||||||
|
[PreserveSig] int GetDouble(Guid guidKey, out double pfValue);
|
||||||
|
[PreserveSig] int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||||
|
[PreserveSig] int GetStringLength(Guid guidKey, out int pcchLength);
|
||||||
|
[PreserveSig] int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||||
|
[PreserveSig] int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||||
|
[PreserveSig] int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||||
|
[PreserveSig] int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||||
|
[PreserveSig] int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||||
|
[PreserveSig] int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||||
|
[PreserveSig] int SetItem(Guid guidKey, IntPtr Value);
|
||||||
|
[PreserveSig] int DeleteItem(Guid guidKey);
|
||||||
|
[PreserveSig] int DeleteAllItems();
|
||||||
|
[PreserveSig] int SetUINT32(Guid guidKey, int unValue);
|
||||||
|
[PreserveSig] int SetUINT64(Guid guidKey, long unValue);
|
||||||
|
[PreserveSig] int SetDouble(Guid guidKey, double fValue);
|
||||||
|
[PreserveSig] int SetGUID(Guid guidKey, Guid guidValue);
|
||||||
|
[PreserveSig] int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||||
|
[PreserveSig] int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||||
|
[PreserveSig] int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||||
|
[PreserveSig] int LockStore();
|
||||||
|
[PreserveSig] int UnlockStore();
|
||||||
|
[PreserveSig] int GetCount(out int pcItems);
|
||||||
|
[PreserveSig] int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] int CopyAllItems(IMFAttributes pDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("44AE0FA8-EA31-4109-8D2E-4CAE4997C555")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFMediaType : IMFAttributes
|
||||||
|
{
|
||||||
|
#region IMFAttributes methods
|
||||||
|
[PreserveSig] new int GetItem(Guid guidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] new int GetItemType(Guid guidKey, out ushort pType);
|
||||||
|
[PreserveSig] new int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||||
|
[PreserveSig] new int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||||
|
[PreserveSig] new int GetUINT32(Guid guidKey, out int punValue);
|
||||||
|
[PreserveSig] new int GetUINT64(Guid guidKey, out long punValue);
|
||||||
|
[PreserveSig] new int GetDouble(Guid guidKey, out double pfValue);
|
||||||
|
[PreserveSig] new int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||||
|
[PreserveSig] new int GetStringLength(Guid guidKey, out int pcchLength);
|
||||||
|
[PreserveSig] new int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||||
|
[PreserveSig] new int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||||
|
[PreserveSig] new int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||||
|
[PreserveSig] new int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||||
|
[PreserveSig] new int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||||
|
[PreserveSig] new int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||||
|
[PreserveSig] new int SetItem(Guid guidKey, IntPtr Value);
|
||||||
|
[PreserveSig] new int DeleteItem(Guid guidKey);
|
||||||
|
[PreserveSig] new int DeleteAllItems();
|
||||||
|
[PreserveSig] new int SetUINT32(Guid guidKey, int unValue);
|
||||||
|
[PreserveSig] new int SetUINT64(Guid guidKey, long unValue);
|
||||||
|
[PreserveSig] new int SetDouble(Guid guidKey, double fValue);
|
||||||
|
[PreserveSig] new int SetGUID(Guid guidKey, Guid guidValue);
|
||||||
|
[PreserveSig] new int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||||
|
[PreserveSig] new int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||||
|
[PreserveSig] new int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||||
|
[PreserveSig] new int LockStore();
|
||||||
|
[PreserveSig] new int UnlockStore();
|
||||||
|
[PreserveSig] new int GetCount(out int pcItems);
|
||||||
|
[PreserveSig] new int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] new int CopyAllItems(IMFAttributes pDest);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[PreserveSig] int GetMajorType(out Guid pguidMajorType);
|
||||||
|
[PreserveSig] int IsCompressedFormat(out bool pfCompressed);
|
||||||
|
[PreserveSig] int IsEqual(IMFMediaType pIMediaType, out uint pdwFlags);
|
||||||
|
[PreserveSig] int GetRepresentation(Guid guidRepresentation, out IntPtr ppvRepresentation);
|
||||||
|
[PreserveSig] int FreeRepresentation(Guid guidRepresentation, IntPtr pvRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ganimede.VideoProcessing.MediaFoundation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Video reader class using Windows Media Foundation Source Reader
|
||||||
|
/// Handles frame extraction from video files
|
||||||
|
/// </summary>
|
||||||
|
public class MFVideoReader : IDisposable
|
||||||
|
{
|
||||||
|
private IMFSourceReader? _sourceReader;
|
||||||
|
private bool _disposed = false;
|
||||||
|
private int _videoStreamIndex = 0;
|
||||||
|
private int _width;
|
||||||
|
private int _height;
|
||||||
|
private double _frameRate;
|
||||||
|
private TimeSpan _duration;
|
||||||
|
|
||||||
|
public int Width => _width;
|
||||||
|
public int Height => _height;
|
||||||
|
public double FrameRate => _frameRate;
|
||||||
|
public TimeSpan Duration => _duration;
|
||||||
|
|
||||||
|
public MFVideoReader(string videoPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(videoPath))
|
||||||
|
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||||
|
|
||||||
|
// Initialize Media Foundation
|
||||||
|
int hr = MFExtern.MFStartup(MFExtern.MF_VERSION, 0);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create attributes for source reader
|
||||||
|
IMFAttributes? attributes = null;
|
||||||
|
hr = MFExternExtended.MFCreateAttributes(out attributes, 2);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Enable video processing
|
||||||
|
hr = attributes!.SetUINT32(MFAttributesClsidExtended.MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, 1);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Low latency mode
|
||||||
|
hr = attributes.SetUINT32(MFAttributesClsidExtended.MF_LOW_LATENCY, 1);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Create source reader
|
||||||
|
hr = MFExternExtended.MFCreateSourceReaderFromURL(videoPath, attributes, out _sourceReader);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Find first video stream
|
||||||
|
_videoStreamIndex = MFSourceReaderIndex.FirstVideoStream;
|
||||||
|
|
||||||
|
// Configure output media type (RGB32)
|
||||||
|
IMFMediaType? mediaType = null;
|
||||||
|
hr = MFExternExtended.MFCreateMediaType(out mediaType);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hr = mediaType!.SetGUID(MFAttributesClsid.MF_MT_MAJOR_TYPE, MFMediaType.Video);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
hr = mediaType.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFVideoFormat.RGB32);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
hr = _sourceReader!.SetCurrentMediaType(_videoStreamIndex, IntPtr.Zero, mediaType);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (mediaType != null) Marshal.ReleaseComObject(mediaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get actual output media type
|
||||||
|
IMFMediaType? currentMediaType = null;
|
||||||
|
hr = _sourceReader.GetCurrentMediaType(_videoStreamIndex, out currentMediaType);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get frame size
|
||||||
|
long frameSize;
|
||||||
|
hr = currentMediaType!.GetUINT64(MFAttributesClsid.MF_MT_FRAME_SIZE, out frameSize);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
_width = (int)(frameSize >> 32);
|
||||||
|
_height = (int)(frameSize & 0xFFFFFFFF);
|
||||||
|
|
||||||
|
// Get frame rate
|
||||||
|
long frameRateRatio;
|
||||||
|
hr = currentMediaType.GetUINT64(MFAttributesClsid.MF_MT_FRAME_RATE, out frameRateRatio);
|
||||||
|
if (hr >= 0)
|
||||||
|
{
|
||||||
|
int numerator = (int)(frameRateRatio >> 32);
|
||||||
|
int denominator = (int)(frameRateRatio & 0xFFFFFFFF);
|
||||||
|
_frameRate = denominator > 0 ? (double)numerator / denominator : 30.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_frameRate = 30.0; // Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (currentMediaType != null) Marshal.ReleaseComObject(currentMediaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get duration
|
||||||
|
IntPtr durationPtr = IntPtr.Zero;
|
||||||
|
hr = _sourceReader.GetPresentationAttribute(
|
||||||
|
MFSourceReaderIndex.MediaSource,
|
||||||
|
MFAttributesClsid.MF_PD_DURATION,
|
||||||
|
durationPtr);
|
||||||
|
|
||||||
|
if (hr >= 0 && durationPtr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
long durationTicks = Marshal.ReadInt64(durationPtr);
|
||||||
|
_duration = TimeSpan.FromTicks(durationTicks / 10);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_duration = TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup attributes
|
||||||
|
if (attributes != null) Marshal.ReleaseComObject(attributes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeks to a specific time position in the video
|
||||||
|
/// </summary>
|
||||||
|
public void SeekTo(TimeSpan position)
|
||||||
|
{
|
||||||
|
if (_disposed || _sourceReader == null)
|
||||||
|
throw new ObjectDisposedException(nameof(MFVideoReader));
|
||||||
|
|
||||||
|
long timeInHundredNanoseconds = position.Ticks * 10;
|
||||||
|
IntPtr varPosition = Marshal.AllocHGlobal(16); // PROPVARIANT size
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Write PROPVARIANT structure
|
||||||
|
// VT_I8 = 20 (64-bit signed integer)
|
||||||
|
Marshal.WriteInt16(varPosition, 0, 20); // vt
|
||||||
|
Marshal.WriteInt64(varPosition, 8, timeInHundredNanoseconds); // hVal
|
||||||
|
|
||||||
|
int hr = _sourceReader.SetCurrentPosition(Guid.Empty, varPosition);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(varPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the next video frame
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap? ReadFrame()
|
||||||
|
{
|
||||||
|
if (_disposed || _sourceReader == null)
|
||||||
|
throw new ObjectDisposedException(nameof(MFVideoReader));
|
||||||
|
|
||||||
|
IMFSample? sample = null;
|
||||||
|
int streamIndex;
|
||||||
|
MF_SOURCE_READER_FLAG flags;
|
||||||
|
long timestamp;
|
||||||
|
|
||||||
|
int hr = _sourceReader.ReadSample(
|
||||||
|
_videoStreamIndex,
|
||||||
|
0,
|
||||||
|
out streamIndex,
|
||||||
|
out flags,
|
||||||
|
out timestamp,
|
||||||
|
out sample);
|
||||||
|
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Check for end of stream
|
||||||
|
if ((flags & MF_SOURCE_READER_FLAG.EndOfStream) != 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sample == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ConvertSampleToBitmap(sample);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.ReleaseComObject(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a frame at a specific time position
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap? ReadFrameAtTime(TimeSpan position)
|
||||||
|
{
|
||||||
|
SeekTo(position);
|
||||||
|
return ReadFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an IMFSample to a Bitmap
|
||||||
|
/// </summary>
|
||||||
|
private Bitmap ConvertSampleToBitmap(IMFSample sample)
|
||||||
|
{
|
||||||
|
IMFMediaBuffer? buffer = null;
|
||||||
|
int hr = sample.ConvertToContiguousBuffer(out buffer);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IntPtr pData;
|
||||||
|
int cbMaxLength, cbCurrentLength;
|
||||||
|
|
||||||
|
hr = buffer!.Lock(out pData, out cbMaxLength, out cbCurrentLength);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create bitmap
|
||||||
|
Bitmap bitmap = new Bitmap(_width, _height, PixelFormat.Format32bppRgb);
|
||||||
|
|
||||||
|
// Lock bitmap data
|
||||||
|
Rectangle rect = new Rectangle(0, 0, _width, _height);
|
||||||
|
BitmapData bmpData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int stride = _width * 4; // 4 bytes per pixel (RGB32)
|
||||||
|
|
||||||
|
// Copy data line by line (bottom-up for RGB32)
|
||||||
|
for (int y = 0; y < _height; y++)
|
||||||
|
{
|
||||||
|
IntPtr srcLine = IntPtr.Add(pData, y * stride);
|
||||||
|
IntPtr dstLine = IntPtr.Add(bmpData.Scan0, (_height - 1 - y) * bmpData.Stride);
|
||||||
|
|
||||||
|
// Copy scanline
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(
|
||||||
|
srcLine.ToPointer(),
|
||||||
|
dstLine.ToPointer(),
|
||||||
|
bmpData.Stride,
|
||||||
|
stride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bitmap.UnlockBits(bmpData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
buffer.Unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (buffer != null) Marshal.ReleaseComObject(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (_sourceReader != null)
|
||||||
|
{
|
||||||
|
Marshal.ReleaseComObject(_sourceReader);
|
||||||
|
_sourceReader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MFExtern.MFShutdown();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MFVideoReader()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Helper Classes from VideoAnalyzer
|
||||||
|
|
||||||
|
internal static class MFExtern
|
||||||
|
{
|
||||||
|
private const uint MF_SDK_VERSION = 0x0002;
|
||||||
|
private const uint MF_API_VERSION = 0x0070;
|
||||||
|
internal const uint MF_VERSION = (MF_SDK_VERSION << 16) | MF_API_VERSION;
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFStartup(uint Version, uint dwFlags = 0);
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFAttributesClsid
|
||||||
|
{
|
||||||
|
public static readonly Guid MF_PD_DURATION = new Guid("6c990d33-bb8e-477a-8598-0d5d96fcd88a");
|
||||||
|
public static readonly Guid MF_MT_MAJOR_TYPE = new Guid("48eba18e-f8c9-4687-bf11-0a74c9f96a8f");
|
||||||
|
public static readonly Guid MF_MT_SUBTYPE = new Guid("f7e34c9a-42e8-4714-b74b-cb29d72c35e5");
|
||||||
|
public static readonly Guid MF_MT_FRAME_SIZE = new Guid("1652c33d-d6b2-4012-b834-72030849a37d");
|
||||||
|
public static readonly Guid MF_MT_FRAME_RATE = new Guid("c459a2e8-3d2c-4e44-b132-fee5156c7bb0");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFMediaType
|
||||||
|
{
|
||||||
|
public static readonly Guid Video = new Guid("73646976-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid Audio = new Guid("73647561-0000-0010-8000-00AA00389B71");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
312
Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs
Normal file
312
Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ganimede.VideoProcessing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides video analysis capabilities using native Windows Media Foundation
|
||||||
|
/// NO external dependencies required - uses only Windows built-in APIs
|
||||||
|
/// </summary>
|
||||||
|
public class VideoAnalyzer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Analyzes a video file and returns its metadata using Windows Media Foundation Source Reader
|
||||||
|
/// </summary>
|
||||||
|
public static VideoMetadata Analyze(string videoPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(videoPath))
|
||||||
|
throw new FileNotFoundException($"Video file not found: {videoPath}");
|
||||||
|
|
||||||
|
// Initialize Media Foundation
|
||||||
|
int hr = MFExtern.MFStartup(MFExtern.MF_VERSION, 0);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IntPtr pSourceReader = IntPtr.Zero;
|
||||||
|
IMFSourceReader? sourceReader = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create source reader from URL
|
||||||
|
hr = MFExtern.MFCreateSourceReaderFromURL(videoPath, IntPtr.Zero, out pSourceReader);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
// Convert to interface
|
||||||
|
sourceReader = (IMFSourceReader)Marshal.GetObjectForIUnknown(pSourceReader);
|
||||||
|
|
||||||
|
// Get native media type for first video stream
|
||||||
|
IntPtr pMediaType = IntPtr.Zero;
|
||||||
|
hr = sourceReader!.GetNativeMediaType(
|
||||||
|
MFSourceReaderIndex.FirstVideoStream,
|
||||||
|
0,
|
||||||
|
out pMediaType);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
IMFMediaType? mediaType = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mediaType = (IMFMediaType)Marshal.GetObjectForIUnknown(pMediaType);
|
||||||
|
|
||||||
|
// Get frame size
|
||||||
|
long frameSize;
|
||||||
|
hr = mediaType!.GetUINT64(MFAttributesClsid.MF_MT_FRAME_SIZE, out frameSize);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
int width = (int)(frameSize >> 32);
|
||||||
|
int height = (int)(frameSize & 0xFFFFFFFF);
|
||||||
|
|
||||||
|
// Get frame rate
|
||||||
|
long frameRateRatio;
|
||||||
|
hr = mediaType.GetUINT64(MFAttributesClsid.MF_MT_FRAME_RATE, out frameRateRatio);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
int frameRateNumerator = (int)(frameRateRatio >> 32);
|
||||||
|
int frameRateDenominator = (int)(frameRateRatio & 0xFFFFFFFF);
|
||||||
|
double fps = frameRateDenominator > 0 ? (double)frameRateNumerator / frameRateDenominator : 30.0;
|
||||||
|
|
||||||
|
// Get codec subtype
|
||||||
|
Guid subType;
|
||||||
|
hr = mediaType.GetGUID(MFAttributesClsid.MF_MT_SUBTYPE, out subType);
|
||||||
|
string codecName = GetCodecName(subType);
|
||||||
|
|
||||||
|
// Get duration from presentation attribute
|
||||||
|
IntPtr varDuration = Marshal.AllocHGlobal(16); // PROPVARIANT size
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hr = sourceReader.GetPresentationAttribute(
|
||||||
|
MFSourceReaderIndex.MediaSource,
|
||||||
|
MFAttributesClsid.MF_PD_DURATION,
|
||||||
|
varDuration);
|
||||||
|
|
||||||
|
TimeSpan duration = TimeSpan.Zero;
|
||||||
|
if (hr >= 0)
|
||||||
|
{
|
||||||
|
// Read PROPVARIANT (VT_I8)
|
||||||
|
long durationTicks = Marshal.ReadInt64(varDuration, 8);
|
||||||
|
duration = TimeSpan.FromTicks(durationTicks / 10); // Convert from 100-nanosecond units
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total frames
|
||||||
|
int totalFrames = (int)(duration.TotalSeconds * fps);
|
||||||
|
|
||||||
|
// Get bitrate (approximate from file size)
|
||||||
|
long fileSize = new FileInfo(videoPath).Length;
|
||||||
|
long bitrate = duration.TotalSeconds > 0 ? (long)((fileSize * 8) / duration.TotalSeconds) : 0;
|
||||||
|
|
||||||
|
return new VideoMetadata
|
||||||
|
{
|
||||||
|
Duration = duration,
|
||||||
|
FrameRate = fps,
|
||||||
|
TotalFrames = totalFrames,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
BitRate = bitrate,
|
||||||
|
CodecName = codecName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(varDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (mediaType != null) Marshal.ReleaseComObject(mediaType);
|
||||||
|
if (pMediaType != IntPtr.Zero) Marshal.Release(pMediaType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (sourceReader != null) Marshal.ReleaseComObject(sourceReader);
|
||||||
|
if (pSourceReader != IntPtr.Zero) Marshal.Release(pSourceReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
MFExtern.MFShutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCodecName(Guid subType)
|
||||||
|
{
|
||||||
|
if (subType == MFMediaType.H264) return "H.264";
|
||||||
|
if (subType == MFMediaType.HEVC) return "H.265/HEVC";
|
||||||
|
if (subType == MFMediaType.MP4V) return "MPEG-4";
|
||||||
|
if (subType == MFMediaType.WMV3) return "WMV3";
|
||||||
|
if (subType == MFMediaType.VP80) return "VP8";
|
||||||
|
if (subType == MFMediaType.VP90) return "VP9";
|
||||||
|
if (subType == MFMediaType.AV1) return "AV1";
|
||||||
|
if (subType == MFMediaType.MJPG) return "Motion JPEG";
|
||||||
|
|
||||||
|
return $"Unknown ({subType})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains metadata information about a video file
|
||||||
|
/// </summary>
|
||||||
|
public class VideoMetadata
|
||||||
|
{
|
||||||
|
public TimeSpan Duration { get; set; }
|
||||||
|
public double FrameRate { get; set; }
|
||||||
|
public int TotalFrames { get; set; }
|
||||||
|
public int Width { get; set; }
|
||||||
|
public int Height { get; set; }
|
||||||
|
public long BitRate { get; set; }
|
||||||
|
public string CodecName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Media Foundation P/Invoke Declarations
|
||||||
|
|
||||||
|
internal static class MFExtern
|
||||||
|
{
|
||||||
|
private const uint MF_SDK_VERSION = 0x0002;
|
||||||
|
private const uint MF_API_VERSION = 0x0070;
|
||||||
|
internal const uint MF_VERSION = (MF_SDK_VERSION << 16) | MF_API_VERSION;
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFStartup(uint Version, uint dwFlags = 0);
|
||||||
|
|
||||||
|
[DllImport("mfplat.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFShutdown();
|
||||||
|
|
||||||
|
[DllImport("mfreadwrite.dll", ExactSpelling = true, PreserveSig = true)]
|
||||||
|
internal static extern int MFCreateSourceReaderFromURL(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string pwszURL,
|
||||||
|
IntPtr pAttributes,
|
||||||
|
out IntPtr ppSourceReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("70ae66f2-c809-4e4f-8915-bdcb406b7993")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFSourceReader
|
||||||
|
{
|
||||||
|
[PreserveSig] int GetStreamSelection(int dwStreamIndex, out bool pfSelected);
|
||||||
|
[PreserveSig] int SetStreamSelection(int dwStreamIndex, bool fSelected);
|
||||||
|
[PreserveSig] int GetNativeMediaType(int dwStreamIndex, int dwMediaTypeIndex, out IntPtr ppMediaType);
|
||||||
|
[PreserveSig] int GetCurrentMediaType(int dwStreamIndex, out IntPtr ppMediaType);
|
||||||
|
[PreserveSig] int SetCurrentMediaType(int dwStreamIndex, IntPtr pdwReserved, IntPtr pMediaType);
|
||||||
|
[PreserveSig] int SetCurrentPosition(Guid guidTimeFormat, IntPtr varPosition);
|
||||||
|
[PreserveSig] int ReadSample(int dwStreamIndex, int dwControlFlags, out int pdwActualStreamIndex,
|
||||||
|
out int pdwStreamFlags, out long pllTimestamp, out IntPtr ppSample);
|
||||||
|
[PreserveSig] int Flush(int dwStreamIndex);
|
||||||
|
[PreserveSig] int GetServiceForStream(int dwStreamIndex, Guid guidService, Guid riid, out IntPtr ppvObject);
|
||||||
|
[PreserveSig] int GetPresentationAttribute(int dwStreamIndex, Guid guidAttribute, IntPtr pvarAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFSourceReaderIndex
|
||||||
|
{
|
||||||
|
public const int FirstVideoStream = unchecked((int)0xFFFFFFFC);
|
||||||
|
public const int FirstAudioStream = unchecked((int)0xFFFFFFFD);
|
||||||
|
public const int MediaSource = unchecked((int)0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("2CD2D921-C447-44A7-A13C-4ADABFC247E3")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFAttributes
|
||||||
|
{
|
||||||
|
[PreserveSig] int GetItem(Guid guidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] int GetItemType(Guid guidKey, out ushort pType);
|
||||||
|
[PreserveSig] int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||||
|
[PreserveSig] int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||||
|
[PreserveSig] int GetUINT32(Guid guidKey, out int punValue);
|
||||||
|
[PreserveSig] int GetUINT64(Guid guidKey, out long punValue);
|
||||||
|
[PreserveSig] int GetDouble(Guid guidKey, out double pfValue);
|
||||||
|
[PreserveSig] int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||||
|
[PreserveSig] int GetStringLength(Guid guidKey, out int pcchLength);
|
||||||
|
[PreserveSig] int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||||
|
[PreserveSig] int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||||
|
[PreserveSig] int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||||
|
[PreserveSig] int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||||
|
[PreserveSig] int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||||
|
[PreserveSig] int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||||
|
[PreserveSig] int SetItem(Guid guidKey, IntPtr Value);
|
||||||
|
[PreserveSig] int DeleteItem(Guid guidKey);
|
||||||
|
[PreserveSig] int DeleteAllItems();
|
||||||
|
[PreserveSig] int SetUINT32(Guid guidKey, int unValue);
|
||||||
|
[PreserveSig] int SetUINT64(Guid guidKey, long unValue);
|
||||||
|
[PreserveSig] int SetDouble(Guid guidKey, double fValue);
|
||||||
|
[PreserveSig] int SetGUID(Guid guidKey, Guid guidValue);
|
||||||
|
[PreserveSig] int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||||
|
[PreserveSig] int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||||
|
[PreserveSig] int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||||
|
[PreserveSig] int LockStore();
|
||||||
|
[PreserveSig] int UnlockStore();
|
||||||
|
[PreserveSig] int GetCount(out int pcItems);
|
||||||
|
[PreserveSig] int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] int CopyAllItems(IMFAttributes pDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("44AE0FA8-EA31-4109-8D2E-4CAE4997C555")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IMFMediaType : IMFAttributes
|
||||||
|
{
|
||||||
|
#region IMFAttributes methods
|
||||||
|
[PreserveSig] new int GetItem(Guid guidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] new int GetItemType(Guid guidKey, out ushort pType);
|
||||||
|
[PreserveSig] new int CompareItem(Guid guidKey, IntPtr Value, out bool pbResult);
|
||||||
|
[PreserveSig] new int Compare(IMFAttributes pTheirs, int MatchType, out bool pbResult);
|
||||||
|
[PreserveSig] new int GetUINT32(Guid guidKey, out int punValue);
|
||||||
|
[PreserveSig] new int GetUINT64(Guid guidKey, out long punValue);
|
||||||
|
[PreserveSig] new int GetDouble(Guid guidKey, out double pfValue);
|
||||||
|
[PreserveSig] new int GetGUID(Guid guidKey, out Guid pguidValue);
|
||||||
|
[PreserveSig] new int GetStringLength(Guid guidKey, out int pcchLength);
|
||||||
|
[PreserveSig] new int GetString(Guid guidKey, IntPtr pwszValue, int cchBufSize, IntPtr pcchLength);
|
||||||
|
[PreserveSig] new int GetAllocatedString(Guid guidKey, out IntPtr ppwszValue, out int pcchLength);
|
||||||
|
[PreserveSig] new int GetBlobSize(Guid guidKey, out int pcbBlobSize);
|
||||||
|
[PreserveSig] new int GetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize, IntPtr pcbBlobSize);
|
||||||
|
[PreserveSig] new int GetAllocatedBlob(Guid guidKey, out IntPtr ppBuf, out int pcbSize);
|
||||||
|
[PreserveSig] new int GetUnknown(Guid guidKey, Guid riid, out IntPtr ppv);
|
||||||
|
[PreserveSig] new int SetItem(Guid guidKey, IntPtr Value);
|
||||||
|
[PreserveSig] new int DeleteItem(Guid guidKey);
|
||||||
|
[PreserveSig] new int DeleteAllItems();
|
||||||
|
[PreserveSig] new int SetUINT32(Guid guidKey, int unValue);
|
||||||
|
[PreserveSig] new int SetUINT64(Guid guidKey, long unValue);
|
||||||
|
[PreserveSig] new int SetDouble(Guid guidKey, double fValue);
|
||||||
|
[PreserveSig] new int SetGUID(Guid guidKey, Guid guidValue);
|
||||||
|
[PreserveSig] new int SetString(Guid guidKey, [MarshalAs(UnmanagedType.LPWStr)] string wszValue);
|
||||||
|
[PreserveSig] new int SetBlob(Guid guidKey, IntPtr pBuf, int cbBufSize);
|
||||||
|
[PreserveSig] new int SetUnknown(Guid guidKey, [MarshalAs(UnmanagedType.IUnknown)] object pUnknown);
|
||||||
|
[PreserveSig] new int LockStore();
|
||||||
|
[PreserveSig] new int UnlockStore();
|
||||||
|
[PreserveSig] new int GetCount(out int pcItems);
|
||||||
|
[PreserveSig] new int GetItemByIndex(int unIndex, out Guid pguidKey, IntPtr pValue);
|
||||||
|
[PreserveSig] new int CopyAllItems(IMFAttributes pDest);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[PreserveSig] int GetMajorType(out Guid pguidMajorType);
|
||||||
|
[PreserveSig] int IsCompressedFormat(out bool pfCompressed);
|
||||||
|
[PreserveSig] int IsEqual(IMFMediaType pIMediaType, out uint pdwFlags);
|
||||||
|
[PreserveSig] int GetRepresentation(Guid guidRepresentation, out IntPtr ppvRepresentation);
|
||||||
|
[PreserveSig] int FreeRepresentation(Guid guidRepresentation, IntPtr pvRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFAttributesClsid
|
||||||
|
{
|
||||||
|
public static readonly Guid MF_PD_DURATION = new Guid("6c990d33-bb8e-477a-8598-0d5d96fcd88a");
|
||||||
|
public static readonly Guid MF_MT_MAJOR_TYPE = new Guid("48eba18e-f8c9-4687-bf11-0a74c9f96a8f");
|
||||||
|
public static readonly Guid MF_MT_SUBTYPE = new Guid("f7e34c9a-42e8-4714-b74b-cb29d72c35e5");
|
||||||
|
public static readonly Guid MF_MT_FRAME_SIZE = new Guid("1652c33d-d6b2-4012-b834-72030849a37d");
|
||||||
|
public static readonly Guid MF_MT_FRAME_RATE = new Guid("c459a2e8-3d2c-4e44-b132-fee5156c7bb0");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MFMediaType
|
||||||
|
{
|
||||||
|
public static readonly Guid Video = new Guid("73646976-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid Audio = new Guid("73647561-0000-0010-8000-00AA00389B71");
|
||||||
|
|
||||||
|
// Video formats
|
||||||
|
public static readonly Guid H264 = new Guid("34363248-0000-0010-8000-00aa00389b71");
|
||||||
|
public static readonly Guid HEVC = new Guid("43564548-0000-0010-8000-00aa00389b71");
|
||||||
|
public static readonly Guid MP4V = new Guid("5634504D-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid WMV3 = new Guid("33564D57-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid VP80 = new Guid("30385056-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid VP90 = new Guid("30395056-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid AV1 = new Guid("31305641-0000-0010-8000-00AA00389B71");
|
||||||
|
public static readonly Guid MJPG = new Guid("47504A4D-0000-0010-8000-00AA00389B71");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace Ganimede.ViewModels
|
|
||||||
{
|
|
||||||
// ViewModel principale e futuri ViewModel
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace Ganimede.Views
|
|
||||||
{
|
|
||||||
// Views aggiuntive se necessario
|
|
||||||
}
|
|
||||||
@@ -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,9 +122,13 @@ namespace Ganimede.Windows
|
|||||||
JobNamingPreviewText.Text = "Video1_000001.png (default)";
|
JobNamingPreviewText.Text = "Video1_000001.png (default)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
JobNamingPreviewText.Text = "Video1_000001.png";
|
System.Diagnostics.Debug.WriteLine($"[ERROR] UpdateJobNamingPreview: {ex.Message}");
|
||||||
|
if (JobNamingPreviewText != null)
|
||||||
|
{
|
||||||
|
JobNamingPreviewText.Text = "Video1_000001.png";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Impostazioni" Height="600" Width="640"
|
Title="Impostazioni" Height="550" Width="640"
|
||||||
Background="#1E2228" WindowStartupLocation="CenterOwner">
|
Background="#1E2228" WindowStartupLocation="CenterOwner">
|
||||||
<Grid Margin="22">
|
<Grid Margin="22">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -19,24 +19,6 @@
|
|||||||
<!-- Settings Content -->
|
<!-- Settings Content -->
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<!-- FFmpeg Settings -->
|
|
||||||
<GroupBox Header="Configurazione FFmpeg" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
|
||||||
<StackPanel Margin="12">
|
|
||||||
<TextBlock Text="Cartella Binari FFmpeg:" Foreground="#CCC" Margin="0,0,0,6"/>
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBox x:Name="FFmpegPathTextBox" Grid.Column="0" Height="34" VerticalContentAlignment="Center"
|
|
||||||
Background="#333" Foreground="White" BorderBrush="#555" Margin="0,0,10,0"/>
|
|
||||||
<Button x:Name="BrowseFFmpegButton" Grid.Column="1" Content="Sfoglia" Width="90" Height="34"
|
|
||||||
Click="BrowseFFmpegButton_Click"/>
|
|
||||||
</Grid>
|
|
||||||
<TextBlock x:Name="FFmpegStatusText" Foreground="#AAA" FontSize="12" Margin="0,6,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- Output Settings -->
|
<!-- Output Settings -->
|
||||||
<GroupBox Header="Output" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
<GroupBox Header="Output" Foreground="White" BorderBrush="#444" Margin="0,0,0,18">
|
||||||
<StackPanel Margin="12">
|
<StackPanel Margin="12">
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace Ganimede.Windows
|
|||||||
|
|
||||||
private void LoadSettings()
|
private void LoadSettings()
|
||||||
{
|
{
|
||||||
FFmpegPathTextBox.Text = Settings.Default.FFmpegBinFolder;
|
|
||||||
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
|
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
|
||||||
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
|
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
|
||||||
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
|
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
|
||||||
@@ -55,8 +54,6 @@ namespace Ganimede.Windows
|
|||||||
default:
|
default:
|
||||||
GetDefaultModeRadio("DefaultModeFullRadio")!.IsChecked = true; break;
|
GetDefaultModeRadio("DefaultModeFullRadio")!.IsChecked = true; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateFFmpegStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSelectedDefaultExtractionMode()
|
private string GetSelectedDefaultExtractionMode()
|
||||||
@@ -68,43 +65,6 @@ namespace Ganimede.Windows
|
|||||||
|
|
||||||
private bool GetSingleFrameUseSubfolder() => GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
|
private bool GetSingleFrameUseSubfolder() => GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
|
||||||
|
|
||||||
private void UpdateFFmpegStatus()
|
|
||||||
{
|
|
||||||
var path = FFmpegPathTextBox.Text;
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
FFmpegStatusText.Text = "Nessun percorso specificato";
|
|
||||||
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Orange);
|
|
||||||
}
|
|
||||||
else if (ValidateFFMpegPath(path))
|
|
||||||
{
|
|
||||||
FFmpegStatusText.Text = "? FFmpeg valido";
|
|
||||||
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGreen);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FFmpegStatusText.Text = "? Binari FFmpeg non trovati";
|
|
||||||
FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateFFMpegPath(string path)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(path)) return false;
|
|
||||||
return File.Exists(Path.Combine(path, "ffmpeg.exe")) && File.Exists(Path.Combine(path, "ffprobe.exe"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BrowseFFmpegButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella binari FFmpeg", ShowNewFolderButton = false };
|
|
||||||
if (!string.IsNullOrEmpty(FFmpegPathTextBox.Text)) dialog.SelectedPath = FFmpegPathTextBox.Text;
|
|
||||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
||||||
{
|
|
||||||
FFmpegPathTextBox.Text = dialog.SelectedPath;
|
|
||||||
UpdateFFmpegStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
|
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella output predefinita", ShowNewFolderButton = true };
|
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella output predefinita", ShowNewFolderButton = true };
|
||||||
@@ -119,7 +79,6 @@ namespace Ganimede.Windows
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Settings.Default.FFmpegBinFolder = FFmpegPathTextBox.Text;
|
|
||||||
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
|
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
|
||||||
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
|
||||||
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
|
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;
|
||||||
|
|||||||
Reference in New Issue
Block a user