diff options
| author | Bond-009 <bond.009@outlook.com> | 2026-04-24 19:00:19 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-24 19:00:19 +0200 |
| commit | a183fce142a47db3b2b9faa1e0f2863f0d56e5a1 (patch) | |
| tree | 8637cf347d573917242a3c5546baee33028825a3 /tests/Jellyfin.MediaEncoding.Tests | |
| parent | d1f242bc097b1530de27d5e74f303ff06096c294 (diff) | |
| parent | b1e2419c6593a3aa4c8df3778831a3214ae5a1c0 (diff) | |
Merge branch 'master' into Preservation-of-Watched-Status-on-Re-watch
Diffstat (limited to 'tests/Jellyfin.MediaEncoding.Tests')
4 files changed, 453 insertions, 3 deletions
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 6b703e7416..c7065c670a 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -3,6 +3,7 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> @@ -14,11 +15,11 @@ <ItemGroup> <PackageReference Include="AutoFixture" /> <PackageReference Include="AutoFixture.AutoMoq" /> - <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="AutoFixture.Xunit3" /> <PackageReference Include="coverlet.collector" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 94710a0957..3369af0e84 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -39,6 +39,23 @@ namespace Jellyfin.MediaEncoding.Tests.Probing public void GetFrameRate_Success(string value, float? expected) => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); + [Theory] + [InlineData("1:1", true)] + [InlineData("3201:3200", true)] + [InlineData("1215:1216", true)] + [InlineData("1001:1000", true)] + [InlineData("16:15", false)] + [InlineData("8:9", false)] + [InlineData("32:27", false)] + [InlineData("10:11", false)] + [InlineData("64:45", false)] + [InlineData("4:3", false)] + [InlineData("0:1", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void IsNearSquarePixelSar_DetectsCorrectly(string? sar, bool expected) + => Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar)); + [Fact] public void GetMediaInfo_MetaData_Success() { @@ -123,6 +140,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(358, res.VideoStream.Height); Assert.Equal(720, res.VideoStream.Width); Assert.Equal("2.40:1", res.VideoStream.AspectRatio); + Assert.True(res.VideoStream.IsAnamorphic); // SAR 32:27 — genuinely anamorphic NTSC DVD 16:9 Assert.Equal("yuv420p", res.VideoStream.PixelFormat); Assert.Equal(31d, res.VideoStream.Level); Assert.Equal(1, res.VideoStream.RefFrames); @@ -191,8 +209,20 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal("mkv,webm", res.Container); Assert.Equal(2, res.MediaStreams.Count); + Assert.Equal(540, res.MediaStreams[0].Width); + Assert.Equal(360, res.MediaStreams[0].Height); + } - Assert.False(res.MediaStreams[0].IsAVC); + [Fact] + public void GetMediaInfo_WebM_Like_Mkv() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_web_like_mkv_with_subtitle.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File); + + Assert.Equal("mkv", res.Container); + Assert.Equal(3, res.MediaStreams.Count); } [Fact] diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs new file mode 100644 index 0000000000..5f84e85592 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs @@ -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); + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json new file mode 100644 index 0000000000..4f52dd90dc --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json @@ -0,0 +1,137 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "vp8", + "codec_long_name": "On2 VP8", + "profile": "1", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 540, + "height": 360, + "coded_width": 540, + "coded_height": 360, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "3:2", + "pix_fmt": "yuv420p", + "level": -99, + "field_order": "progressive", + "refs": 1, + "r_frame_rate": "2997/125", + "avg_frame_rate": "2997/125", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 1, + "codec_name": "vorbis", + "codec_long_name": "Vorbis", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration": "117.707000", + "bit_rate": "127998", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 2, + "codec_name": "subrip", + "codec_long_name": "SubRip subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + } + ], + "format": { + "filename": "sample.mkv", + "nb_streams": 3, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "117.700914", + "size": "8566268", + "bit_rate": "582239", + "probe_score": 100 + } +} |
