diff options
Diffstat (limited to 'Jellyfin.Api/Helpers')
| -rw-r--r-- | Jellyfin.Api/Helpers/AudioHelper.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 22 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/MediaInfoHelper.cs | 189 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 16 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 39 |
6 files changed, 100 insertions, 176 deletions
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index bec961dad..27497cd59 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -138,7 +138,7 @@ namespace Jellyfin.Api.Helpers StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); } if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) @@ -167,9 +167,7 @@ namespace Jellyfin.Api.Helpers return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, - contentType, - isHeadRequest, - _httpContextAccessor.HttpContext); + contentType); } // Need to start ffmpeg (because media can't be returned directly) diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 6385b62c9..5bdd3fe2e 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -22,14 +22,12 @@ namespace Jellyfin.Api.Helpers /// Returns a static file from a remote source. /// </summary> /// <param name="state">The current <see cref="StreamState"/>.</param> - /// <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, CancellationToken cancellationToken = default) @@ -45,12 +43,6 @@ namespace Jellyfin.Api.Helpers httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; - if (isHeadRequest) - { - httpContext.Response.Headers[HeaderNames.ContentType] = contentType; - return new OkResult(); - } - return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); } @@ -59,23 +51,11 @@ namespace Jellyfin.Api.Helpers /// </summary> /// <param name="path">The path to the file.</param> /// <param name="contentType">The content type of the file.</param> - /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> - /// <param name="httpContext">The current http context.</param> /// <returns>An <see cref="ActionResult"/> the file.</returns> public static ActionResult GetStaticFileResult( string path, - string contentType, - bool isHeadRequest, - HttpContext httpContext) + string contentType) { - httpContext.Response.ContentType = contentType; - - // 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 OkResult(); - } - return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 3b8dc7e31..31b979836 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -89,9 +89,9 @@ namespace Jellyfin.Api.Helpers string? mediaSourceId = null, string? liveStreamId = null) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var item = _libraryManager.GetItemById(id); var result = new PlaybackInfoResponse(); @@ -191,7 +191,9 @@ namespace Jellyfin.Api.Helpers DeviceId = auth.DeviceId, ItemId = item.Id, Profile = profile, - MaxAudioChannels = maxAudioChannels + MaxAudioChannels = maxAudioChannels, + AllowAudioStreamCopy = allowAudioStreamCopy, + AllowVideoStreamCopy = allowVideoStreamCopy }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) @@ -208,7 +210,7 @@ namespace Jellyfin.Api.Helpers mediaSource.SupportsDirectPlay = false; } - if (!enableDirectStream) + if (!enableDirectStream || !allowVideoStreamCopy) { mediaSource.SupportsDirectStream = false; } @@ -235,168 +237,79 @@ namespace Jellyfin.Api.Helpers user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } - // Beginning of Playback Determination: Attempt DirectPlay first - if (mediaSource.SupportsDirectPlay) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectPlay = false; - } - else - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectPlay = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectPlay = true; - } - } + if (!options.ForceDirectStream) + { + // direct-stream http streaming is currently broken + options.EnableDirectStream = false; + } - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + // Beginning of Playback Determination + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + streamInfo.StartPositionTicks = startTimeTicks; - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; + mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; + // Players do not handle this being set according to PlayMethod + mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; + mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null; - if (streamInfo != null) + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; + mediaSource.SupportsTranscoding = false; } } - } - - if (mediaSource.SupportsDirectStream) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + else if (item is Video) { - mediaSource.SupportsDirectStream = false; - } - else - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - if (item is Audio) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectStream = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectStream = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; + mediaSource.SupportsTranscoding = false; } } - } - - if (mediaSource.SupportsTranscoding) - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; - } + mediaSource.SupportsDirectPlay = false; + mediaSource.SupportsDirectStream = false; + + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; } else { - if (streamInfo != null) + if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream) { - streamInfo.PlaySessionId = playSessionId; + streamInfo.PlayMethod = PlayMethod.Transcode; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - if (streamInfo.PlayMethod == PlayMethod.Transcode) + if (!allowVideoStreamCopy) { - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - - if (!allowVideoStreamCopy) - { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; } if (!allowAudioStreamCopy) { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } foreach (var attachment in mediaSource.MediaAttachments) diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index 3fa07720a..6f5b64ea8 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -83,10 +83,10 @@ namespace Jellyfin.Api.Helpers int totalBytesRead = 0; var stopwatch = Stopwatch.StartNew(); - while (KeepReading(stopwatch.ElapsedMilliseconds)) + while (true) { totalBytesRead += _stream.Read(buffer); - if (totalBytesRead > 0) + if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) { break; } @@ -109,10 +109,10 @@ namespace Jellyfin.Api.Helpers int totalBytesRead = 0; var stopwatch = Stopwatch.StartNew(); - while (KeepReading(stopwatch.ElapsedMilliseconds)) + while (true) { totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - if (totalBytesRead > 0) + if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) { break; } @@ -172,10 +172,12 @@ namespace Jellyfin.Api.Helpers } } - private bool KeepReading(long elapsed) + private bool StopReading(int bytesRead, long elapsed) { - // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely - return !_job?.HasExited ?? elapsed < _timeoutMs; + // It should stop reading when anything has been successfully read or if the job has exited + // If the job is null, however, it's a live stream and will require user action to close, + // but don't keep it open indefinitely if it isn't reading anything + return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs); } } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index ed071bcd7..34dab75b8 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -102,7 +102,7 @@ namespace Jellyfin.Api.Helpers }; var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); - if (!auth.UserId.Equals(Guid.Empty)) + if (!auth.UserId.Equals(default)) { state.User = userManager.GetUserById(auth.UserId); } @@ -151,7 +151,7 @@ namespace Jellyfin.Api.Helpers ? mediaSources[0] : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal)); - if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) + if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id)) { mediaSource = mediaSources[0]; } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 3526d56c6..da3c1530c 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -13,11 +13,13 @@ using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -42,6 +44,8 @@ namespace Jellyfin.Api.Helpers /// </summary> private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>(); + private readonly IAttachmentExtractor _attachmentExtractor; + private readonly IApplicationPaths _appPaths; private readonly IAuthorizationContext _authorizationContext; private readonly EncodingHelper _encodingHelper; private readonly IFileSystem _fileSystem; @@ -55,6 +59,8 @@ namespace Jellyfin.Api.Helpers /// <summary> /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class. /// </summary> + /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param> + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> @@ -65,6 +71,8 @@ namespace Jellyfin.Api.Helpers /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> public TranscodingJobHelper( + IAttachmentExtractor attachmentExtractor, + IApplicationPaths appPaths, ILogger<TranscodingJobHelper> logger, IMediaSourceManager mediaSourceManager, IFileSystem fileSystem, @@ -75,6 +83,8 @@ namespace Jellyfin.Api.Helpers EncodingHelper encodingHelper, ILoggerFactory loggerFactory) { + _attachmentExtractor = attachmentExtractor; + _appPaths = appPaths; _logger = logger; _mediaSourceManager = mediaSourceManager; _fileSystem = fileSystem; @@ -449,9 +459,12 @@ 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); + HardwareEncodingType? hardwareAccelerationType = null; + if (!string.IsNullOrEmpty(hardwareAccelerationTypeString) + && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) + { + hardwareAccelerationType = parsedHardwareAccelerationType; + } _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo { @@ -467,7 +480,7 @@ namespace Jellyfin.Api.Helpers IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec), IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec), HardwareAccelerationType = hardwareAccelerationType, - TranscodeReasons = state.TranscodeReasons + TranscodeReason = state.TranscodeReason }); } } @@ -513,6 +526,22 @@ namespace Jellyfin.Api.Helpers throw new ArgumentException("FFmpeg path not set."); } + // If subtitles get burned in fonts may need to be extracted from the media file + if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); + await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + + if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) + { + string subtitlePath = state.SubtitleStream.Path; + string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); + string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture); + + await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + } + } + var process = new Process { StartInfo = new ProcessStartInfo @@ -753,6 +782,8 @@ namespace Jellyfin.Api.Helpers job.HasExited = true; job.ExitCode = process.ExitCode; + ReportTranscodingProgress(job, state, null, null, null, null, null); + _logger.LogDebug("Disposing stream resources"); state.Dispose(); |
