diff options
25 files changed, 213 insertions, 114 deletions
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index c0fc5a352..a5650c5a4 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -34,7 +34,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0 + uses: danielpalme/ReportGenerator-GitHub-Action@9f1033dc04b18a7dfa51aeefeb18540e8939021f # 5.3.4 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 47c06998c..76d57a478 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -183,6 +183,7 @@ - [btopherjohnson](https://github.com/btopherjohnson) - [GeorgeH005](https://github.com/GeorgeH005) - [Vedant](https://github.com/viktory36/) + - [NotSaifA](https://github.com/NotSaifA) # Emby Contributors diff --git a/Directory.Packages.props b/Directory.Packages.props index 17726ab4a..b0fe7057d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -48,7 +48,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Moq" Version="4.18.4" /> diff --git a/Emby.Naming/TV/TvParserHelpers.cs b/Emby.Naming/TV/TvParserHelpers.cs index 2eca389da..029917858 100644 --- a/Emby.Naming/TV/TvParserHelpers.cs +++ b/Emby.Naming/TV/TvParserHelpers.cs @@ -10,7 +10,7 @@ namespace Emby.Naming.TV; public static class TvParserHelpers { private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"]; - private static readonly string[] _endedState = ["Cancelled"]; + private static readonly string[] _endedState = ["Cancelled", "Canceled"]; /// <summary> /// Tries to parse a string into <see cref="SeriesStatus"/>. diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 77643505e..845dce5df 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -127,5 +127,7 @@ "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay", "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.", "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання", - "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць." + "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.", + "TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.", + "TaskAudioNormalization": "Нармалізацыя гуку" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 7780759a8..14cfeb71a 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -22,7 +22,7 @@ "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", - "HeaderLiveTV": "Živý přenos", + "HeaderLiveTV": "TV vysílání", "HeaderNextUp": "Další díly", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domácí videa", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 26eab392e..c8e036424 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -126,5 +126,9 @@ "External": "חיצוני", "HearingImpaired": "לקוי שמיעה", "TaskRefreshTrickplayImages": "יצירת תמונות המחשה", - "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות." + "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.", + "TaskAudioNormalization": "נרמול שמע", + "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.", + "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.", + "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index da8d38e40..783aecec7 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -83,7 +83,7 @@ "UserDeletedWithName": "L'utente {0} è stato rimosso", "UserDownloadingItemWithValues": "{0} sta scaricando {1}", "UserLockedOutWithName": "L'utente {0} è stato bloccato", - "UserOfflineFromDevice": "{0} si è disconnesso su {1}", + "UserOfflineFromDevice": "{0} si è disconnesso da {1}", "UserOnlineFromDevice": "{0} è online su {1}", "UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}", "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index d0c914de3..ff6376258 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -118,5 +118,6 @@ "Undefined": "Udefinert", "Forced": "Tvungen", "Default": "Standard", - "External": "Ekstern" + "External": "Ekstern", + "HearingImpaired": "Nedsett høyrsel" } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index af9b54ad1..bab644792 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -103,7 +103,7 @@ "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích", "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích", "HeaderFavoriteAlbums": "Album Ưa Thích", - "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}", + "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}", "DeviceOnlineWithName": "{0} đã kết nối", "DeviceOfflineWithName": "{0} đã ngắt kết nối", "ChapterNameValue": "Phân Cảnh {0}", @@ -127,5 +127,7 @@ "TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay", "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.", "TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát", - "TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại." + "TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.", + "TaskAudioNormalization": "Chuẩn Hóa Âm Thanh", + "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh." } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 212d678a8..5faa7bc59 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -385,6 +385,19 @@ public class MediaInfoHelper /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns> public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request) { + // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons + // Cap the MaxStreamingBitrate to 20Mbps, because we are unable to reliably probe source bitrate, + // which will cause the client to request extremely high bitrate that may fail the player/encoder + request.MaxStreamingBitrate = request.MaxStreamingBitrate > 20000000 ? 20000000 : request.MaxStreamingBitrate; + + if (request.DeviceProfile is not null) + { + // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues + // Notably: Some channels won't play on FireFox and LG webOs + // Some channels from HDHomerun will experience A/V sync issues + request.DeviceProfile.TranscodingProfiles = request.DeviceProfile.TranscodingProfiles.Where(p => !string.Equals(p.Container, "mp4", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); var profile = request.DeviceProfile; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index b607e9104..a3d7f471e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -120,7 +120,12 @@ public static class RequestHelpers internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null) { userId ??= httpContext.User.GetUserId(); - var user = userManager.GetUserById(userId.Value); + User? user = null; + if (!userId.IsNullOrEmpty()) + { + user = userManager.GetUserById(userId.Value); + } + var session = await sessionManager.LogSessionActivity( httpContext.User.GetClient(), httpContext.User.GetVersion(), diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index ad840e66d..efdfc745f 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); var options = _config.Configuration.TrickplayOptions; + if (options.Interval < 1000) + { + _logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval); + options.Interval = 1000; + } + foreach (var width in options.WidthResolutions) { cancellationToken.ThrowIfCancellationRequested(); @@ -267,7 +273,7 @@ public class TrickplayManager : ITrickplayManager } // Update bitrate - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000)); + var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m)); trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); } diff --git a/MediaBrowser.Common/Net/NetworkConstants.cs b/MediaBrowser.Common/Net/NetworkConstants.cs index b18058fa9..ccef5d271 100644 --- a/MediaBrowser.Common/Net/NetworkConstants.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -59,6 +59,11 @@ public static class NetworkConstants public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16); /// <summary> + /// IPv4 Link-Local as defined in RFC 3927. + /// </summary> + public static readonly IPNetwork IPv4RFC3927LinkLocal = new IPNetwork(IPAddress.Parse("169.254.0.0"), 16); + + /// <summary> /// IPv6 loopback as defined in RFC 4291. /// </summary> public static readonly IPNetwork IPv6RFC4291Loopback = new IPNetwork(IPAddress.IPv6Loopback, 128); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 49c208b77..e4ec7a4e3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -55,6 +55,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minKerneli915Hang = new Version(5, 18); private readonly Version _maxKerneli915Hang = new Version(6, 1, 3); private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); + private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); @@ -680,16 +681,6 @@ namespace MediaBrowser.Controller.MediaEncoding return -1; } - public string GetInputPathArgument(EncodingJobInfo state) - { - return state.MediaSource.VideoType switch - { - VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource), - VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource), - _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource) - }; - } - /// <summary> /// Gets the audio encoder. /// </summary> @@ -1005,7 +996,8 @@ namespace MediaBrowser.Controller.MediaEncoding Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc"); if (IsVulkanFullSupported() - && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) + && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop + && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); @@ -1203,7 +1195,7 @@ namespace MediaBrowser.Controller.MediaEncoding else { arg.Append(" -i ") - .Append(GetInputPathArgument(state)); + .Append(_mediaEncoder.GetInputPathArgument(state)); } // sub2video for external graphical subtitles @@ -2083,6 +2075,18 @@ namespace MediaBrowser.Controller.MediaEncoding profile = "constrained_high"; } + if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) + && profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase)) + { + profile = "constrained_baseline"; + } + + if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) + && profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase)) + { + profile = "constrained_high"; + } + if (!string.IsNullOrEmpty(profile)) { // Currently there's no profile option in av1_nvenc encoder @@ -2629,10 +2633,14 @@ namespace MediaBrowser.Controller.MediaEncoding && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value == 6) { + if (!encodingOptions.DownMixAudioBoost.Equals(1)) + { + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); + } + switch (encodingOptions.DownMixStereoAlgorithm) { case DownMixStereoAlgorithms.Dave750: - filters.Add("volume=4.25"); filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3"); break; case DownMixStereoAlgorithms.NightmodeDialogue: @@ -2640,11 +2648,6 @@ namespace MediaBrowser.Controller.MediaEncoding break; case DownMixStereoAlgorithms.None: default: - if (!encodingOptions.DownMixAudioBoost.Equals(1)) - { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); - } - break; } } @@ -3161,7 +3164,9 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedMaxHeight) { var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase); + var isMjpeg = videoEncoder is not null && videoEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var scaleVal = isV4l2 ? 64 : 2; + var targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder // If fixed dimensions were supplied if (requestedWidth.HasValue && requestedHeight.HasValue) @@ -3190,10 +3195,11 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - @"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2", + @"scale=trunc(min(max(iw\,ih*{3})\,min({0}\,{1}*{3}))/{2})*{2}:trunc(min(max(iw/{3}\,ih)\,min({0}/{3}\,{1}))/2)*2", maxWidthParam, maxHeightParam, - scaleVal); + scaleVal, + targetAr); } // If a fixed width was requested @@ -3209,8 +3215,9 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - "scale={0}:trunc(ow/a/2)*2", - widthParam); + "scale={0}:trunc(ow/{1}/2)*2", + widthParam, + targetAr); } // If a fixed height was requested @@ -3220,9 +3227,10 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - "scale=trunc(oh*a/{1})*{1}:{0}", + "scale=trunc(oh*{2}/{1})*{1}:{0}", heightParam, - scaleVal); + scaleVal, + targetAr); } // If a max width was requested @@ -3232,9 +3240,10 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - @"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2", + @"scale=trunc(min(max(iw\,ih*{2})\,{0})/{1})*{1}:trunc(ow/{2}/2)*2", maxWidthParam, - scaleVal); + scaleVal, + targetAr); } // If a max height was requested @@ -3244,9 +3253,10 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - @"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})", + @"scale=trunc(oh*{2}/{1})*{1}:min(max(iw/{2}\,ih)\,{0})", maxHeightParam, - scaleVal); + scaleVal, + targetAr); } return string.Empty; @@ -4291,6 +4301,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // map from qsv to vaapi. mainFilters.Add("hwmap=derive_device=vaapi"); + mainFilters.Add("format=vaapi"); } var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); @@ -4300,6 +4311,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // map from vaapi to qsv. mainFilters.Add("hwmap=derive_device=qsv"); + mainFilters.Add("format=qsv"); } } @@ -4474,7 +4486,8 @@ namespace MediaBrowser.Controller.MediaEncoding // prefered vaapi + vulkan filters pipeline if (_mediaEncoder.IsVaapiDeviceAmd && isVaapiVkSupported - && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) + && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop + && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { // AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support. return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index e696fa52c..26c353a54 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -246,6 +246,21 @@ namespace MediaBrowser.Controller.MediaEncoding IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path); /// <summary> + /// Gets the input path argument from <see cref="EncodingJobInfo"/>. + /// </summary> + /// <param name="state">The <see cref="EncodingJobInfo"/>.</param> + /// <returns>The input path argument.</returns> + string GetInputPathArgument(EncodingJobInfo state); + + /// <summary> + /// Gets the input path argument. + /// </summary> + /// <param name="path">The item path.</param> + /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param> + /// <returns>The input path argument.</returns> + string GetInputPathArgument(string path, MediaSourceInfo mediaSource); + + /// <summary> /// Generates a FFmpeg concat config for the source. /// </summary> /// <param name="source">The <see cref="MediaSourceInfo"/>.</param> diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 8ea0f58ea..80ef6ecf7 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -30,10 +30,8 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using static Nikse.SubtitleEdit.Core.Common.IfoParser; namespace MediaBrowser.MediaEncoding.Encoder { @@ -621,7 +619,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ImageFormat? targetFormat, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFile, mediaSource); + var inputArgument = GetInputPathArgument(inputFile, mediaSource); if (!isAudio) { @@ -824,6 +822,22 @@ namespace MediaBrowser.MediaEncoding.Encoder options.EnableTonemapping = false; } + if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.AspectRatio)) + { + // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimensions + var darParts = imageStream.AspectRatio.Split(':'); + var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], CultureInfo.InvariantCulture)); + // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched. + // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, but we really can do little about it. + var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05; + if (shouldResetHeight) + { + // SAR = DAR * Height / Width + // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR + imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa); + } + } + var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) }; var jobState = new EncodingJobInfo(TranscodingJobType.Progressive) { @@ -945,7 +959,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs; - while (isResponsive) + while (isResponsive && !cancellationToken.IsCancellationRequested) { try { @@ -959,8 +973,6 @@ namespace MediaBrowser.MediaEncoding.Encoder // We don't actually expect the process to be finished in one timeout span, just that one image has been generated. } - cancellationToken.ThrowIfCancellationRequested(); - var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count(); isResponsive = jpegCount > lastCount; @@ -969,7 +981,12 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!ranToCompletion) { - _logger.LogInformation("Stopping trickplay extraction due to process inactivity."); + if (!isResponsive) + { + _logger.LogInformation("Trickplay process unresponsive."); + } + + _logger.LogInformation("Stopping trickplay extraction."); StopProcess(processWrapper, 1000); } } @@ -1137,17 +1154,30 @@ namespace MediaBrowser.MediaEncoding.Encoder var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; // Get all files from the BDMV/STREAMING directory - var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")); - // Only return playable local .m2ts files - return directoryFiles - .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + return validPlaybackFiles + .Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f))) + .Where(f => f.Exists) .Select(f => f.FullName) - .Order() .ToList(); } /// <inheritdoc /> + public string GetInputPathArgument(EncodingJobInfo state) + => GetInputPathArgument(state.MediaPath, state.MediaSource); + + /// <inheritdoc /> + public string GetInputPathArgument(string path, MediaSourceInfo mediaSource) + { + return mediaSource.VideoType switch + { + VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null).ToList(), mediaSource), + VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path).ToList(), mediaSource), + _ => GetInputArgument(path, mediaSource) + }; + } + + /// <inheritdoc /> public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath) { // Get all playable files diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index fd911dbed..2e2979fcf 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Model.Search /// Gets or sets the matched term. /// </summary> /// <value>The matched term.</value> - public string MatchedTerm { get; set; } + public string? MatchedTerm { get; set; } /// <summary> /// Gets or sets the index number. diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index cb8a867f1..234c5869a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -398,7 +398,8 @@ namespace MediaBrowser.Providers.Manager foreach (var child in children) { - if (!child.IsFolder) + // Exclude any folders and virtual items since they are only placeholders + if (!child.IsFolder && !child.IsVirtualItem) { var childDateCreated = child.DateCreated; if (childDateCreated > dateLastMediaAdded) diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 8bb8d5bb4..04da8fb88 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -1,6 +1,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -141,19 +142,15 @@ namespace MediaBrowser.Providers.MediaInfo && item.SupportsLocalMetadata && !video.IsPlaceHolder) { - if (!video.SubtitleFiles.SequenceEqual( - _subtitleResolver.GetExternalFiles(video, directoryService, false) - .Select(info => info.Path).ToList(), - StringComparer.Ordinal)) + var externalFiles = new HashSet<string>(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase); + if (!new HashSet<string>(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles)) { _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path); return true; } - if (!video.AudioFiles.SequenceEqual( - _audioResolver.GetExternalFiles(video, directoryService, false) - .Select(info => info.Path).ToList(), - StringComparer.Ordinal)) + externalFiles = new HashSet<string>(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase); + if (!new HashSet<string>(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles)) { _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path); return true; @@ -161,14 +158,14 @@ namespace MediaBrowser.Providers.MediaInfo } if (item is Audio audio - && item.SupportsLocalMetadata - && !audio.LyricFiles.SequenceEqual( - _lyricResolver.GetExternalFiles(audio, directoryService, false) - .Select(info => info.Path).ToList(), - StringComparer.Ordinal)) + && item.SupportsLocalMetadata) { - _logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path); - return true; + var externalFiles = new HashSet<string>(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase); + if (!new HashSet<string>(audio.LyricFiles, StringComparer.Ordinal).SetEquals(externalFiles)) + { + _logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path); + return true; + } } return false; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 62eb0d89c..2d0bd60b9 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -98,7 +98,7 @@ namespace MediaBrowser.Providers.TV targetItem.SetSeasonName(number, name); } } - else if (!sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey)) + else { var newSeasons = sourceSeasonNames.Where(s => !targetSeasonNames.ContainsKey(s.Key)); foreach (var (number, name) in newSeasons) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index f6dcde4f6..9dc4446fc 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>, bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; bool replace = options.ReplaceAllImages; - if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) + if (!enableDuringScan.GetValueOrDefault(false)) { return ItemUpdateType.None; } @@ -153,20 +153,20 @@ API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently. -**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 secounds to load all extensions and prepare the enviorment while vscode is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab. +**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 seconds to load all extensions and prepare the environment while VS Code is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab. -**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public. +**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VS Code window to public. -**NOTE:** When first opening the server instance with any WebUI, you will be send to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup. +**NOTE:** When first opening the server instance with any WebUI, you will be sent to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup. -There are two configurations for you to chose from. +There are two configurations for you to choose from. #### Default - Development Jellyfin Server -This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run though the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` lunch config to start the server. +This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run through the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` launch config to start the server. -> Keep in mind that as this has no web client you have to connect to it via an extenal client. This can be just another codespace container running the WebUI. vuejs does not work from the getgo as it does not support the setup steps. +> Keep in mind that as this has no web client you have to connect to it via an external client. This can be just another codespace container running the WebUI. vuejs does not work from the get-go as it does not support the setup steps. #### Development Jellyfin Server ffmpeg -this extens the default server with an default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual +this extends the default server with a default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file. Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled. diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs index c1e1a7bda..365f0188d 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs @@ -30,25 +30,8 @@ namespace Jellyfin.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private static readonly string[] _disallowedMimeTypes = - { - "text/plain", - "text/html", - "video/x-matroska", - "video/mp4", - "application/vnd.apple.mpegurl", - "application/mpegurl", - "application/x-mpegurl", - "video/vnd.mpeg.dash.mpd" - }; - - private static readonly string[] _disallowedSharedStreamExtensions = - { - ".mkv", - ".mp4", - ".m3u8", - ".mpd" - }; + private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"]; + private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"]; private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; @@ -113,28 +96,34 @@ namespace Jellyfin.LiveTv.TunerHosts if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { - using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(message, cancellationToken) - .ConfigureAwait(false); + var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path); - if (response.IsSuccessStatusCode) + if (string.IsNullOrEmpty(extension)) { - if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase)) + try { - return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); + using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(message, cancellationToken) + .ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + if (_mimeTypesCanShareHttpStream.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase)) + { + return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); + } + } } - } - else - { - // Fallback to check path extension when the server does not support HEAD method - // Use UriBuilder to remove all query string as GetExtension will include them when used directly - var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path); - if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + catch (Exception) { - return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); + Logger.LogWarning("HEAD request to check MIME type failed, shared stream disabled"); } } + else if (_extensionsCanShareHttpStream.Contains(extension, StringComparison.OrdinalIgnoreCase)) + { + return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); + } } return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper); diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 70790bb5b..148b33fcb 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -903,6 +903,17 @@ public class NetworkManager : INetworkManager, IDisposable return false; } + /// <summary> + /// Get if the IPAddress is Link-local. + /// </summary> + /// <param name="address">The IP Address.</param> + /// <returns>Bool indicates if the address is link-local.</returns> + public bool IsLinkLocalAddress(IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal; + } + /// <inheritdoc/> public bool IsInLocalNetwork(IPAddress address) { @@ -1084,7 +1095,11 @@ public class NetworkManager : INetworkManager, IDisposable private bool MatchesExternalInterface(IPAddress source, out string result) { // Get the first external interface address that isn't a loopback. - var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray(); + var extResult = _interfaces + .Where(p => !IsInLocalNetwork(p.Address)) + .Where(p => p.Address.AddressFamily.Equals(source.AddressFamily)) + .Where(p => !IsLinkLocalAddress(p.Address)) + .OrderBy(x => x.Index).ToArray(); // No external interface found if (extResult.Length == 0) |
