aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Helpers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Helpers')
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs9
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs13
-rw-r--r--Jellyfin.Api/Helpers/ClassMigrationHelper.cs71
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs207
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs15
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs98
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs62
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs4
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs6
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs166
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs75
-rw-r--r--Jellyfin.Api/Helpers/SimilarItemsHelper.cs182
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs59
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs64
14 files changed, 699 insertions, 332 deletions
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index a3f2d88ce..21ec2d32f 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,8 +1,10 @@
-using System.Net.Http;
+using System;
+using System.Net.Http;
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;
@@ -98,6 +100,11 @@ 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;
var cancellationTokenSource = new CancellationTokenSource();
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index df235ced2..29e6b4193 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 000000000..a911a3324
--- /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 af0519ffa..a4da54cfd 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -113,7 +113,7 @@ 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;
var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal(
streamingRequest,
@@ -130,6 +130,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,
@@ -155,7 +160,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();
@@ -202,7 +207,61 @@ namespace Jellyfin.Api.Helpers
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
- AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+ 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";
+
+ EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ 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()))
{
@@ -212,40 +271,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);
+
+ AppendPlaylistCodecsField(playlistBuilder, state);
- AppendPlaylistResolutionField(builder, state);
+ AppendPlaylistResolutionField(playlistBuilder, state);
- AppendPlaylistFramerateField(builder, 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>
@@ -414,15 +510,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 +542,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 = profileString ?? "high";
+ }
+
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ profileString = 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 +603,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 +637,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 +688,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 6b516977e..cfa2c1229 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -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,8 +39,8 @@ 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)).ConfigureAwait(false);
+ var contentType = response.Content.Headers.ContentType?.ToString();
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
@@ -47,7 +49,7 @@ namespace Jellyfin.Api.Helpers
return new FileContentResult(Array.Empty<byte>(), contentType);
}
- return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
+ return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
}
/// <summary>
@@ -123,9 +125,8 @@ namespace Jellyfin.Api.Helpers
state.Dispose();
}
- await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
- return new FileStreamResult(httpContext.Response.Body, 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 95f1906ef..a5369c441 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 242496697..18e23fb5c 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,64 @@ 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;
+ }
+
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ if (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 = 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 e78f63b25..0d8315dee 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -166,7 +166,7 @@ namespace Jellyfin.Api.Helpers
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
- long? maxBitrate,
+ int? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
@@ -551,7 +551,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index e00ed3304..8bddf00d5 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;
@@ -90,6 +91,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;
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
new file mode 100644
index 000000000..824870c7e
--- /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 fb5c283f5..efce11f8a 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,11 +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.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
@@ -71,7 +74,7 @@ namespace Jellyfin.Api.Helpers
}
return removeEmpty
- ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
+ ? value.Split(separator, StringSplitOptions.RemoveEmptyEntries)
: value.Split(separator);
}
@@ -119,47 +122,39 @@ 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)
- {
- if (value == null)
- {
- return Array.Empty<Guid>();
- }
-
- return Split(value, ',', true)
- .Select(i => new Guid(i))
- .ToArray();
- }
-
- /// <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 QueryResult<BaseItemDto> CreateQueryResult(
+ QueryResult<(BaseItem, ItemCounts)> result,
+ DtoOptions dtoOptions,
+ IDtoService dtoService,
+ bool includeItemTypes,
+ User? user)
{
- if (string.IsNullOrEmpty(fields))
+ var dtos = result.Items.Select(i =>
{
- return Array.Empty<ItemFields>();
- }
+ var (baseItem, counts) = i;
+ var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- return Split(fields, ',', true)
- .Select(v =>
+ if (includeItemTypes)
{
- if (Enum.TryParse(v, true, out ItemFields value))
- {
- return (ItemFields?)value;
- }
-
- return null;
- }).Where(i => i.HasValue)
- .Select(i => i!.Value)
- .ToArray();
+ 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
+ };
}
}
}
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
deleted file mode 100644
index b922e76cf..000000000
--- 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 f4ec29bde..c6d844c4f 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -83,8 +83,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))
{
@@ -165,7 +169,9 @@ 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);
@@ -183,7 +189,7 @@ namespace Jellyfin.Api.Helpers
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 +202,41 @@ 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.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;
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 0db1fabff..99c90c315 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;
@@ -12,6 +12,7 @@ using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -102,7 +103,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 +117,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 +145,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 +194,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 +241,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));
}
@@ -304,10 +304,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();
}
}
@@ -470,11 +470,11 @@ namespace Jellyfin.Api.Helpers
}
/// <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 +489,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,13 +501,13 @@ 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.");
+ throw new ArgumentException("FFmpeg path not set.");
}
var process = new Process
@@ -523,7 +524,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
@@ -543,18 +544,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);
@@ -568,20 +571,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)
@@ -747,11 +750,11 @@ namespace Jellyfin.Api.Helpers
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();
@@ -770,8 +773,9 @@ namespace Jellyfin.Api.Helpers
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)
{
@@ -827,7 +831,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;
@@ -837,7 +841,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{