aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.MediaEncoding.Tests/Subtitles
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Jellyfin.MediaEncoding.Tests/Subtitles')
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs282
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs52
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs24
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs94
5 files changed, 138 insertions, 326 deletions
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
index 1f908d7e0e..b03651e5e9 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
@@ -15,13 +15,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
using var stream = File.OpenRead("Test Data/example.ass");
var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass");
- Assert.Single(parsed.TrackEvents);
- var trackEvent = parsed.TrackEvents[0];
+ Assert.Single(parsed.Paragraphs);
+ var paragraph = parsed.Paragraphs[0];
- Assert.Equal("1", trackEvent.Id);
- Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
- Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text);
+ Assert.Equal(1, paragraph.Number);
+ Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, paragraph.StartTime.TimeSpan.Ticks);
+ Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, paragraph.EndTime.TimeSpan.Ticks);
+ Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", paragraph.Text);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
deleted file mode 100644
index 5f84e85592..0000000000
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using System;
-using AutoFixture;
-using AutoFixture.AutoMoq;
-using MediaBrowser.MediaEncoding.Subtitles;
-using MediaBrowser.Model.MediaInfo;
-using Xunit;
-
-namespace Jellyfin.MediaEncoding.Subtitles.Tests
-{
- public class FilterEventsTests
- {
- private readonly SubtitleEncoder _encoder;
-
- public FilterEventsTests()
- {
- var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
- _encoder = fixture.Create<SubtitleEncoder>();
- }
-
- [Fact]
- public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained()
- {
- // Subtitle starts at 5s, ends at 15s.
- // Segment requested from 10s to 20s.
- // The subtitle is still on screen at 10s and should NOT be dropped.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Still on screen")
- {
- StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("2", "Next subtitle")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Equal(2, track.TrackEvents.Count);
- Assert.Equal("1", track.TrackEvents[0].Id);
- Assert.Equal("2", track.TrackEvents[1].Id);
- }
-
- [Fact]
- public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped()
- {
- // Subtitle starts at 2s, ends at 5s.
- // Segment requested from 10s.
- // The subtitle ended before the segment — should be dropped.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Already gone")
- {
- StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(5).Ticks
- },
- new SubtitleTrackEvent("2", "Visible")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Single(track.TrackEvents);
- Assert.Equal("2", track.TrackEvents[0].Id);
- }
-
- [Fact]
- public void FilterEvents_SubtitleAfterSegment_IsDropped()
- {
- // Segment is 10s-20s, subtitle starts at 25s.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "In range")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("2", "After segment")
- {
- StartPositionTicks = TimeSpan.FromSeconds(25).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(30).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Single(track.TrackEvents);
- Assert.Equal("1", track.TrackEvents[0].Id);
- }
-
- [Fact]
- public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps()
- {
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Subtitle")
- {
- StartPositionTicks = TimeSpan.FromSeconds(15).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(20).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
- preserveTimestamps: false);
-
- Assert.Single(track.TrackEvents);
- // Timestamps should be shifted back by 10s
- Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks);
- Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks);
- }
-
- [Fact]
- public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps()
- {
- var startTicks = TimeSpan.FromSeconds(15).Ticks;
- var endTicks = TimeSpan.FromSeconds(20).Ticks;
-
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Subtitle")
- {
- StartPositionTicks = startTicks,
- EndPositionTicks = endTicks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
- preserveTimestamps: true);
-
- Assert.Single(track.TrackEvents);
- Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks);
- Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks);
- }
-
- [Fact]
- public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained()
- {
- // Subtitle ends exactly when the segment begins.
- // EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0,
- // so SkipWhile stops and the subtitle is retained.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Boundary subtitle")
- {
- StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(10).Ticks
- },
- new SubtitleTrackEvent("2", "In range")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Equal(2, track.TrackEvents.Count);
- Assert.Equal("1", track.TrackEvents[0].Id);
- }
-
- [Fact]
- public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps()
- {
- // Subtitle starts at 5s, ends at 15s.
- // Segment requested from 10s to 20s, preserveTimestamps = false.
- // The subtitle spans the boundary and is retained, but shifting
- // StartPositionTicks by -10s would produce -5s (negative).
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Spans boundary")
- {
- StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("2", "Fully in range")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: false);
-
- Assert.Equal(2, track.TrackEvents.Count);
- // Subtitle 1: start should be clamped to 0, not -5s
- Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative");
- Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks);
- // Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s)
- Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks);
- Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks);
- }
-
- [Fact]
- public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition()
- {
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Before")
- {
- StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(4).Ticks
- },
- new SubtitleTrackEvent("2", "After")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("3", "Much later")
- {
- StartPositionTicks = TimeSpan.FromSeconds(500).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(505).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: 0,
- preserveTimestamps: true);
-
- Assert.Equal(2, track.TrackEvents.Count);
- Assert.Equal("2", track.TrackEvents[0].Id);
- Assert.Equal("3", track.TrackEvents[1].Id);
- }
- }
-}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index b7152961cd..01a35e6cb0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -15,19 +15,19 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
using var stream = File.OpenRead("Test Data/example.srt");
var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt");
- Assert.Equal(2, parsed.TrackEvents.Count);
-
- var trackEvent1 = parsed.TrackEvents[0];
- Assert.Equal("1", trackEvent1.Id);
- Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
- Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text);
-
- var trackEvent2 = parsed.TrackEvents[1];
- Assert.Equal("2", trackEvent2.Id);
- Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
- Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
+ Assert.Equal(2, parsed.Paragraphs.Count);
+
+ var paragraph1 = parsed.Paragraphs[0];
+ Assert.Equal(1, paragraph1.Number);
+ Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, paragraph1.StartTime.TimeSpan.Ticks);
+ Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, paragraph1.EndTime.TimeSpan.Ticks);
+ Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", paragraph1.Text);
+
+ var paragraph2 = parsed.Paragraphs[1];
+ Assert.Equal(2, paragraph2.Number);
+ Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, paragraph2.StartTime.TimeSpan.Ticks);
+ Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, paragraph2.EndTime.TimeSpan.Ticks);
+ Assert.Equal("Very good, Lieutenant.", paragraph2.Text);
}
[Fact]
@@ -36,19 +36,19 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
using var stream = File.OpenRead("Test Data/example2.srt");
var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt");
- Assert.Equal(2, parsed.TrackEvents.Count);
-
- var trackEvent1 = parsed.TrackEvents[0];
- Assert.Equal("311", trackEvent1.Id);
- Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
- Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text);
-
- var trackEvent2 = parsed.TrackEvents[1];
- Assert.Equal("312", trackEvent2.Id);
- Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
- Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text);
+ Assert.Equal(2, parsed.Paragraphs.Count);
+
+ var paragraph1 = parsed.Paragraphs[0];
+ Assert.Equal(311, paragraph1.Number);
+ Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, paragraph1.StartTime.TimeSpan.Ticks);
+ Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, paragraph1.EndTime.TimeSpan.Ticks);
+ Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", paragraph1.Text);
+
+ var paragraph2 = parsed.Paragraphs[1];
+ Assert.Equal(312, paragraph2.Number);
+ Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, paragraph2.StartTime.TimeSpan.Ticks);
+ Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, paragraph2.EndTime.TimeSpan.Ticks);
+ Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", paragraph2.Text);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
index 5b7aa7eaa9..d814088593 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -20,19 +20,19 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa));
- SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa");
+ var subtitle = _parser.Parse(stream, "ssa");
- Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
+ Assert.Equal(expectedSubtitleTrackEvents.Count, subtitle.Paragraphs.Count);
for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
{
SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
- SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
+ var actual = subtitle.Paragraphs[i];
- Assert.Equal(expected.Id, actual.Id);
+ Assert.Equal(expected.Id, actual.Number.ToString(CultureInfo.InvariantCulture));
Assert.Equal(expected.Text, actual.Text);
- Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
- Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
+ Assert.Equal(expected.StartPositionTicks, actual.StartTime.TimeSpan.Ticks);
+ Assert.Equal(expected.EndPositionTicks, actual.EndTime.TimeSpan.Ticks);
}
}
@@ -75,13 +75,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
using var stream = File.OpenRead("Test Data/example.ssa");
var parsed = _parser.Parse(stream, "ssa");
- Assert.Single(parsed.TrackEvents);
- var trackEvent = parsed.TrackEvents[0];
+ Assert.Single(parsed.Paragraphs);
+ var paragraph = parsed.Paragraphs[0];
- Assert.Equal("1", trackEvent.Id);
- Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
- Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text);
+ Assert.Equal(1, paragraph.Number);
+ Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, paragraph.StartTime.TimeSpan.Ticks);
+ Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, paragraph.EndTime.TimeSpan.Ticks);
+ Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", paragraph.Text);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
index ce1f005f40..2d0fa29c9a 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -1,3 +1,8 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
@@ -6,12 +11,16 @@ using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
public class SubtitleEncoderTests
{
+ private const int StreamCount = 8;
+ private const int CueCount = 500;
+
public static TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo> GetReadableFile_Valid_TestData()
{
var data = new TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo>();
@@ -103,5 +112,90 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal(subtitleInfo.Format, result.Format);
Assert.Equal(subtitleInfo.IsExternal, result.IsExternal);
}
+
+ [Fact]
+ public void ConvertSubtitles_SequentialCalls_AreDeterministic()
+ {
+ using var encoder = CreateEncoder();
+ var sources = GenerateSources();
+
+ var first = ConvertAllSequential(encoder, sources);
+ var second = ConvertAllSequential(encoder, sources);
+
+ for (var i = 0; i < StreamCount; i++)
+ {
+ Assert.Contains($"S{i}C{CueCount - 1}", first[i], StringComparison.Ordinal);
+ Assert.Equal(first[i], second[i]);
+ }
+ }
+
+ [Fact]
+ public async Task ConvertSubtitles_ConcurrentCalls_MatchSequentialBaseline()
+ {
+ const int Iterations = 10;
+
+ using var encoder = CreateEncoder();
+ var sources = GenerateSources();
+ var baseline = ConvertAllSequential(encoder, sources);
+
+ for (var iteration = 0; iteration < Iterations; iteration++)
+ {
+ var results = await Task.WhenAll(Enumerable.Range(0, StreamCount)
+ .Select(i => Task.Run(() => Convert(encoder, sources[i], i)))
+ .ToArray());
+
+ for (var i = 0; i < StreamCount; i++)
+ {
+ Assert.True(
+ string.Equals(baseline[i], results[i], StringComparison.Ordinal),
+ $"Iteration {iteration}: stream {i} returned corrupted content ({results[i].Length} chars vs {baseline[i].Length} baseline)");
+ }
+ }
+ }
+
+ private static SubtitleEncoder CreateEncoder()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ fixture.Inject<ISubtitleParser>(new SubtitleEditParser(NullLogger<SubtitleEditParser>.Instance));
+ return fixture.Create<SubtitleEncoder>();
+ }
+
+ private static byte[][] GenerateSources()
+ {
+ return Enumerable.Range(0, StreamCount)
+ .Select(i => Encoding.UTF8.GetBytes(GenerateSrt(i, CueCount)))
+ .ToArray();
+ }
+
+ private static string Convert(SubtitleEncoder encoder, byte[] source, int streamIndex)
+ {
+ using var input = new MemoryStream(source);
+ var info = new SubtitleEncoder.SubtitleInfo { Path = $"track{streamIndex}.srt", Format = "srt" };
+ using var output = encoder.ConvertSubtitles(input, info, "vtt", 0, 0, false);
+ return Encoding.UTF8.GetString(output.ToArray());
+ }
+
+ private static string[] ConvertAllSequential(SubtitleEncoder encoder, byte[][] sources)
+ {
+ return sources.Select((source, i) => Convert(encoder, source, i)).ToArray();
+ }
+
+ private static string GenerateSrt(int streamIndex, int cueCount)
+ {
+ var builder = new StringBuilder();
+ for (var i = 0; i < cueCount; i++)
+ {
+ var start = TimeSpan.FromSeconds(i * 4);
+ var end = start + TimeSpan.FromSeconds(2);
+ builder.Append(i + 1).AppendLine()
+ .Append(start.ToString(@"hh\:mm\:ss\,fff", CultureInfo.InvariantCulture))
+ .Append(" --> ")
+ .AppendLine(end.ToString(@"hh\:mm\:ss\,fff", CultureInfo.InvariantCulture))
+ .Append('S').Append(streamIndex).Append('C').Append(i).AppendLine()
+ .AppendLine();
+ }
+
+ return builder.ToString();
+ }
}
}