diff --git a/src/libse/BluRaySup/BluRaySupPicture.cs b/src/libse/BluRaySup/BluRaySupPicture.cs index 735d63d694a..3d3a24381ca 100644 --- a/src/libse/BluRaySup/BluRaySupPicture.cs +++ b/src/libse/BluRaySup/BluRaySupPicture.cs @@ -1141,9 +1141,10 @@ private static byte[] CreateSupFrame(BluRaySupPicture pic, IList= 0; long pts; long endPts; diff --git a/src/libuilogic/Export/ExportHandlerBluRaySup.cs b/src/libuilogic/Export/ExportHandlerBluRaySup.cs index 9b68f039a97..b224cb8f97c 100644 --- a/src/libuilogic/Export/ExportHandlerBluRaySup.cs +++ b/src/libuilogic/Export/ExportHandlerBluRaySup.cs @@ -44,6 +44,12 @@ private sealed record PendingCue(byte[] Buffer, BluRaySupPicture Picture, SKColo private long _pendingStartPts; private long _pendingEndPts; + /// + /// The pending subtitles' times were changed after made their + /// display sets, so even a single one has to be written again. + /// + private bool _pendingRetimed; + public void WriteHeader(string fileOrFolderName, ImageParameter imageParameter) { _width = imageParameter.ScreenWidth; @@ -78,6 +84,7 @@ public void WriteParagraph(ImageParameter param) startPts < _pendingEndPts && endPts > _pendingStartPts; if (_pending.Count > 0 && !overlapsPending) { + BridgeGapShorterThanHalfAFrame(picture.StartTime, startPts, param.FramesPerSecond); FlushPending(); } @@ -114,7 +121,7 @@ private void FlushPending() try { - if (_pending.Count == 1) + if (_pending.Count == 1 && !_pendingRetimed) { // Nothing to compose - the display sets CreateParagraph made are the file. var buffer = _pending[0].Buffer; @@ -128,9 +135,38 @@ private void FlushPending() finally { _pending.Clear(); + _pendingRetimed = false; } } + /// + /// A gap shorter than half a frame is not a pause anyone means: the typewriter effect left + /// 1 ms between its steps (issue #15933). On the frame grid such a gap usually vanishes, but + /// when it straddles the middle of a frame the end rounds down and the next start rounds up, + /// and that frame shows nothing. The text flickers, and with a full frame image the video + /// shows through. The pending subtitles that end last are kept up until the next one starts. + /// + private void BridgeGapShorterThanHalfAFrame(long nextStartMs, long nextStartPts, double fps) + { + var gapMs = nextStartMs - _pendingEndMs; + if (fps <= 0 || gapMs <= 0 || gapMs >= 500.0 / fps || _pendingEndPts >= nextStartPts) + { + return; + } + + foreach (var cue in _pending) + { + if (cue.Picture.EndTime == _pendingEndMs) + { + cue.Picture.EndTime = nextStartMs; + } + } + + _pendingEndMs = nextStartMs; + _pendingEndPts = nextStartPts; + _pendingRetimed = true; + } + /// /// A subtitle as it sits in the frame, ready to be composed with others. /// diff --git a/tests/libuilogic/Export/ExportHandlerBluRaySupOverlapTests.cs b/tests/libuilogic/Export/ExportHandlerBluRaySupOverlapTests.cs index db72c6402b5..c9f6aa36936 100644 --- a/tests/libuilogic/Export/ExportHandlerBluRaySupOverlapTests.cs +++ b/tests/libuilogic/Export/ExportHandlerBluRaySupOverlapTests.cs @@ -247,6 +247,53 @@ public void LinesTouchingEndToStart_AreNotComposed() Assert.Equal(new[] { 1, 0, 1, 0 }, segments.Where(s => s.Type == Pcs).Select(ObjectCount)); } + /// + /// A gap shorter than half a frame that straddles the middle of a frame: the end rounds down + /// and the next start up, which left one frame with nothing on screen (issue #15933, the + /// typewriter effect's 1 ms gaps). The first line now stays up until the second one starts. + /// + [Fact] + public void GapShorterThanHalfAFrame_AcrossAFrame_LeavesNoEmptyFrame() + { + // At 25 fps 3.019 s rounds to the frame at 3.000 s and 3.021 s to the one at 3.040 s. + var segments = ReadSegments(Export( + Cue(0, 1, 3.019, ExportAlignment.BottomCenter, SKColors.White), + Cue(1, 3.021, 5, ExportAlignment.BottomCenter, SKColors.White))); + + var pcs = segments.Where(s => s.Type == Pcs).ToList(); + Assert.Equal(new long[] { 1000 * 90, 3040 * 90, 3040 * 90, 5000 * 90 }, pcs.Select(p => p.Pts)); + Assert.Equal(new[] { 1, 0, 1, 0 }, pcs.Select(ObjectCount)); + Assert.Single(segments.Where(s => s.Type == Ods && s.Pts == 1000 * 90)); + } + + [Fact] + public void OneMillisecondGap_At23976_LeavesNoEmptyFrame() + { + // Two typewriter steps from the issue #15933 sample, 55.367 s and 55.368 s. + var a = Cue(0, 54.000, 55.367, ExportAlignment.BottomCenter, SKColors.White); + var b = Cue(1, 55.368, 57.000, ExportAlignment.BottomCenter, SKColors.White); + a.FramesPerSecond = 23.976; + b.FramesPerSecond = 23.976; + + var segments = ReadSegments(Export(a, b)); + + var pcs = segments.Where(s => s.Type == Pcs).ToList(); + Assert.Equal(new[] { 1, 0, 1, 0 }, pcs.Select(ObjectCount)); + Assert.Equal(BluRaySupPicture.MillisecondsToPts(55368, 23.976), pcs[1].Pts); + Assert.Equal(pcs[2].Pts, pcs[1].Pts); + } + + [Fact] + public void GapOfHalfAFrameOrMore_IsKept() + { + var segments = ReadSegments(Export( + Cue(0, 1, 3.000, ExportAlignment.BottomCenter, SKColors.White), + Cue(1, 3.020, 5, ExportAlignment.BottomCenter, SKColors.White))); + + var pcs = segments.Where(s => s.Type == Pcs).ToList(); + Assert.Equal(new long[] { 1000 * 90, 3000 * 90, 3040 * 90, 5000 * 90 }, pcs.Select(p => p.Pts)); + } + [Fact] public void LinesOnTopOfEachOther_BecomeOneObject() {