diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs
index e41526b..0532887 100644
--- a/Core/RenderManager.cs
+++ b/Core/RenderManager.cs
@@ -226,6 +226,11 @@ internal class RenderManager : MonoBehaviour
private GameObject? _debugMonitorObj;
private GameObject? _escapeHandlerObj;
internal string? CurrentOutPath;
+
+ // Output file of the current render that is not finished yet.
+ // Set just before the encoder opens the file, cleared by the render thread after the encoder finished the file (End).
+ // Only this file is deleted when a render is cancelled, fails or the game exits (DeleteIncompleteOutputFile).
+ private string? _incompleteOutputPath;
private bool _renderSetup;
private void Awake()
@@ -286,6 +291,32 @@ internal class RenderManager : MonoBehaviour
{
RenderThread.Join(200);
}
+
+ // The encoders are disposed above, which closes the output file. An unfinished file of this render is deleted.
+ DeleteIncompleteOutputFile();
+ }
+
+ ///
+ /// Deletes the output file of the current render when the render did not finish it (_incompleteOutputPath).
+ /// Files of earlier, finished renders are never deleted.
+ /// Returns true when a file was deleted.
+ ///
+ private bool DeleteIncompleteOutputFile()
+ {
+ var path = Interlocked.Exchange(ref _incompleteOutputPath, null);
+ if (string.IsNullOrEmpty(path) || !File.Exists(path)) return false;
+
+ try
+ {
+ File.Delete(path);
+ Melon.Logger.Msg($"[File Cleanup] Deleted incomplete video: {path}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Melon.Logger.Error($"[File Cleanup] Failed to delete incomplete video: {ex.Message}");
+ return false;
+ }
}
private void Update()
@@ -634,6 +665,7 @@ internal class RenderManager : MonoBehaviour
{
_allVideoFFmpegInstances.Add(videoFFmpeg);
}
+ Volatile.Write(ref _incompleteOutputPath, CurrentOutPath);
videoFFmpeg.Start(CurrentOutPath);
}
catch (Exception)
@@ -735,7 +767,13 @@ internal class RenderManager : MonoBehaviour
FrameAvailableEvent.WaitOne(50);
}
- if (!IsRenderAborted) videoFFmpeg.End();
+ if (!IsRenderAborted)
+ {
+ // End throws when the file cannot be finished (e.g. the trailer cannot be written),
+ // which is handled as a render failure by the catch block below.
+ videoFFmpeg.End();
+ Volatile.Write(ref _incompleteOutputPath, null);
+ }
videoFFmpeg.Dispose();
lock (_allVideoFFmpegInstances)
@@ -862,18 +900,9 @@ internal class RenderManager : MonoBehaviour
scrController.instance.noFailInfiniteMargin = false;
}
- // 6. Immediately delete cancelled incomplete temporary video file
- if (!string.IsNullOrEmpty(CurrentOutPath) && File.Exists(CurrentOutPath))
+ // 6. Immediately delete the incomplete video file of this render
+ if (DeleteIncompleteOutputFile())
{
- try
- {
- File.Delete(CurrentOutPath);
- Melon.Logger.Msg($"[File Cleanup] Deleted incomplete video: {CurrentOutPath}");
- }
- catch (Exception ex)
- {
- Melon.Logger.Error($"[File Cleanup] Failed to delete temporary video: {ex.Message}");
- }
CurrentOutPath = null;
}
diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs
index bf37a9a..debd386 100644
--- a/Util/FFmpeg/FFmpegNativeEncoder.cs
+++ b/Util/FFmpeg/FFmpegNativeEncoder.cs
@@ -1520,8 +1520,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 2. av_write_trailer: Writes container trailer (end-of-file metadata).
/// For MP4, the moov atom (index info) is completed here.
/// ⚠️ If trailer is not written, the file becomes corrupted and unplayable!
+ /// av_write_trailer also flushes the output buffer and returns the write error of the output (AVIOContext.error), if any.
+ ///
+ /// Throws an exception when a flush or the trailer fails (e.g. the disk is full), because the file is then incomplete.
+ /// End() runs once: after it returns or throws, further calls return immediately.
///
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html
+ /// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libavformat/mux.c (av_write_trailer)
///
public void End()
{
@@ -1529,37 +1534,29 @@ namespace ADOFAIRenderer.Util.FFmpeg
{
if (_disposed || !_isInitialized) return;
- // 1. Flush delayed frames from video encoder (send null frame as "end of input" signal)
try
{
+ // 1. Flush delayed frames from video encoder (send null frame as "end of input" signal)
SendFrameAndWritePackets(null);
- }
- catch (Exception ex)
- {
- // Log error occurring during flush process
- Melon.Logger.Error($"Error during video encoder flush: {ex.Message}");
- }
- // 1.5. Flush remaining samples in audio FIFO + flush audio encoder
- if (_audioEnabled)
- {
- try
+ // 1.5. Flush remaining samples in audio FIFO + flush audio encoder
+ if (_audioEnabled)
{
FlushAudioEncoder();
}
- catch (Exception ex)
+
+ // 2. Write container trailer (moov atom and file termination metadata)
+ if (_pFormatContext != null)
{
- Melon.Logger.Error($"Error during audio encoder flush: {ex.Message}");
+ var ret = ffmpeg.av_write_trailer(_pFormatContext);
+ if (ret < 0)
+ throw new Exception($"Failed to write the container trailer — {GetFFmpegErrorString(ret)} (Code: {ret})");
}
}
-
- // 2. Write container trailer (moov atom and file termination metadata)
- if (_pFormatContext != null)
+ finally
{
- ffmpeg.av_write_trailer(_pFormatContext);
+ _isInitialized = false;
}
-
- _isInitialized = false;
}
}
@@ -1570,11 +1567,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
///
/// Actual resource cleanup logic. Cleanup order is important:
///
- /// 1. Call End() (if not called yet) → flush delayed frames + write trailer
- /// 2. SwsContext (scaler) → independent, can be freed first
- /// 3. AVFrame (src/dst) → freed before codec as codec may reference them
- /// 4. AVCodecContext (codec) → hw_frames_ctx/hw_device_ctx freed together
- /// 5. AVFormatContext (format/IO) → includes closing file handle
+ /// 1. SwsContext (scaler) → independent, can be freed first
+ /// 2. AVFrame (src/dst) → freed before codec as codec may reference them
+ /// 3. AVCodecContext (codec) → hw_frames_ctx/hw_device_ctx freed together
+ /// 4. AVFormatContext (format/IO) → includes closing file handle
+ ///
+ /// Dispose does not finish the file (no encoder flush, no trailer). A file is complete only after End() returned.
+ /// Without End(), the output file is incomplete and is left to the caller (the renderer deletes it).
///
/// Reference:
/// av_frame_free: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html
@@ -1588,12 +1587,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
{
if (_disposed) return;
_disposed = true;
-
- // Trigger End() safely in case Dispose is called without calling End() first
- if (_isInitialized)
- {
- try { End(); } catch { /* Ignore */ }
- }
+ _isInitialized = false;
// Release Sws scaler
if (_pSwsContext != null)