44using System . Collections . Generic ;
55using UnityEngine ;
66using System . Threading . Tasks ;
7+ using System ; // Added for Guid
78
89namespace BetaHub
910{
11+ /// <summary>
12+ /// VideoEncoder handles video recording and encoding using FFmpeg.
13+ ///
14+ /// Bug Fix (File Access Conflicts):
15+ /// - Uses unique instance directories to prevent conflicts between multiple VideoEncoder instances
16+ /// - Implements retry logic with exponential backoff for file deletion operations
17+ /// - Gracefully handles IOException when files are still in use by FFmpeg processes
18+ /// - Properly cleans up instance directories when empty
19+ ///
20+ /// This fixes the issue where scene reloads would cause IOException spam due to
21+ /// multiple VideoEncoder instances trying to access the same segment files.
22+ /// </summary>
1023 public class VideoEncoder
1124 {
1225 private IProcessWrapper ffmpegProcess ;
@@ -25,6 +38,9 @@ public class VideoEncoder
2538 private float frameInterval ;
2639
2740 private bool debugMode ;
41+
42+ // Unique instance identifier to prevent conflicts between multiple instances
43+ private readonly string instanceId ;
2844
2945 // if set to true, the encoding thread will pause adding new frames
3046 public bool IsPaused { get ; set ; }
@@ -36,16 +52,19 @@ public class VideoEncoder
3652 private volatile bool _stopRequest = false ;
3753 private volatile bool _stopRequestHandled = false ;
3854
39- public VideoEncoder ( int width , int height , int frameRate , int recordingDurationSeconds , string outputDir = "Recording" )
55+ public VideoEncoder ( int width , int height , int frameRate , int recordingDurationSeconds , string baseOutputDir = "Recording" )
4056 {
4157 this . width = width ;
4258 this . height = height ;
4359 this . frameRate = frameRate ;
44- this . outputDir = outputDir ;
60+
61+ // Create unique instance identifier and output directory
62+ this . instanceId = Guid . NewGuid ( ) . ToString ( "N" ) . Substring ( 0 , 8 ) ; // Use first 8 characters of GUID
63+ this . outputDir = Path . Combine ( baseOutputDir , instanceId ) ;
4564 this . outputPathPattern = Path . Combine ( outputDir , "segment_%03d.mp4" ) ;
4665
4766 #if BETAHUB_DEBUG
48- UnityEngine . Debug . Log ( "Video output directory: " + outputDir ) ;
67+ UnityEngine . Debug . Log ( $ "Video output directory: { outputDir } (instance: { instanceId } )" ) ;
4968 #endif
5069
5170 Directory . CreateDirectory ( outputDir ) ;
@@ -66,9 +85,16 @@ public void Dispose()
6685 if ( ffmpegProcess != null && ffmpegProcess . IsRunning ( ) )
6786 {
6887 SendStopRequestAndWait ( ) ;
88+
89+ // Give the process a moment to fully release file handles
90+ System . Threading . Thread . Sleep ( 100 ) ;
6991 }
7092
71- RemoveAllSegments ( ) ;
93+ // Clean up segments with retry logic for better file access handling
94+ RemoveAllSegmentsWithRetry ( ) ;
95+
96+ // Clean up the unique instance directory if it's empty
97+ CleanupInstanceDirectory ( ) ;
7298 }
7399
74100 public void StartEncoding ( )
@@ -317,6 +343,9 @@ private void RemoveAllSegments()
317343 // cleanups only the old segments, keeping the ones with the latest segment numbers
318344 private void CleanupSegments ( )
319345 {
346+ if ( ! Directory . Exists ( outputDir ) )
347+ return ;
348+
320349 var directoryInfo = new DirectoryInfo ( outputDir ) ;
321350
322351 var filesToDelete = directoryInfo . GetFiles ( "segment_*.mp4" )
@@ -326,7 +355,40 @@ private void CleanupSegments()
326355
327356 foreach ( var file in filesToDelete )
328357 {
329- file . Delete ( ) ;
358+ // Retry logic for file deletion to handle file access conflicts
359+ int retryCount = 0 ;
360+ const int maxRetries = 3 ;
361+ const int retryDelayMs = 25 ;
362+
363+ while ( retryCount < maxRetries )
364+ {
365+ try
366+ {
367+ file . Delete ( ) ;
368+ break ; // Success, exit retry loop
369+ }
370+ catch ( IOException ex ) when ( ex . Message . Contains ( "being used by another process" ) )
371+ {
372+ retryCount ++ ;
373+ if ( retryCount >= maxRetries )
374+ {
375+ #if BETAHUB_DEBUG
376+ UnityEngine . Debug . LogWarning ( $ "Could not delete old segment file { file . Name } after { maxRetries } attempts. File may still be in use.") ;
377+ #endif
378+ }
379+ else
380+ {
381+ System . Threading . Thread . Sleep ( retryDelayMs * retryCount ) ;
382+ }
383+ }
384+ catch ( System . Exception ex )
385+ {
386+ #if BETAHUB_DEBUG
387+ UnityEngine . Debug . LogError ( $ "Unexpected error deleting old segment file { file . Name } : { ex . Message } ") ;
388+ #endif
389+ break ; // Don't retry for unexpected errors
390+ }
391+ }
330392 }
331393 }
332394
@@ -525,6 +587,85 @@ private static string GetFfmpegPath()
525587
526588 return path ;
527589 }
590+
591+ private void RemoveAllSegmentsWithRetry ( )
592+ {
593+ if ( ! Directory . Exists ( outputDir ) )
594+ return ;
595+
596+ var directoryInfo = new DirectoryInfo ( outputDir ) ;
597+ var files = directoryInfo . GetFiles ( "segment_*.mp4" ) ;
598+
599+ foreach ( var file in files )
600+ {
601+ // Retry logic for file deletion to handle file access conflicts
602+ int retryCount = 0 ;
603+ const int maxRetries = 5 ;
604+ const int retryDelayMs = 50 ;
605+
606+ while ( retryCount < maxRetries )
607+ {
608+ try
609+ {
610+ file . Delete ( ) ;
611+ break ; // Success, exit retry loop
612+ }
613+ catch ( IOException ex ) when ( ex . Message . Contains ( "being used by another process" ) )
614+ {
615+ retryCount ++ ;
616+ if ( retryCount >= maxRetries )
617+ {
618+ UnityEngine . Debug . LogWarning ( $ "Could not delete segment file { file . Name } after { maxRetries } attempts. File may still be in use. Error: { ex . Message } ") ;
619+ }
620+ else
621+ {
622+ #if BETAHUB_DEBUG
623+ UnityEngine . Debug . Log ( $ "Retrying deletion of { file . Name } (attempt { retryCount } /{ maxRetries } )") ;
624+ #endif
625+ System . Threading . Thread . Sleep ( retryDelayMs * retryCount ) ; // Exponential backoff
626+ }
627+ }
628+ catch ( System . Exception ex )
629+ {
630+ UnityEngine . Debug . LogError ( $ "Unexpected error deleting segment file { file . Name } : { ex . Message } ") ;
631+ break ; // Don't retry for unexpected errors
632+ }
633+ }
634+ }
635+ }
636+
637+ private void CleanupInstanceDirectory ( )
638+ {
639+ try
640+ {
641+ if ( Directory . Exists ( outputDir ) )
642+ {
643+ // Check if directory is empty (no files left)
644+ var remainingFiles = Directory . GetFiles ( outputDir ) ;
645+ var remainingDirs = Directory . GetDirectories ( outputDir ) ;
646+
647+ if ( remainingFiles . Length == 0 && remainingDirs . Length == 0 )
648+ {
649+ Directory . Delete ( outputDir ) ;
650+ #if BETAHUB_DEBUG
651+ UnityEngine . Debug . Log ( $ "Cleaned up empty instance directory: { outputDir } ") ;
652+ #endif
653+ }
654+ else
655+ {
656+ #if BETAHUB_DEBUG
657+ UnityEngine . Debug . Log ( $ "Instance directory not empty, keeping: { outputDir } (files: { remainingFiles . Length } , dirs: { remainingDirs . Length } )") ;
658+ #endif
659+ }
660+ }
661+ }
662+ catch ( System . Exception ex )
663+ {
664+ #if BETAHUB_DEBUG
665+ UnityEngine . Debug . LogWarning ( $ "Could not clean up instance directory { outputDir } : { ex . Message } ") ;
666+ #endif
667+ }
668+ }
528669 }
529670
530671 public class CircularBuffer < T >
0 commit comments