aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.MediaEncoding.Tests
diff options
context:
space:
mode:
authorMBR-0001 <55142207+MBR-0001@users.noreply.github.com>2026-04-18 17:14:31 +0200
committerGitHub <noreply@github.com>2026-04-18 17:14:31 +0200
commita2eff41d6617bed7a7c5518d00f227a2406a8421 (patch)
tree933386669537d5145ca6b3706728454aff27d703 /tests/Jellyfin.MediaEncoding.Tests
parentd4a46bc6291560b35a9b64fb88b68019f903d0ef (diff)
parent5aa093d2997d8120ce19037642556070d27ce0bb (diff)
Merge branch 'master' into removeGlobalSubtitleConfiguration
Diffstat (limited to 'tests/Jellyfin.MediaEncoding.Tests')
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs32
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs282
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json137
3 files changed, 450 insertions, 1 deletions
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
+ }
+}