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
6 changes: 3 additions & 3 deletions MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma warning disable CS1591

using System.IO;
using MediaBrowser.Model.MediaInfo;
using Nikse.SubtitleEdit.Core.Common;

namespace MediaBrowser.MediaEncoding.Subtitles
{
Expand All @@ -12,8 +12,8 @@ public interface ISubtitleParser
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="fileExtension">The file extension.</param>
/// <returns>SubtitleTrackInfo.</returns>
SubtitleTrackInfo Parse(Stream stream, string fileExtension);
/// <returns>The parsed subtitle.</returns>
Subtitle Parse(Stream stream, string fileExtension);

/// <summary>
/// Determines whether the file extension is supported by the parser.
Expand Down
20 changes: 2 additions & 18 deletions MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.Common;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
Expand All @@ -30,7 +28,7 @@ public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
}

/// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
public Subtitle Parse(Stream stream, string fileExtension)
{
var subtitle = new Subtitle();
var lines = stream.ReadAllLines().ToList();
Expand Down Expand Up @@ -76,21 +74,7 @@ public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
throw new ArgumentException("Unsupported format: " + fileExtension);
}

var trackInfo = new SubtitleTrackInfo();
int len = subtitle.Paragraphs.Count;
var trackEvents = new SubtitleTrackEvent[len];
for (int i = 0; i < len; i++)
{
var p = subtitle.Paragraphs[i];
trackEvents[i] = new SubtitleTrackEvent(p.Number.ToString(CultureInfo.InvariantCulture), p.Text)
{
StartPositionTicks = p.StartTime.TimeSpan.Ticks,
EndPositionTicks = p.EndTime.TimeSpan.Ticks
};
}

trackInfo.TrackEvents = trackEvents;
return trackInfo;
return subtitle;
}

/// <inheritdoc />
Expand Down
4 changes: 2 additions & 2 deletions MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ public SubtitleEncoder(
_serverConfigurationManager = serverConfigurationManager;
}

private MemoryStream ConvertSubtitles(
internal MemoryStream ConvertSubtitles(
Stream stream,
SubtitleInfo inputInfo,
string outputFormat,
long startTimeTicks,
long endTimeTicks,
bool preserveOriginalTimestamps)
{
var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path));
var subtitle = _subtitleParser.Parse(stream, inputInfo.Format);

FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);

Expand Down
12 changes: 6 additions & 6 deletions tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ public void Parse_Valid_Success()
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);
}
}
}
52 changes: 26 additions & 26 deletions tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ public void Parse_Valid_Success()
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]
Expand All @@ -36,19 +36,19 @@ public void Parse_EmptyNewlineBetweenText_Success()
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);
}
}
}
24 changes: 12 additions & 12 deletions tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTr
{
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);
}
}

Expand Down Expand Up @@ -75,13 +75,13 @@ public void Parse_Valid_Success()
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -6,12 +11,16 @@
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>();
Expand Down Expand Up @@ -103,5 +112,90 @@ public async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, Med
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();
}
}
}
Loading