Skip to content

Commit dffb898

Browse files
committed
Merge branch 'dev' into experimental/new-capture
2 parents b91285b + 979dd04 commit dffb898

File tree

1 file changed

+146
-5
lines changed

1 file changed

+146
-5
lines changed

Runtime/Scripts/VideoEncoder.cs

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@
44
using System.Collections.Generic;
55
using UnityEngine;
66
using System.Threading.Tasks;
7+
using System; // Added for Guid
78

89
namespace 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

Comments
 (0)