Merge remote-tracking branch 'upstream/master' into perf-rebased

This commit is contained in:
Shadowghost
2026-04-19 10:23:34 +02:00
45 changed files with 756 additions and 244 deletions

View File

@@ -0,0 +1,282 @@
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);
}
}
}

View File

@@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests
return (path, query, filename, extension);
}
[Theory]
// EnableSubtitleExtraction = false, internal subtitles
[InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
[InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
[InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
[InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
[InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
// EnableSubtitleExtraction = false, external subtitles
[InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
// EnableSubtitleExtraction = true, internal subtitles
[InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
[InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
[InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
[InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
// EnableSubtitleExtraction = true, external subtitles
[InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
public void GetSubtitleProfile_RespectsExtractionSetting(
string codec,
string profileFormat,
bool enableSubtitleExtraction,
bool isExternal,
PlayMethod playMethod,
SubtitleDeliveryMethod expectedMethod)
{
var mediaSource = new MediaSourceInfo();
var subtitleStream = new MediaStream
{
Type = MediaStreamType.Subtitle,
Index = 0,
IsExternal = isExternal,
Path = isExternal ? "/media/sub." + codec : null,
Codec = codec,
SupportsExternalStream = MediaStream.IsTextFormat(codec)
};
var subtitleProfiles = new[]
{
new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External }
};
var transcoderSupport = new Mock<ITranscoderSupport>();
transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny<string>())).Returns(enableSubtitleExtraction);
var result = StreamBuilder.GetSubtitleProfile(
mediaSource,
subtitleStream,
subtitleProfiles,
playMethod,
transcoderSupport.Object,
null,
null);
Assert.Equal(expectedMethod, result.Method);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
});
var countries = localizationManager.GetCountries().ToList();
Assert.Equal(139, countries.Count);
Assert.Equal(140, countries.Count);
var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal));
Assert.NotNull(germany);
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
Assert.Equal(194, cultures.Count);
Assert.Equal(496, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
@@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
}
[Theory]
[InlineData("mul", "Multiple languages")]
[InlineData("und", "Undetermined")]
[InlineData("mis", "Uncoded languages")]
[InlineData("zxx", "No linguistic content; Not applicable")]
public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName)
{
var localizationManager = Setup(new ServerConfiguration
{
UICulture = "en-US"
});
await localizationManager.LoadAll();
var culture = localizationManager.FindLanguageInfo(code);
Assert.NotNull(culture);
Assert.Equal(expectedDisplayName, culture.DisplayName);
Assert.Equal(code, culture.ThreeLetterISOLanguageName);
}
[Fact]
public async Task GetParentalRatings_Default_Success()
{