Skip to content

Instantly share code, notes, and snippets.

@paulyuk
Last active February 5, 2025 20:00
Show Gist options
  • Save paulyuk/6d60fa73494b08928cdae80332a59246 to your computer and use it in GitHub Desktop.
Save paulyuk/6d60fa73494b08928cdae80332a59246 to your computer and use it in GitHub Desktop.
Porche ai model challenge
// 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