Last active
February 5, 2025 20:00
-
-
Save paulyuk/6d60fa73494b08928cdae80332a59246 to your computer and use it in GitHub Desktop.
Porche ai model challenge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Project: DashcamImporter (Console Application, will be converted to Tray App) | |
// Target Framework: net9.0-windows | |
// NuGet Packages: | |
// - Microsoft.Extensions.Hosting | |
// - Microsoft.Extensions.Configuration | |
// - Microsoft.Extensions.Logging | |
// - System.IO.FileSystem.Watcher (Implicitly part of .NET) | |
// - WindowsAPICodePack-Shell (For taskbar notifications - from NuGet) | |
// - NAudio (For MP4 demuxing and audio/video handling - from NuGet) | |
// Note: Due to complexity, this provides a robust *structure* and implementation *guidance*. | |
// Complete low-level MP4 parsing & stream recombination is omitted | |
// for brevity, and NAudio usage is outlined. Error handling, file locking, | |
// and edge cases are partially addressed but need thorough real-world testing. | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Configuration; | |
using System.Windows.Forms; | |
using System.Drawing; | |
using Microsoft.WindowsAPICodePack.Shell; | |
using Microsoft.WindowsAPICodePack.Taskbar; | |
using NAudio.Wave; // For audio handling. | |
using NAudio.CoreAudioApi; //Potentially needed for lower-level audio tasks. | |
using NAudio.MediaFoundation; //For handling MP4 (H.264/AAC) | |
using System.Threading; | |
namespace DashcamImporter | |
{ | |
public class Settings | |
{ | |
public string SDCardPath { get; set; } = ""; | |
public string DestinationDirectory { get; set; } = ""; | |
public HashSet<string> ProcessedFiles { get; set; } = new HashSet<string>(); | |
} | |
public class Program : ApplicationContext | |
{ | |
private NotifyIcon _trayIcon; | |
private IHost _host; | |
private Settings _settings; | |
private FileSystemWatcher _watcher; | |
private ILogger<Program> _logger; | |
private bool _isProcessing = false; | |
private string _processedFilePath; //Path to the file where we remember processed clips. | |
public Program() | |
{ | |
// Initialize settings and logging (before starting the host) | |
LoadSettings(); | |
_host = Host.CreateDefaultBuilder() | |
.ConfigureLogging(logging => | |
{ | |
logging.AddConsole(); | |
logging.SetMinimumLevel(LogLevel.Information); //Adjust as needed. | |
}) | |
.Build(); | |
_logger = _host.Services.GetRequiredService<ILogger<Program>>(); | |
// Tray Icon Setup | |
_trayIcon = new NotifyIcon() | |
{ | |
Icon = SystemIcons.Application, // Replace with your own icon. | |
Visible = true, | |
ContextMenuStrip = new ContextMenuStrip(), | |
Text = "Dashcam Importer" | |
}; | |
_trayIcon.ContextMenuStrip.Items.Add("Settings", null, OnSettingsClicked); | |
_trayIcon.ContextMenuStrip.Items.Add("Exit", null, OnExitClicked); | |
_trayIcon.DoubleClick += OnSettingsClicked; // Double-click to open settings. | |
// File System Watcher Setup | |
SetupFileSystemWatcher(); | |
//Check if the SDCard is already inserted. | |
CheckAndProcessSDCard(); | |
} | |
private void LoadSettings() | |
{ | |
// Use a simple JSON file for settings. | |
var configBuilder = new ConfigurationBuilder() | |
.SetBasePath(Directory.GetCurrentDirectory()) | |
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); //optional = true means the app doesn't crash, if the file doesn't exist. | |
IConfiguration configuration = configBuilder.Build(); | |
_processedFilePath = Path.Combine(Directory.GetCurrentDirectory(), "processed_files.txt"); | |
_settings = configuration.GetSection("Settings").Get<Settings>() ?? new Settings(); | |
if (File.Exists(_processedFilePath)) | |
{ | |
_settings.ProcessedFiles = new HashSet<string>(File.ReadAllLines(_processedFilePath)); | |
} | |
} | |
private void SaveSettings() | |
{ | |
// Serialize settings back to JSON. | |
var config = new ConfigurationBuilder() | |
.SetBasePath(Directory.GetCurrentDirectory()) | |
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) | |
.Build(); | |
config["Settings:SDCardPath"] = _settings.SDCardPath; | |
config["Settings:DestinationDirectory"] = _settings.DestinationDirectory; | |
//We don't use config to store the list of processed files, because this file is user-editable. | |
// Save using System.Text.Json (more modern) | |
string jsonString = System.Text.Json.JsonSerializer.Serialize(_settings, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); | |
File.WriteAllText("appsettings.json", jsonString); | |
File.WriteAllLines(_processedFilePath, _settings.ProcessedFiles); | |
} | |
private void SetupFileSystemWatcher() | |
{ | |
if (string.IsNullOrEmpty(_settings.SDCardPath) || !Directory.Exists(_settings.SDCardPath)) | |
{ | |
_logger.LogWarning("SD Card path is not set or invalid. Please configure in settings."); | |
return; // Don't start the watcher if the path is invalid. | |
} | |
_watcher = new FileSystemWatcher(_settings.SDCardPath) | |
{ | |
// Watch for changes in directory (drive insertion/removal). | |
NotifyFilter = NotifyFilters.Attributes | NotifyFilters.DirectoryName | NotifyFilters.LastWrite | NotifyFilters.FileName, | |
EnableRaisingEvents = true | |
}; | |
_watcher.Created += OnDriveChanged; | |
_watcher.Deleted += OnDriveChanged; | |
_watcher.Changed += OnDriveChanged; | |
_watcher.Error += OnWatcherError; | |
_watcher.IncludeSubdirectories = true; // Essential to watch "driving" and "parking" | |
//Filter for video files to avoid unneccessary event triggers. | |
_watcher.Filter = "*.mp4"; | |
} | |
private void OnWatcherError(object sender, ErrorEventArgs e) | |
{ | |
_logger.LogError($"FileSystemWatcher Error: {e.GetException()}"); | |
// Consider restarting the watcher or alerting the user. | |
_watcher.EnableRaisingEvents = false; | |
Thread.Sleep(5000); // Wait 5 seconds | |
SetupFileSystemWatcher(); | |
} | |
private void OnDriveChanged(object sender, FileSystemEventArgs e) | |
{ | |
// Debounce: Multiple events fire rapidly; use a simple timer. | |
// This is a quick and dirty solution, which may fail in some edge cases. | |
// For higher robustness, use System.Reactive and its Throttle method, | |
// but this adds another dependency. | |
if (_isProcessing) return; | |
_isProcessing = true; | |
Task.Delay(1000).ContinueWith(_ => // Wait 1 second for changes to settle. | |
{ | |
_isProcessing = false; | |
CheckAndProcessSDCard(); | |
}); | |
} | |
private void CheckAndProcessSDCard() | |
{ | |
try | |
{ | |
if (!Directory.Exists(_settings.SDCardPath)) | |
{ | |
_logger.LogInformation("SD Card not found."); | |
return; | |
} | |
_logger.LogInformation("SD Card detected. Processing..."); | |
ShowNotification("Dashcam Importer", "SD Card detected. Processing started."); | |
ProcessVideos(_settings.SDCardPath, _settings.DestinationDirectory); | |
ShowNotification("Dashcam Importer", "Processing complete."); | |
} | |
catch (Exception ex) | |
{ | |
_logger.LogError(ex, "Error during SD card processing."); | |
ShowNotification("Dashcam Importer", <span class="math-inline">"Error\: \{ex\.Message\}",ToolTipIcon\.Error\); | |
\} | |
\} | |
private void ProcessVideos\(string sdCardPath, string destinationDir\) | |
\{ | |
if \(\!Directory\.Exists\(destinationDir\)\) | |
\{ | |
Directory\.CreateDirectory\(destinationDir\); | |
\_logger\.LogInformation\(</span>"Created destination directory: {destinationDir}"); | |
} | |
ProcessVideoDirectory(Path.Combine(sdCardPath, "driving"), destinationDir); | |
ProcessVideoDirectory(Path.Combine(sdCardPath, "parking"), destinationDir); | |
_logger.LogInformation("Finished processing all videos."); | |
} | |
private void ProcessVideoDirectory(string directoryPath, string destinationDir) | |
{ | |
if (!Directory.Exists(directoryPath)) | |
{ | |
_logger.LogWarning(<span class="math-inline">"Directory not found\: \{directoryPath\}"\); | |
return; | |
\} | |
\_logger\.LogInformation\(</span>"Processing directory: {directoryPath}"); | |
// 1. Get all MP4 files, ordered by creation time. | |
var files = Directory.GetFiles(directoryPath, "*.mp4") | |
.Select(f => new FileInfo(f)) | |
.OrderBy(fi => fi.CreationTime) | |
.ToList(); | |
// 2. Group consecutive files. | |
var groups = new List<List<FileInfo>>(); | |
List<FileInfo> currentGroup = new List<FileInfo>(); | |
for (int i = 0; i < files.Count; i++) | |
{ | |
currentGroup.Add(files[i]); | |
// Check for gap: If next file's creation time is too far, start a new group. | |
if (i + 1 < files.Count) | |
{ | |
TimeSpan difference = files[i + 1].CreationTime - files[i].CreationTime; | |
// Define "consecutive" (e.g., within 5 seconds). Adjust as needed. | |
if (difference.TotalSeconds > 5) | |
{ | |
groups.Add(currentGroup); | |
currentGroup = new List<FileInfo>(); | |
} | |
} | |
} | |
if(currentGroup.Count > 0) | |
{ | |
groups.Add(currentGroup); // Add the last group. | |
} | |
// 3. Process each group. | |
foreach (var group in groups) | |
{ | |
ProcessGroup(group, destinationDir); | |
} | |
} | |
private void ProcessGroup(List<FileInfo> group, string destinationDir) | |
{ | |
//Create a group hash, based on the names and order of files in the group. | |
string groupHash = CalculateGroupHash(group); | |
if (_settings.ProcessedFiles.Contains(groupHash)) | |
{ | |
_logger.LogInformation(<span class="math-inline">"Skipping already processed group\: \{group\[0\]\.Name\} and \{group\.Count\-1\} others\."\); | |
return; | |
\} | |
\_logger\.LogInformation\(</span>"Processing group: {group[0].Name} and {group.Count - 1} others."); | |
// Use NAudio.MediaFoundation to demux the MP4. | |
// This requires careful stream handling and is the MOST COMPLEX part. | |
List<string> frontStreams = new List<string>(); | |
List<string> backStreams = new List<string>(); | |
try | |
{ | |
foreach (var fileInfo in group) | |
{ | |
//Simplified. In a real scenario, you would analyze the MP4 structure and identify the tracks. | |
//You can use MediaFoundationReader and then inspect its WaveFormat. | |
//It may also be, that your dashcam stores two video tracks. | |
using (var reader = new MediaFoundationReader(fileInfo.FullName)) | |
{ | |
//For this example, we assume one video (H.264) and one audio (AAC) track. | |
if (reader.WaveFormat.Encoding == WaveFormatEncoding.Mpeg4 || reader.WaveFormat.Encoding == WaveFormatEncoding.H264 ) // Check if it's video (simplified) | |
{ | |
// Write out the video track (requires decoding and re-encoding, omitted for brevity). | |
// Create temp files, to be concatenated later. | |
string frontStreamTempFile = Path.Combine(_settings.DestinationDirectory, $"{Path.GetFileNameWithoutExtension(fileInfo.Name)}_front_temp_{frontStreams.Count}.h264"); //Or .mp4, depending on encoder. | |
string backStreamTempFile = Path.Combine(_settings.DestinationDirectory, $"{Path.GetFileNameWithoutExtension(fileInfo.Name)}_back_temp_{backStreams.Count}.h264"); //Or .mp4 | |
// PSEUDO-CODE for separating front/back (YOU NEED TO IMPLEMENT THIS): | |
// This part is HIGHLY dependent on your dashcam's specific MP4 format. | |
// You *might* be able to separate by simply alternating chunks, or you | |
// *might* need to inspect the MP4 structure (atoms/boxes) to find | |
// different video tracks (if they exist) or different NAL units | |
// within a single track. This often involves looking at sample flags | |
// or SEI messages within the H.264 stream. | |
// Assume you have byte[] frontData and byte[] backData for each chunk. | |
// File.WriteAllBytes(frontStreamTempFile, frontData); | |
// File.WriteAllBytes(backStreamTempFile, backData); | |
frontStreams.Add(frontStreamTempFile); | |
backStreams.Add(backStreamTempFile); | |
} | |
} | |
} | |
//Concatenate streams. | |
string baseFileName = Path.GetFileNameWithoutExtension(group[0].Name); | |
string frontOutputFile = Path.Combine(destinationDir, $"{baseFileName}_front.mp4"); | |
string backOutputFile = Path.Combine(destinationDir, $"{baseFileName}_back.mp4"); | |
ConcatenateFiles(frontStreams, frontOutputFile); | |
ConcatenateFiles(backStreams, backOutputFile); | |
//Add group to processed files. | |
_settings.ProcessedFiles.Add(groupHash); | |
SaveSettings(); | |
} | |
catch(Exception ex) | |
{ | |
_logger.LogError(ex, <span class="math-inline">"Error processing group starting with \{group\[0\]\.FullName\}\."\); | |
//Clean up temp files, if an error occurred\. | |
\} | |
finally | |
\{ | |
// Clean up temporary files\. | |
foreach \(var tempFile in frontStreams\) | |
\{ | |
if \(File\.Exists\(tempFile\)\) File\.Delete\(tempFile\); | |
\} | |
foreach \(var tempFile in backStreams\) | |
\{ | |
if \(File\.Exists\(tempFile\)\) File\.Delete\(tempFile\); | |
\} | |
\} | |
\} | |
private string CalculateGroupHash\(List<FileInfo\> group\) | |
\{ | |
//Concatenate the filenames\. | |
string combinedNames \= string\.Join\("\|", group\.Select\(f \=\> f\.Name\)\); | |
//Compute hash | |
<7\>using \(var sha256 \= System\.Security\.Cryptography\.SHA256\.Create\(\)\) | |
\{ | |
byte\[\] hashBytes \= sha256\.ComputeHash\(System\.Text\.Encoding\.UTF8\.GetBytes\(combinedNames\)\);</7\> | |
return Convert\.ToBase64String\(hashBytes\); // Or BitConverter\.ToString\(hashBytes\) | |
\} | |
\} | |
private void ConcatenateFiles\(List<string\> inputFiles, string outputFile\) | |
\{ | |
//For simplicity, we just use File\.Copy to append to the file\. | |
//In a production application, you would use the correct container format \(e\.g\., MP4\) | |
//and write the video and audio data to it, handling timing and other details correctly\. | |
using \(var outputStream \= File\.Create\(outputFile\)\) | |
\{ | |
foreach \(var inputFile in inputFiles\) | |
\{ | |
using \(var inputStream \= File\.OpenRead\(inputFile\)\) | |
\{ | |
inputStream\.CopyTo\(outputStream\); | |
\} | |
\} | |
\} | |
\_logger\.LogInformation\(</span>"Created combined file: {outputFile}"); | |
} | |
private void ShowNotification(string title, string message, ToolTipIcon icon = ToolTipIcon.Info) | |
{ | |
_trayIcon.ShowBalloonTip(5000, title, message, icon); | |
} | |
private void OnSettingsClicked(object? sender, EventArgs e) | |
{ | |
// Show the settings dialog (FormSettings). | |
FormSettings settingsForm = new FormSettings(_settings, this); | |
settingsForm.ShowDialog(); | |
} | |
private void OnExitClicked(object sender, EventArgs e) | |
{ | |
_trayIcon.Visible = false; // Hide the icon before exiting. | |
_host.StopAsync().Wait(); // Gracefully shut down. | |
_host.Dispose(); | |
Application.Exit(); | |
} | |
//Called by the Settings form, when settings were saved. | |
public void OnSettingsSaved() | |
{ | |
//Restart the FileSystemWatcher. | |
if(_watcher != null) | |
{ | |
_watcher.EnableRaisingEvents = false; | |
_watcher.Dispose(); | |
} | |
SetupFileSystemWatcher(); | |
//Reprocess | |
CheckAndProcessSDCard(); | |
} | |
[STAThread] | |
public static void Main(string[] args) | |
{ | |
Application.SetHighDpiMode(HighDpiMode.SystemAware); | |
Application.EnableVisualStyles(); | |
Application.SetCompatibleTextRenderingDefault(false); | |
Application.Run(new Program()); | |
} | |
} | |
public class FormSettings : Form | |
{ | |
private Settings _settings; | |
private TextBox _txtSDCardPath; | |
private TextBox _txtDestinationDir; | |
private Button _btnSave; | |
private Button _btnBrowseSDCard; | |
private Button _btnBrowseDestination; | |
private Program _parent; | |
public FormSettings(Settings settings, Program parent) | |
{ | |
_settings = settings; | |
_parent = parent; | |
InitializeComponents(); | |
LoadSettingsIntoUI(); | |
} | |
private void InitializeComponents() | |
{ | |
// Basic UI setup (using Windows Forms Designer is recommended). | |
this. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment