Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/libse/BluRaySup/BluRaySupPicture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,9 +1141,10 @@ private static byte[] CreateSupFrame(BluRaySupPicture pic, IList<BluRaySupCompos
var fpsId = GetFpsId(fps);
var writer = new SegmentWriter();

// Start and end go on the frame grid (issue #15478). That already closes every gap
// shorter than half a frame, so the small gap removal - which nudges the start one
// tick past the previous end, i.e. off the grid - is only needed without a frame rate.
// Start and end go on the frame grid (issue #15478). That closes most gaps shorter
// than half a frame, and the export handler bridges the rest (issue #15933), so the
// small gap removal - which nudges the start one tick past the previous end, i.e. off
// the grid - is only needed without a frame rate.
var onFrameGrid = TryGetExactFrameRate(fps, out var fpsNumerator, out var fpsDenominator) && pic.StartTime >= 0;
long pts;
long endPts;
Expand Down
38 changes: 37 additions & 1 deletion src/libuilogic/Export/ExportHandlerBluRaySup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ private sealed record PendingCue(byte[] Buffer, BluRaySupPicture Picture, SKColo
private long _pendingStartPts;
private long _pendingEndPts;

/// <summary>
/// The pending subtitles' times were changed after <see cref="CreateParagraph"/> made their
/// display sets, so even a single one has to be written again.
/// </summary>
private bool _pendingRetimed;

public void WriteHeader(string fileOrFolderName, ImageParameter imageParameter)
{
_width = imageParameter.ScreenWidth;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand All @@ -128,9 +135,38 @@ private void FlushPending()
finally
{
_pending.Clear();
_pendingRetimed = false;
}
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
/// A subtitle as it sits in the frame, ready to be composed with others.
/// </summary>
Expand Down
47 changes: 47 additions & 0 deletions tests/libuilogic/Export/ExportHandlerBluRaySupOverlapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,53 @@ public void LinesTouchingEndToStart_AreNotComposed()
Assert.Equal(new[] { 1, 0, 1, 0 }, segments.Where(s => s.Type == Pcs).Select(ObjectCount));
}

/// <summary>
/// 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.
/// </summary>
[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()
{
Expand Down
Loading