aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Helpers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Helpers')
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs36
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs13
-rw-r--r--Jellyfin.Api/Helpers/ClassMigrationHelper.cs71
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs251
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs30
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs98
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs66
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs20
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs74
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs166
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs146
-rw-r--r--Jellyfin.Api/Helpers/SimilarItemsHelper.cs182
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs103
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs106
14 files changed, 842 insertions, 520 deletions
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index eae2be05ba..264131905f 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -3,6 +3,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@@ -30,13 +32,11 @@ namespace Jellyfin.Api.Helpers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
/// <summary>
/// Initializes a new instance of the <see cref="AudioHelper"/> class.
@@ -48,13 +48,11 @@ namespace Jellyfin.Api.Helpers
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public AudioHelper(
IDlnaManager dlnaManager,
IAuthorizationContext authContext,
@@ -63,13 +61,11 @@ namespace Jellyfin.Api.Helpers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
IHttpClientFactory httpClientFactory,
- IHttpContextAccessor httpContextAccessor)
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
_dlnaManager = dlnaManager;
_authContext = authContext;
@@ -78,13 +74,11 @@ namespace Jellyfin.Api.Helpers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
}
/// <summary>
@@ -97,7 +91,14 @@ namespace Jellyfin.Api.Helpers
TranscodingJobType transcodingJobType,
StreamingRequestDto streamingRequest)
{
+ if (_httpContextAccessor.HttpContext == null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
+
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
@@ -109,9 +110,7 @@ namespace Jellyfin.Api.Helpers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -138,7 +137,7 @@ namespace Jellyfin.Api.Helpers
{
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- var httpClient = _httpClientFactory.CreateClient();
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
}
@@ -180,8 +179,7 @@ namespace Jellyfin.Api.Helpers
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
- var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index df235ced25..29e6b4193e 100644
--- a/Jellyfin.Api/Helpers/ClaimHelpers.cs
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -63,6 +63,19 @@ namespace Jellyfin.Api.Helpers
public static string? GetToken(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Token);
+ /// <summary>
+ /// Gets a flag specifying whether the request is using an api key.
+ /// </summary>
+ /// <param name="user">Current claims principal.</param>
+ /// <returns>The flag specifying whether the request is using an api key.</returns>
+ public static bool GetIsApiKey(in ClaimsPrincipal user)
+ {
+ var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
+ return !string.IsNullOrEmpty(claimValue)
+ && bool.TryParse(claimValue, out var parsedClaimValue)
+ && parsedClaimValue;
+ }
+
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
{
return user?.Identities
diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
new file mode 100644
index 0000000000..a911a33241
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Reflection;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// A static class for copying matching properties from one object to another.
+ /// TODO: remove at the point when a fixed migration path has been decided upon.
+ /// </summary>
+ public static class ClassMigrationHelper
+ {
+ /// <summary>
+ /// Extension for 'Object' that copies the properties to a destination object.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="destination">The destination.</param>
+ public static void CopyProperties(this object source, object destination)
+ {
+ // If any this null throw an exception.
+ if (source == null || destination == null)
+ {
+ throw new Exception("Source or/and Destination Objects are null");
+ }
+
+ // Getting the Types of the objects.
+ Type typeDest = destination.GetType();
+ Type typeSrc = source.GetType();
+
+ // Iterate the Properties of the source instance and populate them from their destination counterparts.
+ PropertyInfo[] srcProps = typeSrc.GetProperties();
+ foreach (PropertyInfo srcProp in srcProps)
+ {
+ if (!srcProp.CanRead)
+ {
+ continue;
+ }
+
+ var targetProperty = typeDest.GetProperty(srcProp.Name);
+ if (targetProperty == null)
+ {
+ continue;
+ }
+
+ if (!targetProperty.CanWrite)
+ {
+ continue;
+ }
+
+ var obj = targetProperty.GetSetMethod(true);
+ if (obj != null && obj.IsPrivate)
+ {
+ continue;
+ }
+
+ var target = targetProperty.GetSetMethod();
+ if (target != null && (target.Attributes & MethodAttributes.Static) != 0)
+ {
+ continue;
+ }
+
+ if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType))
+ {
+ continue;
+ }
+
+ // Passed all tests, lets set the value.
+ targetProperty.SetValue(destination, srcProp.GetValue(source, null), null);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 6a8829d462..dc5d6715b5 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -39,14 +40,12 @@ namespace Jellyfin.Api.Helpers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
@@ -58,14 +57,12 @@ namespace Jellyfin.Api.Helpers
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public DynamicHlsHelper(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -74,14 +71,12 @@ namespace Jellyfin.Api.Helpers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger,
- IHttpContextAccessor httpContextAccessor)
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -90,14 +85,12 @@ namespace Jellyfin.Api.Helpers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_networkManager = networkManager;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
}
/// <summary>
@@ -112,7 +105,8 @@ namespace Jellyfin.Api.Helpers
StreamingRequestDto streamingRequest,
bool enableAdaptiveBitrateStreaming)
{
- var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head;
+ var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal(
streamingRequest,
@@ -129,6 +123,11 @@ namespace Jellyfin.Api.Helpers
TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource)
{
+ if (_httpContextAccessor.HttpContext == null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
_httpContextAccessor.HttpContext.Request,
@@ -138,9 +137,7 @@ namespace Jellyfin.Api.Helpers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -154,7 +151,7 @@ namespace Jellyfin.Api.Helpers
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
}
- var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
+ var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
var builder = new StringBuilder();
@@ -165,13 +162,15 @@ namespace Jellyfin.Api.Helpers
var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
// from universal audio service
- if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
+ if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
+ && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
{
queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
}
// from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
+ if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
+ && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
{
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
}
@@ -198,12 +197,65 @@ namespace Jellyfin.Api.Helpers
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
- AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User);
- }
-
- AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress))
+ AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
+ }
+
+ var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+
+ if (state.VideoStream != null && state.VideoRequest != null)
+ {
+ // Provide SDR HEVC entrance for backward compatibility.
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
+ if (requestedVideoProfiles != null && requestedVideoProfiles.Length > 0)
+ {
+ // Force HEVC Main Profile and disable video stream copy.
+ state.OutputVideoCodec = "hevc";
+ var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
+ sdrVideoUrl += "&AllowVideoStreamCopy=false";
+
+ var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
+ var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
+ var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+
+ AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
+
+ // Restore the video codec
+ state.OutputVideoCodec = "copy";
+ }
+ }
+
+ // Provide Level 5.0 entrance for backward compatibility.
+ // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
+ // but in fact it is capable of playing videos up to Level 6.1.
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream.Level.HasValue
+ && state.VideoStream.Level > 150
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ var playlistCodecsField = new StringBuilder();
+ AppendPlaylistCodecsField(playlistCodecsField, state);
+
+ // Force the video level to 5.0.
+ var originalLevel = state.VideoStream.Level;
+ state.VideoStream.Level = 150;
+ var newPlaylistCodecsField = new StringBuilder();
+ AppendPlaylistCodecsField(newPlaylistCodecsField, state);
+
+ // Restore the video level.
+ state.VideoStream.Level = originalLevel;
+ var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
+ builder.Append(newPlaylist);
+ }
+ }
+
+ if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
@@ -211,40 +263,77 @@ namespace Jellyfin.Api.Helpers
var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2;
newBitrate = totalBitrate - variation;
- variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
- private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+ private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
{
- builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+ var playlistBuilder = new StringBuilder();
+ playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
- AppendPlaylistCodecsField(builder, state);
+ AppendPlaylistVideoRangeField(playlistBuilder, state);
- AppendPlaylistResolutionField(builder, state);
+ AppendPlaylistCodecsField(playlistBuilder, state);
- AppendPlaylistFramerateField(builder, state);
+ AppendPlaylistResolutionField(playlistBuilder, state);
+
+ AppendPlaylistFramerateField(playlistBuilder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
- builder.Append(",SUBTITLES=\"")
+ playlistBuilder.Append(",SUBTITLES=\"")
.Append(subtitleGroup)
.Append('"');
}
- builder.Append(Environment.NewLine);
- builder.AppendLine(url);
+ playlistBuilder.Append(Environment.NewLine);
+ playlistBuilder.AppendLine(url);
+ builder.Append(playlistBuilder);
+
+ return playlistBuilder;
+ }
+
+ /// <summary>
+ /// Appends a VIDEO-RANGE field containing the range of the output video stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
+ {
+ if (state.VideoStream != null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
+ {
+ var videoRange = state.VideoStream.VideoRange;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(",VIDEO-RANGE=SDR");
+ }
+
+ if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(",VIDEO-RANGE=PQ");
+ }
+ }
+ else
+ {
+ // Currently we only encode to SDR.
+ builder.Append(",VIDEO-RANGE=SDR");
+ }
+ }
}
/// <summary>
@@ -330,15 +419,14 @@ namespace Jellyfin.Api.Helpers
if (framerate.HasValue)
{
builder.Append(",FRAME-RATE=")
- .Append(framerate.Value);
+ .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
}
}
private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
{
// Within the local network this will likely do more harm than good.
- var ip = RequestHelpers.NormalizeIp(ipAddress).ToString();
- if (_networkManager.IsInLocalNetwork(ip))
+ if (_networkManager.IsInLocalNetwork(ipAddress))
{
return false;
}
@@ -414,15 +502,27 @@ namespace Jellyfin.Api.Helpers
/// <returns>H.26X level of the output video stream.</returns>
private int? GetOutputVideoCodecLevel(StreamState state)
{
- string? levelString;
+ string levelString = string.Empty;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream != null
&& state.VideoStream.Level.HasValue)
{
- levelString = state.VideoStream?.Level.ToString();
+ levelString = state.VideoStream.Level.ToString() ?? string.Empty;
}
else
{
- levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+ }
+
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+ }
}
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@@ -434,6 +534,38 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
+ /// Get the H.26X profile of the output video stream.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="codec">Video codec.</param>
+ /// <returns>H.26X profile of the output video stream.</returns>
+ private string GetOutputVideoCodecProfile(StreamState state, string codec)
+ {
+ string profileString = string.Empty;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.Profile))
+ {
+ profileString = state.VideoStream.Profile;
+ }
+ else if (!string.IsNullOrEmpty(codec))
+ {
+ profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ profileString ??= "high";
+ }
+
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ profileString ??= "main";
+ }
+ }
+
+ return profileString;
+ }
+
+ /// <summary>
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
/// </summary>
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
@@ -463,6 +595,16 @@ namespace Jellyfin.Api.Helpers
return HlsCodecStringHelpers.GetEAC3String();
}
+ if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetFLACString();
+ }
+
+ if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetALACString();
+ }
+
return string.Empty;
}
@@ -487,15 +629,14 @@ namespace Jellyfin.Api.Helpers
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
- string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+ string profile = GetOutputVideoCodecProfile(state, "h264");
return HlsCodecStringHelpers.GetH264String(profile, level);
}
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
+ string profile = GetOutputVideoCodecProfile(state, "hevc");
return HlsCodecStringHelpers.GetH265String(profile, level);
}
@@ -539,12 +680,30 @@ namespace Jellyfin.Api.Helpers
return variation;
}
- private string ReplaceBitrate(string url, int oldValue, int newValue)
+ private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
+
+ private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
+ {
+ string profileStr = codec + "-profile=";
+ return url.Replace(
+ profileStr + oldValue,
+ profileStr + newValue,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+ {
+ var oldPlaylist = playlist.ToString();
+ return oldPlaylist.Replace(
+ oldValue.ToString(),
+ newValue.ToString(),
+ StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index deb54dbe61..b0fd59e5e3 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Net.Http;
using System.Threading;
@@ -24,12 +24,14 @@ namespace Jellyfin.Api.Helpers
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
/// <param name="httpContext">The current http context.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
public static async Task<ActionResult> GetStaticRemoteStreamResult(
StreamState state,
bool isHeadRequest,
HttpClient httpClient,
- HttpContext httpContext)
+ HttpContext httpContext,
+ CancellationToken cancellationToken = default)
{
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
@@ -37,17 +39,18 @@ namespace Jellyfin.Api.Helpers
}
// Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType.ToString();
+ var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
+ var contentType = response.Content.Headers.ContentType?.ToString();
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
if (isHeadRequest)
{
- return new FileContentResult(Array.Empty<byte>(), contentType);
+ httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+ return new OkResult();
}
- return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
+ return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
}
/// <summary>
@@ -66,13 +69,13 @@ namespace Jellyfin.Api.Helpers
{
httpContext.Response.ContentType = contentType;
- // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
+ // if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request
if (isHeadRequest)
{
- return new NoContentResult();
+ return new OkResult();
}
- return new PhysicalFileResult(path, contentType);
+ return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
}
/// <summary>
@@ -105,7 +108,8 @@ namespace Jellyfin.Api.Helpers
// Headers only
if (isHeadRequest)
{
- return new FileContentResult(Array.Empty<byte>(), contentType);
+ httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+ return new OkResult();
}
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
@@ -123,10 +127,8 @@ namespace Jellyfin.Api.Helpers
state.Dispose();
}
- var memoryStream = new MemoryStream();
- await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
- memoryStream.Position = 0;
- return new FileStreamResult(memoryStream, contentType);
+ var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
finally
{
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 95f1906ef0..a5369c441c 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -10,12 +10,37 @@ namespace Jellyfin.Api.Helpers
public static class HlsCodecStringHelpers
{
/// <summary>
+ /// Codec name for MP3.
+ /// </summary>
+ public const string MP3 = "mp4a.40.34";
+
+ /// <summary>
+ /// Codec name for AC-3.
+ /// </summary>
+ public const string AC3 = "mp4a.a5";
+
+ /// <summary>
+ /// Codec name for E-AC-3.
+ /// </summary>
+ public const string EAC3 = "mp4a.a6";
+
+ /// <summary>
+ /// Codec name for FLAC.
+ /// </summary>
+ public const string FLAC = "fLaC";
+
+ /// <summary>
+ /// Codec name for ALAC.
+ /// </summary>
+ public const string ALAC = "alac";
+
+ /// <summary>
/// Gets a MP3 codec string.
/// </summary>
/// <returns>MP3 codec string.</returns>
public static string GetMP3String()
{
- return "mp4a.40.34";
+ return MP3;
}
/// <summary>
@@ -23,7 +48,7 @@ namespace Jellyfin.Api.Helpers
/// </summary>
/// <param name="profile">AAC profile.</param>
/// <returns>AAC codec string.</returns>
- public static string GetAACString(string profile)
+ public static string GetAACString(string? profile)
{
StringBuilder result = new StringBuilder("mp4a", 9);
@@ -41,12 +66,48 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
+ /// Gets an AC-3 codec string.
+ /// </summary>
+ /// <returns>AC-3 codec string.</returns>
+ public static string GetAC3String()
+ {
+ return AC3;
+ }
+
+ /// <summary>
+ /// Gets an E-AC-3 codec string.
+ /// </summary>
+ /// <returns>E-AC-3 codec string.</returns>
+ public static string GetEAC3String()
+ {
+ return EAC3;
+ }
+
+ /// <summary>
+ /// Gets an FLAC codec string.
+ /// </summary>
+ /// <returns>FLAC codec string.</returns>
+ public static string GetFLACString()
+ {
+ return FLAC;
+ }
+
+ /// <summary>
+ /// Gets an ALAC codec string.
+ /// </summary>
+ /// <returns>ALAC codec string.</returns>
+ public static string GetALACString()
+ {
+ return ALAC;
+ }
+
+ /// <summary>
/// Gets a H.264 codec string.
/// </summary>
/// <param name="profile">H.264 profile.</param>
/// <param name="level">H.264 level.</param>
/// <returns>H.264 string.</returns>
- public static string GetH264String(string profile, int level)
+ public static string GetH264String(string? profile, int level)
{
StringBuilder result = new StringBuilder("avc1", 11);
@@ -80,46 +141,29 @@ namespace Jellyfin.Api.Helpers
/// <param name="profile">H.265 profile.</param>
/// <param name="level">H.265 level.</param>
/// <returns>H.265 string.</returns>
- public static string GetH265String(string profile, int level)
+ public static string GetH265String(string? profile, int level)
{
// The h265 syntax is a bit of a mystery at the time this comment was written.
// This is what I've found through various sources:
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
- StringBuilder result = new StringBuilder("hev1", 16);
+ StringBuilder result = new StringBuilder("hvc1", 16);
- if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
{
- result.Append(".2.6");
+ result.Append(".2.4");
}
else
{
// Default to main if profile is invalid
- result.Append(".1.6");
+ result.Append(".1.4");
}
result.Append(".L")
- .Append(level * 3)
+ .Append(level)
.Append(".B0");
return result.ToString();
}
-
- /// <summary>
- /// Gets an AC-3 codec string.
- /// </summary>
- /// <returns>AC-3 codec string.</returns>
- public static string GetAC3String()
- {
- return "mp4a.a5";
- }
-
- /// <summary>
- /// Gets an E-AC-3 codec string.
- /// </summary>
- /// <returns>E-AC-3 codec string.</returns>
- public static string GetEAC3String()
- {
- return "mp4a.a6";
- }
}
}
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index 2424966973..d1cdaf867e 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -1,8 +1,11 @@
using System;
using System.Globalization;
using System.IO;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -45,6 +48,11 @@ namespace Jellyfin.Api.Helpers
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync().ConfigureAwait(false);
+ if (line == null)
+ {
+ // Nothing currently in buffer.
+ break;
+ }
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
{
@@ -70,24 +78,60 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
+ /// Gets the #EXT-X-MAP string.
+ /// </summary>
+ /// <param name="outputPath">The output path of the file.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="isOsDepends">Get a normal string or depends on OS.</param>
+ /// <returns>The string text of #EXT-X-MAP.</returns>
+ public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
+ {
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+
+ // on Linux/Unix
+ // #EXT-X-MAP:URI="prefix-1.mp4"
+ var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension;
+ if (!isOsDepends)
+ {
+ return fmp4InitFileName;
+ }
+
+ if (OperatingSystem.IsWindows())
+ {
+ // on Windows
+ // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
+ fmp4InitFileName = outputPrefix + "-1" + outputExtension;
+ }
+
+ return fmp4InitFileName;
+ }
+
+ /// <summary>
/// Gets the hls playlist text.
/// </summary>
/// <param name="path">The path to the playlist file.</param>
- /// <param name="segmentLength">The segment length.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
/// <returns>The playlist text as a string.</returns>
- public static string GetLivePlaylistText(string path, int segmentLength)
+ public static string GetLivePlaylistText(string path, StreamState state)
{
- using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var reader = new StreamReader(stream);
+ var text = File.ReadAllText(path);
- var text = reader.ReadToEnd();
-
- text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture);
-
- var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
+ var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+ if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var fmp4InitFileName = GetFmp4InitFileName(path, state, true);
+ var baseUrlParam = string.Format(
+ CultureInfo.InvariantCulture,
+ "hls/{0}/",
+ Path.GetFileNameWithoutExtension(path));
+ var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false);
- text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
- // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
+ // Replace fMP4 init file URI.
+ text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
+ }
return text;
}
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 3a736d1e8a..295cfaf089 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -1,11 +1,13 @@
using System;
using System.Globalization;
using System.Linq;
+using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -165,7 +167,7 @@ namespace Jellyfin.Api.Helpers
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
- long? maxBitrate,
+ int? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
@@ -178,7 +180,7 @@ namespace Jellyfin.Api.Helpers
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy,
- string ipAddress)
+ IPAddress ipAddress)
{
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
@@ -281,6 +283,7 @@ namespace Jellyfin.Api.Helpers
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -306,7 +309,7 @@ namespace Jellyfin.Api.Helpers
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+ && user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
@@ -325,6 +328,7 @@ namespace Jellyfin.Api.Helpers
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -352,6 +356,7 @@ namespace Jellyfin.Api.Helpers
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
else
@@ -389,6 +394,7 @@ namespace Jellyfin.Api.Helpers
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -498,7 +504,7 @@ namespace Jellyfin.Api.Helpers
true,
true,
true,
- httpRequest.HttpContext.Connection.RemoteIpAddress.ToString());
+ httpRequest.HttpContext.GetNormalizedRemoteIp());
}
else
{
@@ -522,7 +528,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="type">Dlna profile type.</param>
public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
{
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
+ mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
@@ -550,10 +556,10 @@ namespace Jellyfin.Api.Helpers
}
}
- private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
{
var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+ var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index 432df97086..963e177245 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
@@ -70,7 +71,8 @@ namespace Jellyfin.Api.Helpers
/// <returns>A <see cref="Task"/>.</returns>
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
{
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken);
+ cancellationToken = linkedCancellationTokenSource.Token;
try
{
@@ -90,6 +92,11 @@ namespace Jellyfin.Api.Helpers
allowAsyncFileRead = true;
}
+ if (_path == null)
+ {
+ throw new ResourceNotFoundException(nameof(_path));
+ }
+
await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
var eofCount = 0;
@@ -130,34 +137,10 @@ namespace Jellyfin.Api.Helpers
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
{
var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
- int bytesRead;
- int totalBytesRead = 0;
-
- if (readAsync)
- {
- bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = source.Read(array, 0, array.Length);
- }
-
- while (bytesRead != 0)
+ try
{
- var bytesToWrite = bytesRead;
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
-
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
- }
+ int bytesRead;
+ int totalBytesRead = 0;
if (readAsync)
{
@@ -167,9 +150,40 @@ namespace Jellyfin.Api.Helpers
{
bytesRead = source.Read(array, 0, array.Length);
}
- }
- return totalBytesRead;
+ while (bytesRead != 0)
+ {
+ var bytesToWrite = bytesRead;
+
+ if (bytesToWrite > 0)
+ {
+ await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+ _bytesWritten += bytesRead;
+ totalBytesRead += bytesRead;
+
+ if (_job != null)
+ {
+ _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+ }
+ }
+
+ if (readAsync)
+ {
+ bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = source.Read(array, 0, array.Length);
+ }
+ }
+
+ return totalBytesRead;
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(array);
+ }
}
}
}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
new file mode 100644
index 0000000000..824870c7ef
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -0,0 +1,166 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Model.IO;
+
+namespace Jellyfin.Api.Helpers
+{
+ /// <summary>
+ /// A progressive file stream for transferring transcoded files as they are written to.
+ /// </summary>
+ public class ProgressiveFileStream : Stream
+ {
+ private readonly FileStream _fileStream;
+ private readonly TranscodingJobDto? _job;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly bool _allowAsyncFileRead;
+ private int _bytesWritten;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="filePath">The path to the transcoded file.</param>
+ /// <param name="job">The transcoding job information.</param>
+ /// <param name="transcodingJobHelper">The transcoding job helper.</param>
+ public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper)
+ {
+ _job = job;
+ _transcodingJobHelper = transcodingJobHelper;
+ _bytesWritten = 0;
+
+ var fileOptions = FileOptions.SequentialScan;
+ _allowAsyncFileRead = false;
+
+ // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ fileOptions |= FileOptions.Asynchronous;
+ _allowAsyncFileRead = true;
+ }
+
+ _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+ }
+
+ /// <inheritdoc />
+ public override bool CanRead => _fileStream.CanRead;
+
+ /// <inheritdoc />
+ public override bool CanSeek => false;
+
+ /// <inheritdoc />
+ public override bool CanWrite => false;
+
+ /// <inheritdoc />
+ public override long Length => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ /// <inheritdoc />
+ public override void Flush()
+ {
+ _fileStream.Flush();
+ }
+
+ /// <inheritdoc />
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _fileStream.Read(buffer, offset, count);
+ }
+
+ /// <inheritdoc />
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ int totalBytesRead = 0;
+ int remainingBytesToRead = count;
+
+ int newOffset = offset;
+ while (remainingBytesToRead > 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ int bytesRead;
+ if (_allowAsyncFileRead)
+ {
+ bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
+ }
+
+ remainingBytesToRead -= bytesRead;
+ newOffset += bytesRead;
+
+ if (bytesRead > 0)
+ {
+ _bytesWritten += bytesRead;
+ totalBytesRead += bytesRead;
+
+ if (_job != null)
+ {
+ _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+ }
+ }
+ else
+ {
+ // If the job is null it's a live stream and will require user action to close
+ if (_job?.HasExited ?? false)
+ {
+ break;
+ }
+
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ return totalBytesRead;
+ }
+
+ /// <inheritdoc />
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ if (disposing)
+ {
+ _fileStream.Dispose();
+
+ if (_job != null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ }
+ }
+ }
+ finally
+ {
+ _disposed = true;
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index fbaa692700..56585aeab5 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
@@ -21,72 +25,33 @@ namespace Jellyfin.Api.Helpers
/// <param name="sortBy">Sort By. Comma delimited string.</param>
/// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
/// <returns>Order By.</returns>
- public static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
+ public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
{
- var val = sortBy;
-
- if (string.IsNullOrEmpty(val))
+ if (sortBy.Count == 0)
{
return Array.Empty<ValueTuple<string, SortOrder>>();
}
- var vals = val.Split(',');
- if (string.IsNullOrWhiteSpace(requestedSortOrder))
+ var result = new (string, SortOrder)[sortBy.Count];
+ var i = 0;
+ // Add elements which have a SortOrder specified
+ for (; i < requestedSortOrder.Count; i++)
{
- requestedSortOrder = "Ascending";
+ result[i] = (sortBy[i], requestedSortOrder[i]);
}
- var sortOrders = requestedSortOrder.Split(',');
-
- var result = new ValueTuple<string, SortOrder>[vals.Length];
-
- for (var i = 0; i < vals.Length; i++)
+ // Add remaining elements with the first specified SortOrder
+ // or the default one if no SortOrders are specified
+ var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
+ for (; i < sortBy.Count; i++)
{
- var sortOrderIndex = sortOrders.Length > i ? i : 0;
-
- var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
- var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
- ? SortOrder.Descending
- : SortOrder.Ascending;
-
- result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
+ result[i] = (sortBy[i], order);
}
return result;
}
/// <summary>
- /// Get parsed filters.
- /// </summary>
- /// <param name="filters">The filters.</param>
- /// <returns>Item filters.</returns>
- public static IEnumerable<ItemFilter> GetFilters(string? filters)
- {
- return string.IsNullOrEmpty(filters)
- ? Array.Empty<ItemFilter>()
- : filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
- }
-
- /// <summary>
- /// Splits a string at a separating character into an array of substrings.
- /// </summary>
- /// <param name="value">The string to split.</param>
- /// <param name="separator">The char that separates the substrings.</param>
- /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
- /// <returns>An array of the substrings.</returns>
- internal static string[] Split(string? value, char separator, bool removeEmpty)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return Array.Empty<string>();
- }
-
- return removeEmpty
- ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
- : value.Split(separator);
- }
-
- /// <summary>
/// Checks if the user can update an entry.
/// </summary>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
@@ -119,7 +84,7 @@ namespace Jellyfin.Api.Helpers
authorization.Version,
authorization.DeviceId,
authorization.Device,
- request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ request.HttpContext.GetNormalizedRemoteIp().ToString(),
user);
if (session == null)
@@ -130,52 +95,55 @@ namespace Jellyfin.Api.Helpers
return session;
}
- /// <summary>
- /// Get Guid array from string.
- /// </summary>
- /// <param name="value">String value.</param>
- /// <returns>Guid array.</returns>
- internal static Guid[] GetGuids(string? value)
+ internal static QueryResult<BaseItemDto> CreateQueryResult(
+ QueryResult<(BaseItem, ItemCounts)> result,
+ DtoOptions dtoOptions,
+ IDtoService dtoService,
+ bool includeItemTypes,
+ User? user)
{
- if (value == null)
+ var dtos = result.Items.Select(i =>
{
- return Array.Empty<Guid>();
- }
+ var (baseItem, counts) = i;
+ var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- return Split(value, ',', true)
- .Select(i => new Guid(i))
- .ToArray();
+ if (includeItemTypes)
+ {
+ dto.ChildCount = counts.ItemCount;
+ dto.ProgramCount = counts.ProgramCount;
+ dto.SeriesCount = counts.SeriesCount;
+ dto.EpisodeCount = counts.EpisodeCount;
+ dto.MovieCount = counts.MovieCount;
+ dto.TrailerCount = counts.TrailerCount;
+ dto.AlbumCount = counts.AlbumCount;
+ dto.SongCount = counts.SongCount;
+ dto.ArtistCount = counts.ArtistCount;
+ }
+
+ return dto;
+ });
+
+ return new QueryResult<BaseItemDto>
+ {
+ Items = dtos.ToArray(),
+ TotalRecordCount = result.TotalRecordCount
+ };
}
- /// <summary>
- /// Gets the item fields.
- /// </summary>
- /// <param name="fields">The fields string.</param>
- /// <returns>IEnumerable{ItemFields}.</returns>
- internal static ItemFields[] GetItemFields(string? fields)
+ internal static string[] GetItemTypeStrings(IReadOnlyList<BaseItemKind> itemKinds)
{
- if (string.IsNullOrEmpty(fields))
+ if (itemKinds.Count == 0)
{
- return Array.Empty<ItemFields>();
+ return Array.Empty<string>();
}
- return Split(fields, ',', true)
- .Select(v =>
- {
- if (Enum.TryParse(v, true, out ItemFields value))
- {
- return (ItemFields?)value;
- }
-
- return null;
- }).Where(i => i.HasValue)
- .Select(i => i!.Value)
- .ToArray();
- }
+ var itemTypes = new string[itemKinds.Count];
+ for (var i = 0; i < itemKinds.Count; i++)
+ {
+ itemTypes[i] = itemKinds[i].ToString();
+ }
- internal static IPAddress NormalizeIp(IPAddress ip)
- {
- return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
+ return itemTypes;
}
}
}
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
deleted file mode 100644
index b922e76cfd..0000000000
--- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
+++ /dev/null
@@ -1,182 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-
-namespace Jellyfin.Api.Helpers
-{
- /// <summary>
- /// The similar items helper class.
- /// </summary>
- public static class SimilarItemsHelper
- {
- internal static QueryResult<BaseItemDto> GetSimilarItemsResult(
- DtoOptions dtoOptions,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- Guid? userId,
- string id,
- string? excludeArtistIds,
- int? limit,
- Type[] includeTypes,
- Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
- {
- var user = userId.HasValue && !userId.Equals(Guid.Empty)
- ? userManager.GetUserById(userId.Value)
- : null;
-
- var item = string.IsNullOrEmpty(id) ?
- (!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
- libraryManager.RootFolder) : libraryManager.GetItemById(id);
-
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
- Recursive = true,
- DtoOptions = dtoOptions,
- ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds)
- };
-
- var inputItems = libraryManager.GetItemList(query);
-
- var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)
- .ToList();
-
- var returnItems = items;
-
- if (limit.HasValue)
- {
- returnItems = returnItems.Take(limit.Value).ToList();
- }
-
- var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos,
- TotalRecordCount = items.Count
- };
- }
-
- /// <summary>
- /// Gets the similaritems.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="inputItems">The input items.</param>
- /// <param name="getSimilarityScore">The get similarity score.</param>
- /// <returns>IEnumerable{BaseItem}.</returns>
- private static IEnumerable<BaseItem> GetSimilaritems(
- BaseItem item,
- ILibraryManager libraryManager,
- IEnumerable<BaseItem> inputItems,
- Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
- {
- var itemId = item.Id;
- inputItems = inputItems.Where(i => i.Id != itemId);
- var itemPeople = libraryManager.GetPeople(item);
- var allPeople = libraryManager.GetPeople(new InternalPeopleQuery
- {
- AppearsInItemId = item.Id
- });
-
- return inputItems.Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, itemPeople, allPeople, i)))
- .Where(i => i.Item2 > 2)
- .OrderByDescending(i => i.Item2)
- .Select(i => i.Item1);
- }
-
- private static IEnumerable<string> GetTags(BaseItem item)
- {
- return item.Tags;
- }
-
- /// <summary>
- /// Gets the similiarity score.
- /// </summary>
- /// <param name="item1">The item1.</param>
- /// <param name="item1People">The item1 people.</param>
- /// <param name="allPeople">All people.</param>
- /// <param name="item2">The item2.</param>
- /// <returns>System.Int32.</returns>
- internal static int GetSimiliarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
- {
- var points = 0;
-
- if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
- {
- points += 10;
- }
-
- // Find common genres
- points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-
- // Find common tags
- points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-
- // Find common studios
- points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
-
- var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id)
- .Select(i => i.Name)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .DistinctNames()
- .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
- points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
- {
- if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
- {
- return 5;
- }
-
- if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
-
- return 1;
- });
-
- if (item1.ProductionYear.HasValue && item2.ProductionYear.HasValue)
- {
- var diff = Math.Abs(item1.ProductionYear.Value - item2.ProductionYear.Value);
-
- // Add if they came out within the same decade
- if (diff < 10)
- {
- points += 2;
- }
-
- // And more if within five years
- if (diff < 5)
- {
- points += 2;
- }
- }
-
- return points;
- }
- }
-}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index b12590080c..8cffe9c4c9 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -41,9 +41,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
@@ -59,16 +57,13 @@ namespace Jellyfin.Api.Helpers
ILibraryManager libraryManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
+ EncodingHelper encodingHelper,
IDlnaManager dlnaManager,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
TranscodingJobType transcodingJobType,
CancellationToken cancellationToken)
{
- EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
// Parse the DLNA time seek header
if (!streamingRequest.StartTimeTicks.HasValue)
{
@@ -83,8 +78,12 @@ namespace Jellyfin.Api.Helpers
}
streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
+ if (httpRequest.Path.Value == null)
+ {
+ throw new ResourceNotFoundException(nameof(httpRequest.Path));
+ }
- var url = httpRequest.Path.Value.Split('.').Last();
+ var url = httpRequest.Path.Value.Split('.')[^1];
if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
{
@@ -117,14 +116,14 @@ namespace Jellyfin.Api.Helpers
if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
{
state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
+ state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
?? state.SupportedAudioCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
{
state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
+ state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)
?? state.SupportedSubtitleCodecs.FirstOrDefault();
}
@@ -165,11 +164,13 @@ namespace Jellyfin.Api.Helpers
state.DirectStreamProvider = liveStreamInfo.Item2;
}
- encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
+ var encodingOptions = serverConfigurationManager.GetEncodingOptions();
+
+ encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
string? containerInternal = Path.GetExtension(state.RequestedUrl);
- if (string.IsNullOrEmpty(streamingRequest.Container))
+ if (!string.IsNullOrEmpty(streamingRequest.Container))
{
containerInternal = streamingRequest.Container;
}
@@ -177,13 +178,13 @@ namespace Jellyfin.Api.Helpers
if (string.IsNullOrEmpty(containerInternal))
{
containerInternal = streamingRequest.Static ?
- StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio)
+ StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
: GetOutputFileExtension(state);
}
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
- state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
state.OutputAudioCodec = streamingRequest.AudioCodec;
@@ -196,20 +197,42 @@ namespace Jellyfin.Api.Helpers
encodingHelper.TryStreamCopy(state);
- if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{
- var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream?.BitRate,
- state.VideoStream?.Width,
- state.VideoStream?.Height,
- state.OutputVideoBitrate.Value,
- state.VideoStream?.Codec,
- state.OutputVideoCodec,
- state.VideoRequest.MaxWidth,
- state.VideoRequest.MaxHeight);
-
- state.VideoRequest.MaxWidth = resolution.MaxWidth;
- state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
+ && !state.VideoRequest.Height.HasValue
+ && !state.VideoRequest.MaxWidth.HasValue
+ && !state.VideoRequest.MaxHeight.HasValue;
+
+ if (isVideoResolutionNotRequested
+ && state.VideoStream != null
+ && state.VideoRequest.VideoBitRate.HasValue
+ && state.VideoStream.BitRate.HasValue
+ && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
+ {
+ // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
+ // and the requested video bitrate is higher than source video bitrate.
+ if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
+ {
+ state.VideoRequest.MaxWidth = state.VideoStream?.Width;
+ state.VideoRequest.MaxHeight = state.VideoStream?.Height;
+ }
+ }
+ else
+ {
+ var resolution = ResolutionNormalizer.Normalize(
+ state.VideoStream?.BitRate,
+ state.VideoStream?.Width,
+ state.VideoStream?.Height,
+ state.OutputVideoBitrate.Value,
+ state.VideoStream?.Codec,
+ state.OutputVideoCodec,
+ state.VideoRequest.MaxWidth,
+ state.VideoRequest.MaxHeight);
+
+ state.VideoRequest.MaxWidth = resolution.MaxWidth;
+ state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ }
}
}
@@ -217,7 +240,7 @@ namespace Jellyfin.Api.Helpers
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state)
- : ('.' + state.OutputContainer);
+ : ("." + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
@@ -269,16 +292,14 @@ namespace Jellyfin.Api.Helpers
}
}
- if (profile == null)
- {
- profile = dlnaManager.GetDefaultProfile();
- }
+ profile ??= dlnaManager.GetDefaultProfile();
var audioCodec = state.ActualOutputAudioCodec;
if (!state.IsVideoRequest)
{
- responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader(
+ responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
+ profile,
state.OutputContainer,
audioCodec,
state.OutputAudioBitrate,
@@ -295,7 +316,7 @@ namespace Jellyfin.Api.Helpers
responseHeaders.Add(
"contentFeatures.dlna.org",
- new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
+ ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
}
}
@@ -480,17 +501,15 @@ namespace Jellyfin.Api.Helpers
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
{
- var headers = request.Headers;
-
if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
- }
- else if (!string.IsNullOrWhiteSpace(deviceProfileId))
- {
- var caps = deviceManager.GetCapabilities(deviceProfileId);
- state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
+ if (state.DeviceProfile == null)
+ {
+ var caps = deviceManager.GetCapabilities(deviceProfileId);
+ state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
+ }
}
var profile = state.DeviceProfile;
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 67e4503729..05fa5b1350 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -22,7 +22,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Helpers
@@ -45,7 +44,6 @@ namespace Jellyfin.Api.Helpers
private readonly IAuthorizationContext _authorizationContext;
private readonly EncodingHelper _encodingHelper;
private readonly IFileSystem _fileSystem;
- private readonly IIsoManager _isoManager;
private readonly ILogger<TranscodingJobHelper> _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
@@ -63,9 +61,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- /// <param name="isoManager">Instance of the <see cref="IIsoManager"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public TranscodingJobHelper(
ILogger<TranscodingJobHelper> logger,
@@ -75,9 +71,7 @@ namespace Jellyfin.Api.Helpers
IServerConfigurationManager serverConfigurationManager,
ISessionManager sessionManager,
IAuthorizationContext authorizationContext,
- IIsoManager isoManager,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
+ EncodingHelper encodingHelper,
ILoggerFactory loggerFactory)
{
_logger = logger;
@@ -87,9 +81,8 @@ namespace Jellyfin.Api.Helpers
_serverConfigurationManager = serverConfigurationManager;
_sessionManager = sessionManager;
_authorizationContext = authorizationContext;
- _isoManager = isoManager;
+ _encodingHelper = encodingHelper;
_loggerFactory = loggerFactory;
- _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
DeleteEncodedMediaCache();
@@ -102,7 +95,7 @@ namespace Jellyfin.Api.Helpers
/// </summary>
/// <param name="playSessionId">Playback session id.</param>
/// <returns>The transcoding job.</returns>
- public TranscodingJobDto GetTranscodingJob(string playSessionId)
+ public TranscodingJobDto? GetTranscodingJob(string playSessionId)
{
lock (_activeTranscodingJobs)
{
@@ -116,7 +109,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="path">Path to the transcoding file.</param>
/// <param name="type">The <see cref="TranscodingJobType"/>.</param>
/// <returns>The transcoding job.</returns>
- public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
+ public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
{
lock (_activeTranscodingJobs)
{
@@ -144,7 +137,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
- // Progressive streams can stop on their own reliably
+ // Progressive streams can stop on their own reliably.
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
}
@@ -193,10 +186,9 @@ namespace Jellyfin.Api.Helpers
/// Called when [transcode kill timer stopped].
/// </summary>
/// <param name="state">The state.</param>
- private async void OnTranscodeKillTimerStopped(object state)
+ private async void OnTranscodeKillTimerStopped(object? state)
{
- var job = (TranscodingJobDto)state;
-
+ var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
{
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
@@ -241,7 +233,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
- // Progressive streams can stop on their own reliably
+ // Progressive streams can stop on their own reliably.
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
}
@@ -277,7 +269,7 @@ namespace Jellyfin.Api.Helpers
{
_activeTranscodingJobs.Remove(job);
- if (!job.CancellationTokenSource!.IsCancellationRequested)
+ if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
job.CancellationTokenSource.Cancel();
}
@@ -304,10 +296,10 @@ namespace Jellyfin.Api.Helpers
process!.StandardInput.WriteLine("q");
- // Need to wait because killing is asynchronous
+ // Need to wait because killing is asynchronous.
if (!process.WaitForExit(5000))
{
- _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
+ _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
process.Kill();
}
}
@@ -387,7 +379,9 @@ namespace Jellyfin.Api.Helpers
/// <param name="outputFilePath">The output file path.</param>
private void DeleteHlsPartialStreamFiles(string outputFilePath)
{
- var directory = Path.GetDirectoryName(outputFilePath);
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
var name = Path.GetFileNameWithoutExtension(outputFilePath);
var filesToDelete = _fileSystem.GetFilePaths(directory)
@@ -450,6 +444,10 @@ namespace Jellyfin.Api.Helpers
{
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
+ var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
+ HardwareEncodingType? hardwareAccelerationType = string.IsNullOrEmpty(hardwareAccelerationTypeString)
+ ? null
+ : (HardwareEncodingType)Enum.Parse(typeof(HardwareEncodingType), hardwareAccelerationTypeString, true);
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
@@ -464,17 +462,18 @@ namespace Jellyfin.Api.Helpers
AudioChannels = state.OutputAudioChannels,
IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+ HardwareAccelerationType = hardwareAccelerationType,
TranscodeReasons = state.TranscodeReasons
});
}
}
/// <summary>
- /// Starts the FFMPEG.
+ /// Starts FFmpeg.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="outputPath">The output path.</param>
- /// <param name="commandLineArguments">The command line arguments for ffmpeg.</param>
+ /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
/// <param name="request">The <see cref="HttpRequest"/>.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
@@ -489,7 +488,8 @@ namespace Jellyfin.Api.Helpers
CancellationTokenSource cancellationTokenSource,
string? workingDirectory = null)
{
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(directory);
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
@@ -500,10 +500,15 @@ namespace Jellyfin.Api.Helpers
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
- throw new ArgumentException("User does not have access to video transcoding");
+ throw new ArgumentException("User does not have access to video transcoding.");
}
}
+ if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
+ {
+ throw new ArgumentException("FFmpeg path not set.");
+ }
+
var process = new Process
{
StartInfo = new ProcessStartInfo
@@ -518,7 +523,7 @@ namespace Jellyfin.Api.Helpers
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,
Arguments = commandLineArguments,
- WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
+ WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
ErrorDialog = false
},
EnableRaisingEvents = true
@@ -538,18 +543,20 @@ namespace Jellyfin.Api.Helpers
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
_logger.LogInformation(commandLineLogMessage);
- var logFilePrefix = "ffmpeg-transcode";
+ var logFilePrefix = "FFmpeg.Transcode-";
if (state.VideoRequest != null
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
- ? "ffmpeg-remux"
- : "ffmpeg-directstream";
+ ? "FFmpeg.Remux-"
+ : "FFmpeg.DirectStream-";
}
- var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
+ var logFilePath = Path.Combine(
+ _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
+ $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
- // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+ // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
@@ -563,20 +570,20 @@ namespace Jellyfin.Api.Helpers
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error starting ffmpeg");
+ _logger.LogError(ex, "Error starting FFmpeg");
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw;
}
- _logger.LogDebug("Launched ffmpeg process");
+ _logger.LogDebug("Launched FFmpeg process");
state.TranscodingJob = transcodingJob;
- // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+ // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
- // Wait for the file to exist before proceeeding
+ // Wait for the file to exist before proceeding
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
_logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
@@ -735,41 +742,34 @@ namespace Jellyfin.Api.Helpers
/// <param name="state">The state.</param>
private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
{
- if (job != null)
- {
- job.HasExited = true;
- }
+ job.HasExited = true;
_logger.LogDebug("Disposing stream resources");
state.Dispose();
if (process.ExitCode == 0)
{
- _logger.LogInformation("FFMpeg exited with code 0");
+ _logger.LogInformation("FFmpeg exited with code 0");
}
else
{
- _logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
+ _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
}
- process.Dispose();
+ job.Dispose();
}
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
{
- if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath))
- {
- state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
- }
-
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
{
var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
- new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
- cancellationTokenSource.Token)
+ new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+ cancellationTokenSource.Token)
.ConfigureAwait(false);
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
+ _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
if (state.VideoRequest != null)
{
@@ -825,7 +825,7 @@ namespace Jellyfin.Api.Helpers
{
lock (_transcodingLocks)
{
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
+ if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
{
result = new SemaphoreSlim(1, 1);
_transcodingLocks[outputPath] = result;
@@ -835,7 +835,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{