Repository navigation
Fix Windows export hanging in finalize when audio and video end apart - #190
Merged
Merged
Conversation
ruccho
marked this pull request as ready for review
October 9, 2026 08:39
hkmt-mmy
approved these changes
Oct 13, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Windows,
StopAndExportAsynccan wait forever: the exported mp4 is left asftyp / mdatwith nomoov, and the session never completes. It happens when the audio and video in the exported segment end more than about 1.3–1.4 s apart, so it shows up intermittently in real projects.The cause is that
PlaceMarker(MFSTREAMSINK_MARKER_ENDOFSEGMENT, NULL, NULL)is rejected withMF_E_INVALIDTYPEon recent Windows builds (the failure reported in #156). Since #157 the rejection is treated as non-fatal, but the MPEG-4 sink then never learns that the stream has ended, and with a large enough end gap it stops requesting samples from the other stream. That stream never drains, finalization never starts, and the export hangs.The rejection only happens when
pvarContextValueis NULL orVT_EMPTY. With a non-empty context value the marker is accepted andMEStreamSinkMarkerfollows. This PR passes one, and waits forMEStreamSinkMarkerbefore finalizing.Root cause
unienc_windows_mfplaces the end-of-segment marker asPlaceMarker(ENDOFSEGMENT, NULL, NULL).mfmp4srcsnk.dll10.0.26100.8457 and later; verified on 10.0.26100.8875), the MPEG-4 stream sink returnsMF_E_INVALIDTYPEwheneverpvarContextValueis NULL orVT_EMPTY. The documentation allows NULL, and this HRESULT is not among the documented return values. Every marker type and everypvarMarkerValuevariant we tried fails the same way, which matches Windows: export always fails with "Failed to complete muxer: channel closed" — OS MPEG4 sink rejects PlaceMarker (MF_E_INVALIDTYPE) #156.MEStreamSinkRequestSamplefor the longer stream, waiting for samples from the stream that has ended.video_finish_rx/audio_finish_rxnever resolves,BeginFinalizeis never called, andStopAndExportAsyncwaits forever. All unienc worker threads are idle at that point (parked in the thread pool), with nothing inWriteFile/WriteConsoleWor inside Media Foundation.Whether an export hangs depends only on the end gap of the exported segment, so it is intermittent in real projects. In our project the video in the exported segment usually ends 0.6–1.2 s after the audio, and in about one run out of five 1.4–2.7 s after.
Changes
InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/mux/mod.rsonly.PROPVARIANT(VT_UI4) aspvarContextValueof the end-of-segment marker, so that the marker is accepted.MEStreamSinkMarker, i.e. once the sink has consumed everything placed before the marker, instead of right after placing it. This is the point from which finalization is safe to start.PlaceMarkeris still rejected, report the stream drained right away as before (noMEStreamSinkMarkerfollows a rejected marker).The last three points are the same approach as 33ab159 on
feature/windows-stability. That commit alone does not fix this hang, because the marker is still rejected and the fallback path behaves as before.Verification
Standalone repro (no Unity)
A small C++ program drives
MFCreateMPEG4MediaSinkthe same way unienc does (system-time presentation clock started and never stopped, oneProcessSampleperMEStreamSinkRequestSample,PlaceMarkeronce the input is exhausted,BeginFinalizeonce both streams have drained). It uses real H.264 + AAC samples and trims one stream to vary the end gap. Source:mp4sink_repro_v5.cpp(attached).mp4sink_repro_v5.cpp
NULL, NULL(current)NULL, VT_UI4(this PR)End-gap sweep with the current code (
NULL, NULL), 5 runs per point:So it hangs deterministically above about 1.3 s when video is the shorter stream, and with rising probability between about 1.4 and 1.85 s when audio is the shorter stream.
PlaceMarkerargument variants with a 3 s gap:NULL/NULL,VT_EMPTY/VT_EMPTY,VT_EMPTY/NULL,NULL/VT_EMPTY→MF_E_INVALIDTYPEand hang.NULL/VT_UI4→S_OK,MEStreamSinkMarkerarrives, finalize OK.For reference,
IMFSinkWriterwriting the same content with 3 s end gaps finalizes fine on the same machine.In a Unity project (Unity 6000.2.13f1, Windows 11 24H2 / 10.0.26100.8875, RTX 4070, NVIDIA H.264 Encoder MFT)
RealtimeInstantReplaySession, 1280x720, fixed 15 fps, 2.5 Mbps, AAC with Wwise audio capture; 60 s recording, thenStopAndExportAsyncwith a 120 s timeout.To see the end gap of runs that hang (their mp4 has no
moovto read durations from), we measured the frames handed to the muxer: the export path was replicated step by step and the result ofGetFramesForDurationwas recorded before muxing. On successful runs this matches the durations in the exported mp4 within 0.03 s.v1.6.11, 8 measured runs (video ends later than audio in every run):
PlaceMarker failedlogs instead of 4)Across all 18 measured runs (these 8 plus the 10 runs with this PR), the end gap was 0.56–1.17 s in 14 runs and 1.40, 1.87, 1.92 and 2.71 s in the other 4. Applying the thresholds from the standalone sweep, that gives an expected hang rate of about 15–20% for v1.6.11 in this setup, consistent with the 2/12 observed.
With this PR,
PlaceMarker(ENDOFSEGMENT) failed (non-fatal)no longer appears in the log.Notes
pvarContextValueis only attached to theMEStreamSinkMarkerevent; its value is not used.mfmp4srcsnk.dll10.0.26100.8457). On builds that accept NULL, behavior is unchanged except that the stream is reported drained onMEStreamSinkMarker.StopAndExportAsync()and no duration, video starts at the first keyframe in the buffer and audio at the first audio frame, and each stream is rebased to 0 on its own. If the recording starts with a stall (e.g. scene loading right after the session is created), the video keeps the frame from before the stall while the audio effectively starts after it, so the video comes out longer by about the stall length. The same rebasing would also shift audio against video by that amount; we have not checked the exported files for this yet.BeginFinalize. Media Session stops it first, but that is not required by the documentation, and it made no difference in the repro.