aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Helpers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Helpers')
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs273
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs1144
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs171
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs295
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs193
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs773
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs261
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs237
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs1239
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs1416
10 files changed, 3050 insertions, 2952 deletions
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index bc83ff48a..2b18c389d 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -16,165 +16,164 @@ using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Audio helper.
+/// </summary>
+public class AudioHelper
{
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
+
/// <summary>
- /// Audio helper.
+ /// Initializes a new instance of the <see cref="AudioHelper"/> class.
/// </summary>
- public class AudioHelper
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <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="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,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ IHttpClientFactory httpClientFactory,
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
- private readonly IDlnaManager _dlnaManager;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- 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.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <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="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,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory,
- IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
+ _dlnaManager = dlnaManager;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _httpClientFactory = httpClientFactory;
+ _httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
+ }
+
+ /// <summary>
+ /// Get audio stream.
+ /// </summary>
+ /// <param name="transcodingJobType">Transcoding job type.</param>
+ /// <param name="streamingRequest">Streaming controller.Request dto.</param>
+ /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+ public async Task<ActionResult> GetAudioStream(
+ TranscodingJobType transcodingJobType,
+ StreamingRequestDto streamingRequest)
+ {
+ if (_httpContextAccessor.HttpContext is null)
{
- _dlnaManager = dlnaManager;
- _userManager = userManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _httpClientFactory = httpClientFactory;
- _httpContextAccessor = httpContextAccessor;
- _encodingHelper = encodingHelper;
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
}
- /// <summary>
- /// Get audio stream.
- /// </summary>
- /// <param name="transcodingJobType">Transcoding job type.</param>
- /// <param name="streamingRequest">Streaming controller.Request dto.</param>
- /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
- public async Task<ActionResult> GetAudioStream(
- 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;
- 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(
- streamingRequest,
- _httpContextAccessor.HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- if (streamingRequest.Static && state.DirectStreamProvider != null)
- {
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
- var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
- if (liveStreamInfo == null)
- {
- throw new FileNotFoundException();
- }
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ _httpContextAccessor.HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
- // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
- }
+ if (streamingRequest.Static && state.DirectStreamProvider is not null)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- // Static remote stream
- if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo is null)
{
- 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, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+ throw new FileNotFoundException();
}
- if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
- {
- return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically");
- }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
+ // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
+ }
- var outputPath = state.OutputFilePath;
- var outputPathExists = File.Exists(outputPath);
+ // Static remote stream
+ if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob != null;
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+ }
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+ if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
+ {
+ return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically");
+ }
- // Static stream
- if (streamingRequest.Static)
- {
- var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+ var outputPath = state.OutputFilePath;
+ var outputPathExists = File.Exists(outputPath);
- if (state.MediaSource.IsInfiniteStream)
- {
- var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
- return new FileStreamResult(stream, contentType);
- }
+ var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+ var isTranscodeCached = outputPathExists && transcodingJob is not null;
- return FileStreamResponseHelpers.GetStaticFileResult(
- state.MediaPath,
- contentType);
+ StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+
+ // Static stream
+ if (streamingRequest.Static)
+ {
+ var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+ if (state.MediaSource.IsInfiniteStream)
+ {
+ var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
- // Need to start ffmpeg (because media can't be returned directly)
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
- return await FileStreamResponseHelpers.GetTranscodedFile(
- state,
- isHeadRequest,
- _httpContextAccessor.HttpContext,
- _transcodingJobHelper,
- ffmpegCommandLineArguments,
- transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
+ return FileStreamResponseHelpers.GetStaticFileResult(
+ state.MediaPath,
+ contentType);
}
+
+ // Need to start ffmpeg (because media can't be returned directly)
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+ return await FileStreamResponseHelpers.GetTranscodedFile(
+ state,
+ isHeadRequest,
+ _httpContextAccessor.HttpContext,
+ _transcodingJobHelper,
+ ffmpegCommandLineArguments,
+ transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index fa392e567..4486954c6 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,6 +9,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -24,684 +26,732 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Dynamic hls helper.
+/// </summary>
+public class DynamicHlsHelper
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ 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>
- /// Dynamic hls helper.
+ /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
/// </summary>
- public class DynamicHlsHelper
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ /// <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="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,
+ IDlnaManager dlnaManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ INetworkManager networkManager,
+ ILogger<DynamicHlsHelper> logger,
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- 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.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <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="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,
- IDlnaManager dlnaManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- INetworkManager networkManager,
- ILogger<DynamicHlsHelper> logger,
- IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _networkManager = networkManager;
- _logger = logger;
- _httpContextAccessor = httpContextAccessor;
- _encodingHelper = encodingHelper;
- }
-
- /// <summary>
- /// Get master hls playlist.
- /// </summary>
- /// <param name="transcodingJobType">Transcoding job type.</param>
- /// <param name="streamingRequest">Streaming request dto.</param>
- /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
- /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
- public async Task<ActionResult> GetMasterHlsPlaylist(
- TranscodingJobType transcodingJobType,
- StreamingRequestDto streamingRequest,
- bool enableAdaptiveBitrateStreaming)
- {
- var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- return await GetMasterPlaylistInternal(
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _networkManager = networkManager;
+ _logger = logger;
+ _httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
+ }
+
+ /// <summary>
+ /// Get master hls playlist.
+ /// </summary>
+ /// <param name="transcodingJobType">Transcoding job type.</param>
+ /// <param name="streamingRequest">Streaming request dto.</param>
+ /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+ public async Task<ActionResult> GetMasterHlsPlaylist(
+ TranscodingJobType transcodingJobType,
+ StreamingRequestDto streamingRequest,
+ bool enableAdaptiveBitrateStreaming)
+ {
+ var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ return await GetMasterPlaylistInternal(
+ streamingRequest,
+ isHeadRequest,
+ enableAdaptiveBitrateStreaming,
+ transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+
+ private async Task<ActionResult> GetMasterPlaylistInternal(
+ StreamingRequestDto streamingRequest,
+ bool isHeadRequest,
+ bool enableAdaptiveBitrateStreaming,
+ TranscodingJobType transcodingJobType,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ if (_httpContextAccessor.HttpContext is null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
+ using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
- isHeadRequest,
- enableAdaptiveBitrateStreaming,
+ _httpContextAccessor.HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
- }
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
- private async Task<ActionResult> GetMasterPlaylistInternal(
- StreamingRequestDto streamingRequest,
- bool isHeadRequest,
- bool enableAdaptiveBitrateStreaming,
- TranscodingJobType transcodingJobType,
- CancellationTokenSource cancellationTokenSource)
+ _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
+ if (isHeadRequest)
{
- if (_httpContextAccessor.HttpContext == null)
- {
- throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
- }
+ return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- _httpContextAccessor.HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
- if (isHeadRequest)
- {
- 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();
- var builder = new StringBuilder();
+ builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXTM3U");
+ var isLiveStream = state.IsSegmentedLiveStream;
- var isLiveStream = state.IsSegmentedLiveStream;
+ var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
- var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
+ // from universal audio service
+ 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.SegmentContainer)
- && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
- {
- queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
- }
+ // from universal audio service
+ if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
+ && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
+ {
+ queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
+ }
- // from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
- && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
- {
- queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
- }
+ // Main stream
+ var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
- // Main stream
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+ playlistUrl += queryString;
- playlistUrl += queryString;
+ var subtitleStreams = state.MediaSource
+ .MediaStreams
+ .Where(i => i.IsTextSubtitleStream)
+ .ToList();
- var subtitleStreams = state.MediaSource
- .MediaStreams
- .Where(i => i.IsTextSubtitleStream)
- .ToList();
+ var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
+ ? "subs"
+ : null;
- var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
- ? "subs"
- : null;
+ // If we're burning in subtitles then don't add additional subs to the manifest
+ if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ subtitleGroup = null;
+ }
- // If we're burning in subtitles then don't add additional subs to the manifest
- if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
- {
- subtitleGroup = null;
- }
+ if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ {
+ AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
+ }
+
+ var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ if (state.VideoStream is not null && state.VideoRequest is not null)
+ {
+ // Provide a workaround for the case issue between flac and fLaC.
+ var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
+ if (!string.IsNullOrEmpty(flacWaPlaylist))
{
- AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
+ builder.Append(flacWaPlaylist);
}
- var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- if (state.VideoStream != null && state.VideoRequest != null)
+ // Provide SDR HEVC entrance for backward compatibility.
+ if (encodingOptions.AllowHevcEncoding
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- // 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 is not null && requestedVideoProfiles.Length > 0)
{
- 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);
+ var sdrOutputAudioBitrate = 0;
+ if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
{
- // 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);
- var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
- var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+ sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
+ }
+ else
+ {
+ sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
+ }
- AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
+ var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+ var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
- // Restore the video codec
- state.OutputVideoCodec = "copy";
+ // Provide a workaround for the case issue between flac and fLaC.
+ flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
+ if (!string.IsNullOrEmpty(flacWaPlaylist))
+ {
+ builder.Append(flacWaPlaylist);
}
+
+ // 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))
+ // 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);
+
+ // Provide a workaround for the case issue between flac and fLaC.
+ flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
+ if (!string.IsNullOrEmpty(flacWaPlaylist))
{
- 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);
+ builder.Append(flacWaPlaylist);
}
}
+ }
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
- {
- var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
- // By default, vary by just 200k
- var variation = GetBitrateVariation(totalBitrate);
+ if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
+ {
+ var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
- var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+ // By default, vary by just 200k
+ var variation = GetBitrateVariation(totalBitrate);
- variation *= 2;
- newBitrate = totalBitrate - variation;
- variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
- }
+ var newBitrate = totalBitrate - variation;
+ var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
- return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ variation *= 2;
+ newBitrate = totalBitrate - variation;
+ variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
- private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
- {
- var playlistBuilder = new StringBuilder();
- playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture))
- .Append(",AVERAGE-BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture));
-
- AppendPlaylistVideoRangeField(playlistBuilder, state);
+ return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- AppendPlaylistCodecsField(playlistBuilder, state);
+ private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+ {
+ var playlistBuilder = new StringBuilder();
+ playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture))
+ .Append(",AVERAGE-BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture));
- AppendPlaylistResolutionField(playlistBuilder, state);
+ AppendPlaylistVideoRangeField(playlistBuilder, state);
- AppendPlaylistFramerateField(playlistBuilder, state);
+ AppendPlaylistCodecsField(playlistBuilder, state);
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- playlistBuilder.Append(",SUBTITLES=\"")
- .Append(subtitleGroup)
- .Append('"');
- }
+ AppendPlaylistResolutionField(playlistBuilder, state);
- playlistBuilder.Append(Environment.NewLine);
- playlistBuilder.AppendLine(url);
- builder.Append(playlistBuilder);
+ AppendPlaylistFramerateField(playlistBuilder, state);
- return playlistBuilder;
+ if (!string.IsNullOrWhiteSpace(subtitleGroup))
+ {
+ playlistBuilder.Append(",SUBTITLES=\"")
+ .Append(subtitleGroup)
+ .Append('"');
}
- /// <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)
+ 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 is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
{
- if (state.VideoStream != null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
+ var videoRange = state.VideoStream.VideoRange;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
- var videoRange = state.VideoStream.VideoRange;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
{
- 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>
- /// Appends a CODECS field containing formatted strings of
- /// the active streams output video and audio codecs.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
- {
- // Video
- string videoCodecs = string.Empty;
- int? videoCodecLevel = GetOutputVideoCodecLevel(state);
- if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
- {
- videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+ if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(",VIDEO-RANGE=PQ");
+ }
}
-
- // Audio
- string audioCodecs = string.Empty;
- if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+ else
{
- audioCodecs = GetPlaylistAudioCodecs(state);
+ // Currently we only encode to SDR.
+ builder.Append(",VIDEO-RANGE=SDR");
}
+ }
+ }
- StringBuilder codecs = new StringBuilder();
+ /// <summary>
+ /// Appends a CODECS field containing formatted strings of
+ /// the active streams output video and audio codecs.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
+ {
+ // Video
+ string videoCodecs = string.Empty;
+ int? videoCodecLevel = GetOutputVideoCodecLevel(state);
+ if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
+ {
+ videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+ }
- codecs.Append(videoCodecs);
+ // Audio
+ string audioCodecs = string.Empty;
+ if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+ {
+ audioCodecs = GetPlaylistAudioCodecs(state);
+ }
- if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
- {
- codecs.Append(',');
- }
+ StringBuilder codecs = new StringBuilder();
- codecs.Append(audioCodecs);
+ codecs.Append(videoCodecs);
- if (codecs.Length > 1)
- {
- builder.Append(",CODECS=\"")
- .Append(codecs)
- .Append('"');
- }
+ if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
+ {
+ codecs.Append(',');
}
- /// <summary>
- /// Appends a RESOLUTION field containing the resolution of the output 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 AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+ codecs.Append(audioCodecs);
+
+ if (codecs.Length > 1)
{
- if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
- {
- builder.Append(",RESOLUTION=")
- .Append(state.OutputWidth.GetValueOrDefault())
- .Append('x')
- .Append(state.OutputHeight.GetValueOrDefault());
- }
+ builder.Append(",CODECS=\"")
+ .Append(codecs)
+ .Append('"');
}
+ }
- /// <summary>
- /// Appends a FRAME-RATE field containing the framerate of the output 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 AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+ /// <summary>
+ /// Appends a RESOLUTION field containing the resolution of the output 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 AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+ {
+ if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
{
- double? framerate = null;
- if (state.TargetFramerate.HasValue)
- {
- framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
- }
- else if (state.VideoStream?.RealFrameRate != null)
- {
- framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
- }
+ builder.Append(",RESOLUTION=")
+ .Append(state.OutputWidth.GetValueOrDefault())
+ .Append('x')
+ .Append(state.OutputHeight.GetValueOrDefault());
+ }
+ }
- if (framerate.HasValue)
- {
- builder.Append(",FRAME-RATE=")
- .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
- }
+ /// <summary>
+ /// Appends a FRAME-RATE field containing the framerate of the output 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 AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+ {
+ double? framerate = null;
+ if (state.TargetFramerate.HasValue)
+ {
+ framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
+ }
+ else if (state.VideoStream?.RealFrameRate is not null)
+ {
+ framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
}
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+ if (framerate.HasValue)
{
- // Within the local network this will likely do more harm than good.
- if (_networkManager.IsInLocalNetwork(ipAddress))
- {
- return false;
- }
+ builder.Append(",FRAME-RATE=")
+ .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ }
- if (!enableAdaptiveBitrateStreaming)
- {
- return false;
- }
+ private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+ {
+ // Within the local network this will likely do more harm than good.
+ if (_networkManager.IsInLocalNetwork(ipAddress))
+ {
+ return false;
+ }
- if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
- {
- // Opening live streams is so slow it's not even worth it
- return false;
- }
+ if (!enableAdaptiveBitrateStreaming)
+ {
+ return false;
+ }
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- return false;
- }
+ if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
+ {
+ // Opening live streams is so slow it's not even worth it
+ return false;
+ }
- if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
- {
- return false;
- }
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ return false;
+ }
- if (!state.IsOutputVideo)
- {
- return false;
- }
+ if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
+ {
+ return false;
+ }
- // Having problems in android
+ if (!state.IsOutputVideo)
+ {
return false;
- // return state.VideoRequest.VideoBitRate.HasValue;
}
- private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
+ // Having problems in android
+ return false;
+ // return state.VideoRequest.VideoBitRate.HasValue;
+ }
+
+ private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
+ {
+ if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
{
- if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
- {
- return;
- }
+ return;
+ }
- var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
- const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
+ var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
+ const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
- foreach (var stream in subtitles)
- {
- var name = stream.DisplayTitle;
-
- var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
- var isForced = stream.IsForced;
-
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
- state.Request.MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- 30.ToString(CultureInfo.InvariantCulture),
- user.GetToken());
-
- var line = string.Format(
- CultureInfo.InvariantCulture,
- Format,
- name,
- isDefault ? "YES" : "NO",
- isForced ? "YES" : "NO",
- url,
- stream.Language ?? "Unknown");
-
- builder.AppendLine(line);
- }
+ foreach (var stream in subtitles)
+ {
+ var name = stream.DisplayTitle;
+
+ var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
+ var isForced = stream.IsForced;
+
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
+ state.Request.MediaSourceId,
+ stream.Index.ToString(CultureInfo.InvariantCulture),
+ 30.ToString(CultureInfo.InvariantCulture),
+ user.GetToken());
+
+ var line = string.Format(
+ CultureInfo.InvariantCulture,
+ Format,
+ name,
+ isDefault ? "YES" : "NO",
+ isForced ? "YES" : "NO",
+ url,
+ stream.Language ?? "Unknown");
+
+ builder.AppendLine(line);
}
+ }
- /// <summary>
- /// Get the H.26X level of the output video stream.
- /// </summary>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>H.26X level of the output video stream.</returns>
- private int? GetOutputVideoCodecLevel(StreamState state)
+ /// <summary>
+ /// Get the H.26X level of the output video stream.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>H.26X level of the output video stream.</returns>
+ private int? GetOutputVideoCodecLevel(StreamState state)
+ {
+ string levelString = string.Empty;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream is not null
+ && state.VideoStream.Level.HasValue)
{
- string levelString = string.Empty;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream != null
- && state.VideoStream.Level.HasValue)
- {
- levelString = state.VideoStream.Level.ToString() ?? string.Empty;
- }
- else
+ levelString = state.VideoStream.Level.ToString() ?? string.Empty;
+ }
+ else
+ {
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
- 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);
- }
+ levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
- if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return parsedLevel;
+ levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
-
- return null;
}
- /// <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)
+ if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
{
- 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";
- }
+ return parsedLevel;
+ }
- if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- profileString ??= "main";
- }
- }
+ return null;
+ }
- return profileString;
+ /// <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;
}
-
- /// <summary>
- /// Gets a formatted string of the output audio codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>Formatted audio codec string.</returns>
- private string GetPlaylistAudioCodecs(StreamState state)
+ else if (!string.IsNullOrEmpty(codec))
{
- if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+ profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
- string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
- return HlsCodecStringHelpers.GetAACString(profile);
+ profileString ??= "high";
}
- if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return HlsCodecStringHelpers.GetMP3String();
+ profileString ??= "main";
}
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetAC3String();
- }
+ return profileString;
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetEAC3String();
- }
+ /// <summary>
+ /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+ /// </summary>
+ /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>Formatted audio codec string.</returns>
+ private string GetPlaylistAudioCodecs(StreamState state)
+ {
+ if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+ {
+ string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
+ return HlsCodecStringHelpers.GetAACString(profile);
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetFLACString();
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetMP3String();
+ }
- if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetALACString();
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetAC3String();
+ }
- return string.Empty;
+ if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetEAC3String();
}
- /// <summary>
- /// Gets a formatted string of the output video codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <param name="codec">Video codec.</param>
- /// <param name="level">Video level.</param>
- /// <returns>Formatted video codec string.</returns>
- private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+ if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
{
- if (level == 0)
- {
- // This is 0 when there's no requested H.26X level in the device profile
- // and the source is not encoded in H.26X
- _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
- return string.Empty;
- }
+ return HlsCodecStringHelpers.GetFLACString();
+ }
- if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- string profile = GetOutputVideoCodecProfile(state, "h264");
- return HlsCodecStringHelpers.GetH264String(profile, level);
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetALACString();
+ }
- if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- string profile = GetOutputVideoCodecProfile(state, "hevc");
- return HlsCodecStringHelpers.GetH265String(profile, level);
- }
+ if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetOPUSString();
+ }
+
+ return string.Empty;
+ }
+ /// <summary>
+ /// Gets a formatted string of the output video codec, for use in the CODECS field.
+ /// </summary>
+ /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+ /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="codec">Video codec.</param>
+ /// <param name="level">Video level.</param>
+ /// <returns>Formatted video codec string.</returns>
+ private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+ {
+ if (level == 0)
+ {
+ // This is 0 when there's no requested H.26X level in the device profile
+ // and the source is not encoded in H.26X
+ _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
return string.Empty;
}
- private int GetBitrateVariation(int bitrate)
+ if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
- // By default, vary by just 50k
- var variation = 50000;
-
- if (bitrate >= 10000000)
- {
- variation = 2000000;
- }
- else if (bitrate >= 5000000)
- {
- variation = 1500000;
- }
- else if (bitrate >= 3000000)
- {
- variation = 1000000;
- }
- else if (bitrate >= 2000000)
- {
- variation = 500000;
- }
- else if (bitrate >= 1000000)
- {
- variation = 300000;
- }
- else if (bitrate >= 600000)
- {
- variation = 200000;
- }
- else if (bitrate >= 400000)
- {
- variation = 100000;
- }
-
- return variation;
+ string profile = GetOutputVideoCodecProfile(state, "h264");
+ return HlsCodecStringHelpers.GetH264String(profile, level);
}
- private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
+ if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return url.Replace(
- "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
- "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
- StringComparison.OrdinalIgnoreCase);
+ string profile = GetOutputVideoCodecProfile(state, "hevc");
+ return HlsCodecStringHelpers.GetH265String(profile, level);
}
- private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
+ return string.Empty;
+ }
+
+ private int GetBitrateVariation(int bitrate)
+ {
+ // By default, vary by just 50k
+ var variation = 50000;
+
+ if (bitrate >= 10000000)
+ {
+ variation = 2000000;
+ }
+ else if (bitrate >= 5000000)
{
- string profileStr = codec + "-profile=";
- return url.Replace(
- profileStr + oldValue,
- profileStr + newValue,
- StringComparison.OrdinalIgnoreCase);
+ variation = 1500000;
}
+ else if (bitrate >= 3000000)
+ {
+ variation = 1000000;
+ }
+ else if (bitrate >= 2000000)
+ {
+ variation = 500000;
+ }
+ else if (bitrate >= 1000000)
+ {
+ variation = 300000;
+ }
+ else if (bitrate >= 600000)
+ {
+ variation = 200000;
+ }
+ else if (bitrate >= 400000)
+ {
+ variation = 100000;
+ }
+
+ return variation;
+ }
+
+ 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.Ordinal);
+ }
- private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+ private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
+ {
+ if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
{
- var oldPlaylist = playlist.ToString();
- return oldPlaylist.Replace(
- oldValue.ToString(),
- newValue.ToString(),
- StringComparison.OrdinalIgnoreCase);
+ return string.Empty;
}
+
+ var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
+
+ return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
}
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 5bdd3fe2e..0f0a70c69 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -11,110 +11,109 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// The stream response helpers.
+/// </summary>
+public static class FileStreamResponseHelpers
{
/// <summary>
- /// The stream response helpers.
+ /// Returns a static file from a remote source.
/// </summary>
- public static class FileStreamResponseHelpers
+ /// <param name="state">The current <see cref="StreamState"/>.</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,
+ HttpClient httpClient,
+ HttpContext httpContext,
+ CancellationToken cancellationToken = default)
{
- /// <summary>
- /// Returns a static file from a remote source.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</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,
- HttpClient httpClient,
- HttpContext httpContext,
- CancellationToken cancellationToken = default)
+ if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
- if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
- {
- httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
- }
+ httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
+ }
- // Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
+ // Can't dispose the response as it's required up the call chain.
+ var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
+ var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
- httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
+ httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
- return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
- }
+ return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
+ }
- /// <summary>
- /// Returns a static file from the server.
- /// </summary>
- /// <param name="path">The path to the file.</param>
- /// <param name="contentType">The content type of the file.</param>
- /// <returns>An <see cref="ActionResult"/> the file.</returns>
- public static ActionResult GetStaticFileResult(
- string path,
- string contentType)
- {
- return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
- }
+ /// <summary>
+ /// Returns a static file from the server.
+ /// </summary>
+ /// <param name="path">The path to the file.</param>
+ /// <param name="contentType">The content type of the file.</param>
+ /// <returns>An <see cref="ActionResult"/> the file.</returns>
+ public static ActionResult GetStaticFileResult(
+ string path,
+ string contentType)
+ {
+ return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
+ }
- /// <summary>
- /// Returns a transcoded file from the server.
- /// </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="httpContext">The current http context.</param>
- /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
- /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
- /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
- /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
- /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
- public static async Task<ActionResult> GetTranscodedFile(
- StreamState state,
- bool isHeadRequest,
- HttpContext httpContext,
- TranscodingJobHelper transcodingJobHelper,
- string ffmpegCommandLineArguments,
- TranscodingJobType transcodingJobType,
- CancellationTokenSource cancellationTokenSource)
- {
- // Use the command line args with a dummy playlist path
- var outputPath = state.OutputFilePath;
+ /// <summary>
+ /// Returns a transcoded file from the server.
+ /// </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="httpContext">The current http context.</param>
+ /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+ /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
+ /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+ /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
+ /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
+ public static async Task<ActionResult> GetTranscodedFile(
+ StreamState state,
+ bool isHeadRequest,
+ HttpContext httpContext,
+ TranscodingJobHelper transcodingJobHelper,
+ string ffmpegCommandLineArguments,
+ TranscodingJobType transcodingJobType,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ // Use the command line args with a dummy playlist path
+ var outputPath = state.OutputFilePath;
- httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
+ httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
- var contentType = state.GetMimeType(outputPath);
+ var contentType = state.GetMimeType(outputPath);
- // Headers only
- if (isHeadRequest)
- {
- httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
- return new OkResult();
- }
+ // Headers only
+ if (isHeadRequest)
+ {
+ httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+ return new OkResult();
+ }
- var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- try
+ var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ try
+ {
+ TranscodingJobDto? job;
+ if (!File.Exists(outputPath))
{
- TranscodingJobDto? job;
- if (!File.Exists(outputPath))
- {
- job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
- }
- else
- {
- job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
- state.Dispose();
- }
-
- var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
- return new FileStreamResult(stream, contentType);
+ job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
}
- finally
+ else
{
- transcodingLock.Release();
+ job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+ state.Dispose();
}
+
+ var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
+ }
+ finally
+ {
+ transcodingLock.Release();
}
}
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index a5369c441..995488397 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -2,168 +2,181 @@
using System.Globalization;
using System.Text;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Hls Codec string helpers.
+/// </summary>
+public static class HlsCodecStringHelpers
{
/// <summary>
- /// Hls Codec string helpers.
+ /// 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 static class HlsCodecStringHelpers
+ public const string FLAC = "flac";
+
+ /// <summary>
+ /// Codec name for ALAC.
+ /// </summary>
+ public const string ALAC = "alac";
+
+ /// <summary>
+ /// Codec name for OPUS.
+ /// </summary>
+ public const string OPUS = "opus";
+
+ /// <summary>
+ /// Gets a MP3 codec string.
+ /// </summary>
+ /// <returns>MP3 codec string.</returns>
+ public static string GetMP3String()
{
- /// <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 MP3;
+ }
+
+ /// <summary>
+ /// Gets an AAC codec string.
+ /// </summary>
+ /// <param name="profile">AAC profile.</param>
+ /// <returns>AAC codec string.</returns>
+ public static string GetAACString(string? profile)
+ {
+ StringBuilder result = new StringBuilder("mp4a", 9);
+
+ if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
{
- return MP3;
+ result.Append(".40.5");
}
-
- /// <summary>
- /// Gets an AAC codec string.
- /// </summary>
- /// <param name="profile">AAC profile.</param>
- /// <returns>AAC codec string.</returns>
- public static string GetAACString(string? profile)
+ else
{
- StringBuilder result = new StringBuilder("mp4a", 9);
-
- if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".40.5");
- }
- else
- {
- // Default to LC if profile is invalid
- result.Append(".40.2");
- }
-
- return result.ToString();
+ // Default to LC if profile is invalid
+ result.Append(".40.2");
}
- /// <summary>
- /// Gets an AC-3 codec string.
- /// </summary>
- /// <returns>AC-3 codec string.</returns>
- public static string GetAC3String()
+ return result.ToString();
+ }
+
+ /// <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 an OPUS codec string.
+ /// </summary>
+ /// <returns>OPUS codec string.</returns>
+ public static string GetOPUSString()
+ {
+ return OPUS;
+ }
+
+ /// <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)
+ {
+ StringBuilder result = new StringBuilder("avc1", 11);
+
+ if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
{
- return AC3;
+ result.Append(".6400");
}
-
- /// <summary>
- /// Gets an E-AC-3 codec string.
- /// </summary>
- /// <returns>E-AC-3 codec string.</returns>
- public static string GetEAC3String()
+ else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
{
- return EAC3;
+ result.Append(".4D40");
}
-
- /// <summary>
- /// Gets an FLAC codec string.
- /// </summary>
- /// <returns>FLAC codec string.</returns>
- public static string GetFLACString()
+ else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
{
- return FLAC;
+ result.Append(".42E0");
}
-
- /// <summary>
- /// Gets an ALAC codec string.
- /// </summary>
- /// <returns>ALAC codec string.</returns>
- public static string GetALACString()
+ else
{
- return ALAC;
+ // Default to constrained baseline if profile is invalid
+ result.Append(".4240");
}
- /// <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)
+ string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
+ result.Append(levelHex);
+
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Gets a H.265 codec string.
+ /// </summary>
+ /// <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)
+ {
+ // 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("hvc1", 16);
+
+ if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
{
- StringBuilder result = new StringBuilder("avc1", 11);
-
- if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".6400");
- }
- else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".4D40");
- }
- else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".42E0");
- }
- else
- {
- // Default to constrained baseline if profile is invalid
- result.Append(".4240");
- }
-
- string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
- result.Append(levelHex);
-
- return result.ToString();
+ result.Append(".2.4");
}
-
- /// <summary>
- /// Gets a H.265 codec string.
- /// </summary>
- /// <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)
+ else
{
- // 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("hvc1", 16);
-
- if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
- || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
- {
- result.Append(".2.4");
- }
- else
- {
- // Default to main if profile is invalid
- result.Append(".1.4");
- }
-
- result.Append(".L")
- .Append(level)
- .Append(".B0");
-
- return result.ToString();
+ // Default to main if profile is invalid
+ result.Append(".1.4");
}
+
+ result.Append(".L")
+ .Append(level)
+ .Append(".B0");
+
+ return result.ToString();
}
}
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index 456762147..2155e305d 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -8,131 +8,130 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// The hls helpers.
+/// </summary>
+public static class HlsHelpers
{
/// <summary>
- /// The hls helpers.
+ /// Waits for a minimum number of segments to be available.
/// </summary>
- public static class HlsHelpers
+ /// <param name="playlist">The playlist string.</param>
+ /// <param name="segmentCount">The segment count.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+ /// <returns>A <see cref="Task"/> indicating the waiting process.</returns>
+ public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken)
{
- /// <summary>
- /// Waits for a minimum number of segments to be available.
- /// </summary>
- /// <param name="playlist">The playlist string.</param>
- /// <param name="segmentCount">The segment count.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
- /// <returns>A <see cref="Task"/> indicating the waiting process.</returns>
- public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken)
- {
- logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
+ logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
- while (!cancellationToken.IsCancellationRequested)
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
{
- try
+ // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
+ var fileStream = new FileStream(
+ playlist,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ IODefaults.FileStreamBufferSize,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+ await using (fileStream.ConfigureAwait(false))
{
- // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
- var fileStream = new FileStream(
- playlist,
- FileMode.Open,
- FileAccess.Read,
- FileShare.ReadWrite,
- IODefaults.FileStreamBufferSize,
- FileOptions.Asynchronous | FileOptions.SequentialScan);
- await using (fileStream.ConfigureAwait(false))
- {
- using var reader = new StreamReader(fileStream);
- var count = 0;
+ using var reader = new StreamReader(fileStream);
+ var count = 0;
- while (!reader.EndOfStream)
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
+ if (line is null)
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
- if (line == null)
- {
- // Nothing currently in buffer.
- break;
- }
+ // Nothing currently in buffer.
+ break;
+ }
- if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ count++;
+ if (count >= segmentCount)
{
- count++;
- if (count >= segmentCount)
- {
- logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
- return;
- }
+ logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
+ return;
}
}
}
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- catch (IOException)
- {
- // May get an error if the file is locked
}
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
- }
-
- /// <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)
+ catch (IOException)
{
- return fmp4InitFileName;
+ // May get an error if the file is locked
}
- if (OperatingSystem.IsWindows())
- {
- // on Windows
- // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
- fmp4InitFileName = outputPrefix + "-1" + outputExtension;
- }
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ /// <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;
}
- /// <summary>
- /// Gets the hls playlist text.
- /// </summary>
- /// <param name="path">The path to the playlist file.</param>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <returns>The playlist text as a string.</returns>
- public static string GetLivePlaylistText(string path, StreamState state)
+ if (OperatingSystem.IsWindows())
{
- var text = File.ReadAllText(path);
+ // on Windows
+ // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
+ fmp4InitFileName = outputPrefix + "-1" + outputExtension;
+ }
- 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);
+ return fmp4InitFileName;
+ }
- // Replace fMP4 init file URI.
- text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
- }
+ /// <summary>
+ /// Gets the hls playlist text.
+ /// </summary>
+ /// <param name="path">The path to the playlist file.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The playlist text as a string.</returns>
+ public static string GetLivePlaylistText(string path, StreamState state)
+ {
+ var text = File.ReadAllText(path);
+
+ 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);
- return text;
+ // 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 4441ae023..5910d8073 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -25,476 +25,475 @@ using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Media info helper.
+/// </summary>
+public class MediaInfoHelper
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ILogger<MediaInfoHelper> _logger;
+ private readonly INetworkManager _networkManager;
+ private readonly IDeviceManager _deviceManager;
+
/// <summary>
- /// Media info helper.
+ /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
/// </summary>
- public class MediaInfoHelper
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ public MediaInfoHelper(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IMediaSourceManager mediaSourceManager,
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<MediaInfoHelper> logger,
+ INetworkManager networkManager,
+ IDeviceManager deviceManager)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly ILogger<MediaInfoHelper> _logger;
- private readonly INetworkManager _networkManager;
- private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- public MediaInfoHelper(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder,
- IServerConfigurationManager serverConfigurationManager,
- ILogger<MediaInfoHelper> logger,
- INetworkManager networkManager,
- IDeviceManager deviceManager)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
- _mediaEncoder = mediaEncoder;
- _serverConfigurationManager = serverConfigurationManager;
- _logger = logger;
- _networkManager = networkManager;
- _deviceManager = deviceManager;
- }
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+ _mediaEncoder = mediaEncoder;
+ _serverConfigurationManager = serverConfigurationManager;
+ _logger = logger;
+ _networkManager = networkManager;
+ _deviceManager = deviceManager;
+ }
- /// <summary>
- /// Get playback info.
- /// </summary>
- /// <param name="id">Item id.</param>
- /// <param name="userId">User Id.</param>
- /// <param name="mediaSourceId">Media source id.</param>
- /// <param name="liveStreamId">Live stream id.</param>
- /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
- public async Task<PlaybackInfoResponse> GetPlaybackInfo(
- Guid id,
- Guid? userId,
- string? mediaSourceId = null,
- string? liveStreamId = null)
+ /// <summary>
+ /// Get playback info.
+ /// </summary>
+ /// <param name="id">Item id.</param>
+ /// <param name="userId">User Id.</param>
+ /// <param name="mediaSourceId">Media source id.</param>
+ /// <param name="liveStreamId">Live stream id.</param>
+ /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
+ public async Task<PlaybackInfoResponse> GetPlaybackInfo(
+ Guid id,
+ Guid? userId,
+ string? mediaSourceId = null,
+ string? liveStreamId = null)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(id);
+ var result = new PlaybackInfoResponse();
+
+ MediaSourceInfo[] mediaSources;
+ if (string.IsNullOrWhiteSpace(liveStreamId))
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = _libraryManager.GetItemById(id);
- var result = new PlaybackInfoResponse();
-
- MediaSourceInfo[] mediaSources;
- if (string.IsNullOrWhiteSpace(liveStreamId))
- {
- // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
- var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
- if (string.IsNullOrWhiteSpace(mediaSourceId))
- {
- mediaSources = mediaSourcesList.ToArray();
- }
- else
- {
- mediaSources = mediaSourcesList
- .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
- .ToArray();
- }
- }
- else
- {
- var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+ // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
+ var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
- mediaSources = new[] { mediaSource };
- }
-
- if (mediaSources.Length == 0)
+ if (string.IsNullOrWhiteSpace(mediaSourceId))
{
- result.MediaSources = Array.Empty<MediaSourceInfo>();
-
- result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+ mediaSources = mediaSourcesList.ToArray();
}
else
{
- // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
- // Should we move this directly into MediaSourceManager?
- var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
- if (mediaSourcesClone != null)
- {
- result.MediaSources = mediaSourcesClone;
- }
-
- result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ mediaSources = mediaSourcesList
+ .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
}
+ }
+ else
+ {
+ var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
- return result;
+ mediaSources = new[] { mediaSource };
}
- /// <summary>
- /// SetDeviceSpecificData.
- /// </summary>
- /// <param name="item">Item to set data for.</param>
- /// <param name="mediaSource">Media source info.</param>
- /// <param name="profile">Device profile.</param>
- /// <param name="claimsPrincipal">Current claims principal.</param>
- /// <param name="maxBitrate">Max bitrate.</param>
- /// <param name="startTimeTicks">Start time ticks.</param>
- /// <param name="mediaSourceId">Media source id.</param>
- /// <param name="audioStreamIndex">Audio stream index.</param>
- /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
- /// <param name="maxAudioChannels">Max audio channels.</param>
- /// <param name="playSessionId">Play session id.</param>
- /// <param name="userId">User id.</param>
- /// <param name="enableDirectPlay">Enable direct play.</param>
- /// <param name="enableDirectStream">Enable direct stream.</param>
- /// <param name="enableTranscoding">Enable transcoding.</param>
- /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
- /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
- /// <param name="ipAddress">Requesting IP address.</param>
- public void SetDeviceSpecificData(
- BaseItem item,
- MediaSourceInfo mediaSource,
- DeviceProfile profile,
- ClaimsPrincipal claimsPrincipal,
- int? maxBitrate,
- long startTimeTicks,
- string mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex,
- int? maxAudioChannels,
- string playSessionId,
- Guid userId,
- bool enableDirectPlay,
- bool enableDirectStream,
- bool enableTranscoding,
- bool allowVideoStreamCopy,
- bool allowAudioStreamCopy,
- IPAddress ipAddress)
+ if (mediaSources.Length == 0)
{
- var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
+ result.MediaSources = Array.Empty<MediaSourceInfo>();
- var options = new VideoOptions
- {
- MediaSources = new[] { mediaSource },
- Context = EncodingContext.Streaming,
- DeviceId = claimsPrincipal.GetDeviceId(),
- ItemId = item.Id,
- Profile = profile,
- MaxAudioChannels = maxAudioChannels,
- AllowAudioStreamCopy = allowAudioStreamCopy,
- AllowVideoStreamCopy = allowVideoStreamCopy
- };
-
- if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+ result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+ }
+ else
+ {
+ // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+ // Should we move this directly into MediaSourceManager?
+ var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+ if (mediaSourcesClone is not null)
{
- options.MediaSourceId = mediaSourceId;
- options.AudioStreamIndex = audioStreamIndex;
- options.SubtitleStreamIndex = subtitleStreamIndex;
+ result.MediaSources = mediaSourcesClone;
}
- var user = _userManager.GetUserById(userId);
+ result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ }
- if (!enableDirectPlay)
- {
- mediaSource.SupportsDirectPlay = false;
- }
+ return result;
+ }
- if (!enableDirectStream || !allowVideoStreamCopy)
- {
- mediaSource.SupportsDirectStream = false;
- }
+ /// <summary>
+ /// SetDeviceSpecificData.
+ /// </summary>
+ /// <param name="item">Item to set data for.</param>
+ /// <param name="mediaSource">Media source info.</param>
+ /// <param name="profile">Device profile.</param>
+ /// <param name="claimsPrincipal">Current claims principal.</param>
+ /// <param name="maxBitrate">Max bitrate.</param>
+ /// <param name="startTimeTicks">Start time ticks.</param>
+ /// <param name="mediaSourceId">Media source id.</param>
+ /// <param name="audioStreamIndex">Audio stream index.</param>
+ /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
+ /// <param name="maxAudioChannels">Max audio channels.</param>
+ /// <param name="playSessionId">Play session id.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="enableDirectPlay">Enable direct play.</param>
+ /// <param name="enableDirectStream">Enable direct stream.</param>
+ /// <param name="enableTranscoding">Enable transcoding.</param>
+ /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
+ /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
+ /// <param name="ipAddress">Requesting IP address.</param>
+ public void SetDeviceSpecificData(
+ BaseItem item,
+ MediaSourceInfo mediaSource,
+ DeviceProfile profile,
+ ClaimsPrincipal claimsPrincipal,
+ int? maxBitrate,
+ long startTimeTicks,
+ string mediaSourceId,
+ int? audioStreamIndex,
+ int? subtitleStreamIndex,
+ int? maxAudioChannels,
+ string playSessionId,
+ Guid userId,
+ bool enableDirectPlay,
+ bool enableDirectStream,
+ bool enableTranscoding,
+ bool allowVideoStreamCopy,
+ bool allowAudioStreamCopy,
+ IPAddress ipAddress)
+ {
+ var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
- if (!enableTranscoding)
- {
- mediaSource.SupportsTranscoding = false;
- }
+ var options = new MediaOptions
+ {
+ MediaSources = new[] { mediaSource },
+ Context = EncodingContext.Streaming,
+ DeviceId = claimsPrincipal.GetDeviceId(),
+ ItemId = item.Id,
+ Profile = profile,
+ MaxAudioChannels = maxAudioChannels,
+ AllowAudioStreamCopy = allowAudioStreamCopy,
+ AllowVideoStreamCopy = allowVideoStreamCopy
+ };
+
+ if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ options.MediaSourceId = mediaSourceId;
+ options.AudioStreamIndex = audioStreamIndex;
+ options.SubtitleStreamIndex = subtitleStreamIndex;
+ }
- if (item is Audio)
- {
- _logger.LogInformation(
- "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
- user.Username,
- user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
- }
- else
- {
- _logger.LogInformation(
- "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
- user.Username,
- user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
- user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
- user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
- }
+ var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
+ if (!enableDirectPlay)
+ {
+ mediaSource.SupportsDirectPlay = false;
+ }
- if (!options.ForceDirectStream)
- {
- // direct-stream http streaming is currently broken
- options.EnableDirectStream = false;
- }
+ if (!enableDirectStream || !allowVideoStreamCopy)
+ {
+ mediaSource.SupportsDirectStream = false;
+ }
- // Beginning of Playback Determination
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
+ if (!enableTranscoding)
+ {
+ mediaSource.SupportsTranscoding = false;
+ }
- if (streamInfo != null)
- {
- streamInfo.PlaySessionId = playSessionId;
- streamInfo.StartPositionTicks = startTimeTicks;
+ if (item is Audio)
+ {
+ _logger.LogInformation(
+ "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+ user.Username,
+ user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+ }
+ else
+ {
+ _logger.LogInformation(
+ "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
+ user.Username,
+ user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+ user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+ user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+ }
- mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
+ options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
- // 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;
+ if (!options.ForceDirectStream)
+ {
+ // direct-stream http streaming is currently broken
+ options.EnableDirectStream = false;
+ }
- mediaSource.SupportsTranscoding =
- streamInfo.PlayMethod == PlayMethod.DirectStream
- || mediaSource.TranscodingContainer != null
- || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
+ // Beginning of Playback Determination
+ var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ ? streamBuilder.GetOptimalAudioStream(options)
+ : streamBuilder.GetOptimalVideoStream(options);
- if (item is Audio)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
- {
- mediaSource.SupportsTranscoding = false;
- }
- }
- else if (item is Video)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
- {
- mediaSource.SupportsTranscoding = false;
- }
- }
+ if (streamInfo is not null)
+ {
+ streamInfo.PlaySessionId = playSessionId;
+ streamInfo.StartPositionTicks = startTimeTicks;
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
- {
- mediaSource.SupportsDirectPlay = false;
- mediaSource.SupportsDirectStream = false;
+ mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
- }
- else
+ // 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 is not null
+ || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
+
+ if (item is Audio)
+ {
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
- if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
- {
- streamInfo.PlayMethod = PlayMethod.Transcode;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
-
- if (!allowVideoStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- }
-
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
- }
+ mediaSource.SupportsTranscoding = false;
}
-
- // Do this after the above so that StartPositionTicks is set
- // The token must not be null
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
- mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
-
- foreach (var attachment in mediaSource.MediaAttachments)
+ else if (item is Video)
{
- attachment.DeliveryUrl = string.Format(
- CultureInfo.InvariantCulture,
- "/Videos/{0}/{1}/Attachments/{2}",
- item.Id,
- mediaSource.Id,
- attachment.Index);
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+ {
+ mediaSource.SupportsTranscoding = false;
+ }
}
- }
- /// <summary>
- /// Sort media source.
- /// </summary>
- /// <param name="result">Playback info response.</param>
- /// <param name="maxBitrate">Max bitrate.</param>
- public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
- {
- var originalList = result.MediaSources.ToList();
+ if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+ {
+ mediaSource.SupportsDirectPlay = false;
+ mediaSource.SupportsDirectStream = false;
- result.MediaSources = result.MediaSources.OrderBy(i =>
+ mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
+ mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+ mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+ mediaSource.TranscodingContainer = streamInfo.Container;
+ mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+ }
+ else
+ {
+ if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
{
- // Nothing beats direct playing a file
- if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
- {
- return 0;
- }
+ streamInfo.PlayMethod = PlayMethod.Transcode;
+ mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
- return 1;
- })
- .ThenBy(i =>
- {
- // Let's assume direct streaming a file is just as desirable as direct playing a remote url
- if (i.SupportsDirectPlay || i.SupportsDirectStream)
+ if (!allowVideoStreamCopy)
{
- return 0;
+ mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
}
- return 1;
- })
- .ThenBy(i =>
- {
- return i.Protocol switch
+ if (!allowAudioStreamCopy)
{
- MediaProtocol.File => 0,
- _ => 1,
- };
- })
- .ThenBy(i =>
- {
- if (maxBitrate.HasValue && i.Bitrate.HasValue)
- {
- return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+ mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
+ }
+ }
- return 1;
- })
- .ThenBy(originalList.IndexOf)
- .ToArray();
+ // Do this after the above so that StartPositionTicks is set
+ // The token must not be null
+ SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
- /// <summary>
- /// Open media source.
- /// </summary>
- /// <param name="httpContext">Http Context.</param>
- /// <param name="request">Live stream request.</param>
- /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
- public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
+ foreach (var attachment in mediaSource.MediaAttachments)
{
- var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
+ attachment.DeliveryUrl = string.Format(
+ CultureInfo.InvariantCulture,
+ "/Videos/{0}/{1}/Attachments/{2}",
+ item.Id,
+ mediaSource.Id,
+ attachment.Index);
+ }
+ }
- var profile = request.DeviceProfile;
- if (profile == null)
+ /// <summary>
+ /// Sort media source.
+ /// </summary>
+ /// <param name="result">Playback info response.</param>
+ /// <param name="maxBitrate">Max bitrate.</param>
+ public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
+ {
+ var originalList = result.MediaSources.ToList();
+
+ result.MediaSources = result.MediaSources.OrderBy(i =>
{
- var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
- if (clientCapabilities != null)
+ // Nothing beats direct playing a file
+ if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
{
- profile = clientCapabilities.DeviceProfile;
+ return 0;
}
- }
- if (profile != null)
+ return 1;
+ })
+ .ThenBy(i =>
{
- var item = _libraryManager.GetItemById(request.ItemId);
-
- SetDeviceSpecificData(
- item,
- result.MediaSource,
- profile,
- httpContext.User,
- request.MaxStreamingBitrate,
- request.StartTimeTicks ?? 0,
- result.MediaSource.Id,
- request.AudioStreamIndex,
- request.SubtitleStreamIndex,
- request.MaxAudioChannels,
- request.PlaySessionId,
- request.UserId,
- request.EnableDirectPlay,
- request.EnableDirectStream,
- true,
- true,
- true,
- httpContext.GetNormalizedRemoteIp());
- }
- else
+ // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+ if (i.SupportsDirectPlay || i.SupportsDirectStream)
+ {
+ return 0;
+ }
+
+ return 1;
+ })
+ .ThenBy(i =>
+ {
+ return i.Protocol switch
+ {
+ MediaProtocol.File => 0,
+ _ => 1,
+ };
+ })
+ .ThenBy(i =>
{
- if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+ if (maxBitrate.HasValue && i.Bitrate.HasValue)
{
- result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+ return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
}
- }
- // here was a check if (result.MediaSource != null) but Rider said it will never be null
- NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
+ return 1;
+ })
+ .ThenBy(originalList.IndexOf)
+ .ToArray();
+ }
- return result;
- }
+ /// <summary>
+ /// Open media source.
+ /// </summary>
+ /// <param name="httpContext">Http Context.</param>
+ /// <param name="request">Live stream request.</param>
+ /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
+ public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
+ {
+ var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
- /// <summary>
- /// Normalize media source container.
- /// </summary>
- /// <param name="mediaSource">Media source.</param>
- /// <param name="profile">Device profile.</param>
- /// <param name="type">Dlna profile type.</param>
- public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
+ var profile = request.DeviceProfile;
+ if (profile is null)
{
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
+ var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
+ if (clientCapabilities is not null)
+ {
+ profile = clientCapabilities.DeviceProfile;
+ }
}
- private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+ if (profile is not null)
+ {
+ var item = _libraryManager.GetItemById(request.ItemId);
+
+ SetDeviceSpecificData(
+ item,
+ result.MediaSource,
+ profile,
+ httpContext.User,
+ request.MaxStreamingBitrate,
+ request.StartTimeTicks ?? 0,
+ result.MediaSource.Id,
+ request.AudioStreamIndex,
+ request.SubtitleStreamIndex,
+ request.MaxAudioChannels,
+ request.PlaySessionId,
+ request.UserId,
+ request.EnableDirectPlay,
+ request.EnableDirectStream,
+ true,
+ true,
+ true,
+ httpContext.GetNormalizedRemoteIp());
+ }
+ else
{
- var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
- mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+ if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+ {
+ result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+ }
+ }
+
+ // here was a check if (result.MediaSource is not null) but Rider said it will never be null
+ NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
- mediaSource.TranscodeReasons = info.TranscodeReasons;
+ return result;
+ }
- foreach (var profile in profiles)
+ /// <summary>
+ /// Normalize media source container.
+ /// </summary>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="profile">Device profile.</param>
+ /// <param name="type">Dlna profile type.</param>
+ public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
+ {
+ mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
+ }
+
+ private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+ {
+ var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
+ mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+
+ mediaSource.TranscodeReasons = info.TranscodeReasons;
+
+ foreach (var profile in profiles)
+ {
+ foreach (var stream in mediaSource.MediaStreams)
{
- foreach (var stream in mediaSource.MediaStreams)
+ if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
{
- if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
- {
- stream.DeliveryMethod = profile.DeliveryMethod;
+ stream.DeliveryMethod = profile.DeliveryMethod;
- if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- stream.DeliveryUrl = profile.Url.TrimStart('-');
- stream.IsExternalUrl = profile.IsExternalUrl;
- }
+ if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ stream.DeliveryUrl = profile.Url.TrimStart('-');
+ stream.IsExternalUrl = profile.IsExternalUrl;
}
}
}
}
+ }
+
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
+ {
+ var maxBitrate = clientMaxBitrate;
+ var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
- private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
+ if (remoteClientMaxBitrate <= 0)
{
- var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
+ remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
+ }
- if (remoteClientMaxBitrate <= 0)
- {
- remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
- }
+ if (remoteClientMaxBitrate > 0)
+ {
+ var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
- if (remoteClientMaxBitrate > 0)
+ _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
+ if (!isInLocalNetwork)
{
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
-
- _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
- if (!isInLocalNetwork)
- {
- maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
- }
+ maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
}
-
- return maxBitrate;
}
+
+ return maxBitrate;
}
}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index 6f5b64ea8..d7b1c9f8b 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -6,178 +6,177 @@ using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using MediaBrowser.Model.IO;
-namespace Jellyfin.Api.Helpers
+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 Stream _stream;
+ private readonly TranscodingJobDto? _job;
+ private readonly TranscodingJobHelper? _transcodingJobHelper;
+ private readonly int _timeoutMs;
+ private bool _disposed;
+
/// <summary>
- /// A progressive file stream for transferring transcoded files as they are written to.
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
/// </summary>
- public class ProgressiveFileStream : Stream
+ /// <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>
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
{
- private readonly Stream _stream;
- private readonly TranscodingJobDto? _job;
- private readonly TranscodingJobHelper? _transcodingJobHelper;
- private readonly int _timeoutMs;
- 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>
- /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
- public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
- {
- _job = job;
- _transcodingJobHelper = transcodingJobHelper;
- _timeoutMs = timeoutMs;
+ _job = job;
+ _transcodingJobHelper = transcodingJobHelper;
+ _timeoutMs = timeoutMs;
- _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
- }
+ _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
- /// </summary>
- /// <param name="stream">The stream to progressively copy.</param>
- /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
- public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
- {
- _job = null;
- _transcodingJobHelper = null;
- _timeoutMs = timeoutMs;
- _stream = stream;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="stream">The stream to progressively copy.</param>
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
+ {
+ _job = null;
+ _transcodingJobHelper = null;
+ _timeoutMs = timeoutMs;
+ _stream = stream;
+ }
- /// <inheritdoc />
- public override bool CanRead => _stream.CanRead;
+ /// <inheritdoc />
+ public override bool CanRead => _stream.CanRead;
- /// <inheritdoc />
- public override bool CanSeek => false;
+ /// <inheritdoc />
+ public override bool CanSeek => false;
- /// <inheritdoc />
- public override bool CanWrite => false;
+ /// <inheritdoc />
+ public override bool CanWrite => false;
- /// <inheritdoc />
- public override long Length => throw new NotSupportedException();
+ /// <inheritdoc />
+ public override long Length => throw new NotSupportedException();
- /// <inheritdoc />
- public override long Position
- {
- get => throw new NotSupportedException();
- set => throw new NotSupportedException();
- }
+ /// <inheritdoc />
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
- /// <inheritdoc />
- public override void Flush()
- {
- // Not supported
- }
+ /// <inheritdoc />
+ public override void Flush()
+ {
+ // Not supported
+ }
- /// <inheritdoc />
- public override int Read(byte[] buffer, int offset, int count)
- => Read(buffer.AsSpan(offset, count));
+ /// <inheritdoc />
+ public override int Read(byte[] buffer, int offset, int count)
+ => Read(buffer.AsSpan(offset, count));
- /// <inheritdoc />
- public override int Read(Span<byte> buffer)
- {
- int totalBytesRead = 0;
- var stopwatch = Stopwatch.StartNew();
+ /// <inheritdoc />
+ public override int Read(Span<byte> buffer)
+ {
+ int totalBytesRead = 0;
+ var stopwatch = Stopwatch.StartNew();
- while (true)
+ while (true)
+ {
+ totalBytesRead += _stream.Read(buffer);
+ if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
{
- totalBytesRead += _stream.Read(buffer);
- if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
- {
- break;
- }
-
- Thread.Sleep(50);
+ break;
}
- UpdateBytesWritten(totalBytesRead);
-
- return totalBytesRead;
+ Thread.Sleep(50);
}
- /// <inheritdoc />
- public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
+ UpdateBytesWritten(totalBytesRead);
- /// <inheritdoc />
- public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
- {
- int totalBytesRead = 0;
- var stopwatch = Stopwatch.StartNew();
+ return totalBytesRead;
+ }
- while (true)
- {
- totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
- {
- break;
- }
+ /// <inheritdoc />
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
- }
+ /// <inheritdoc />
+ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ int totalBytesRead = 0;
+ var stopwatch = Stopwatch.StartNew();
- UpdateBytesWritten(totalBytesRead);
+ while (true)
+ {
+ totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+ if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
+ {
+ break;
+ }
- return totalBytesRead;
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
- /// <inheritdoc />
- public override long Seek(long offset, SeekOrigin origin)
- => throw new NotSupportedException();
+ UpdateBytesWritten(totalBytesRead);
+
+ 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 SetLength(long value)
+ => throw new NotSupportedException();
- /// <inheritdoc />
- public override void Write(byte[] buffer, int offset, int count)
- => throw new NotSupportedException();
+ /// <inheritdoc />
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
- /// <inheritdoc />
- protected override void Dispose(bool disposing)
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
{
- if (_disposed)
- {
- return;
- }
+ return;
+ }
- try
+ try
+ {
+ if (disposing)
{
- if (disposing)
- {
- _stream.Dispose();
+ _stream.Dispose();
- if (_job != null)
- {
- _transcodingJobHelper?.OnTranscodeEndRequest(_job);
- }
+ if (_job is not null)
+ {
+ _transcodingJobHelper?.OnTranscodeEndRequest(_job);
}
}
- finally
- {
- _disposed = true;
- base.Dispose(disposing);
- }
}
-
- private void UpdateBytesWritten(int totalBytesRead)
+ finally
{
- if (_job != null)
- {
- _job.BytesDownloaded += totalBytesRead;
- }
+ _disposed = true;
+ base.Dispose(disposing);
}
+ }
- private bool StopReading(int bytesRead, long elapsed)
+ private void UpdateBytesWritten(int totalBytesRead)
+ {
+ if (_job is not null)
{
- // 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);
+ _job.BytesDownloaded += totalBytesRead;
}
}
+
+ private bool StopReading(int bytesRead, long elapsed)
+ {
+ // 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/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 8c5af013a..57098edba 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -11,138 +11,169 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Request Extensions.
+/// </summary>
+public static class RequestHelpers
{
/// <summary>
- /// Request Extensions.
+ /// Get Order By.
/// </summary>
- public static class RequestHelpers
+ /// <param name="sortBy">Sort By. Comma delimited string.</param>
+ /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
+ /// <returns>Order By.</returns>
+ public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
{
- /// <summary>
- /// Get Order By.
- /// </summary>
- /// <param name="sortBy">Sort By. Comma delimited string.</param>
- /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
- /// <returns>Order By.</returns>
- public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
+ if (sortBy.Count == 0)
{
- if (sortBy.Count == 0)
- {
- return Array.Empty<(string, SortOrder)>();
- }
+ return Array.Empty<(string, SortOrder)>();
+ }
- var result = new (string, SortOrder)[sortBy.Count];
- var i = 0;
- // Add elements which have a SortOrder specified
- for (; i < requestedSortOrder.Count; i++)
- {
- result[i] = (sortBy[i], requestedSortOrder[i]);
- }
+ var result = new (string, SortOrder)[sortBy.Count];
+ var i = 0;
+ // Add elements which have a SortOrder specified
+ for (; i < requestedSortOrder.Count; i++)
+ {
+ result[i] = (sortBy[i], requestedSortOrder[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++)
- {
- result[i] = (sortBy[i], order);
- }
+ // 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++)
+ {
+ result[i] = (sortBy[i], order);
+ }
- return result;
+ return result;
+ }
+
+ /// <summary>
+ /// Checks if the user can access a user.
+ /// </summary>
+ /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>A <see cref="bool"/> whether the user can access the user.</returns>
+ internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId)
+ {
+ var authenticatedUserId = claimsPrincipal.GetUserId();
+
+ // UserId not provided, fall back to authenticated user id.
+ if (userId is null || userId.Value.Equals(default))
+ {
+ return authenticatedUserId;
}
- /// <summary>
- /// Checks if the user can update an entry.
- /// </summary>
- /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
- /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
- internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences)
+ // User must be administrator to access another user.
+ var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
+ if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator)
{
- var authenticatedUserId = claimsPrincipal.GetUserId();
- var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
+ throw new SecurityException("Forbidden");
+ }
- // If they're going to update the record of another user, they must be an administrator
- if (!userId.Equals(authenticatedUserId) && !isAdministrator)
- {
- return false;
- }
+ return userId.Value;
+ }
- // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere
- if (!restrictUserPreferences || isAdministrator)
- {
- return true;
- }
+ /// <summary>
+ /// Checks if the user can update an entry.
+ /// </summary>
+ /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
+ /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
+ internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences)
+ {
+ var authenticatedUserId = claimsPrincipal.GetUserId();
+ var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
- var user = userManager.GetUserById(userId);
- return user.EnableUserPreferenceAccess;
+ // If they're going to update the record of another user, they must be an administrator
+ if (!userId.Equals(authenticatedUserId) && !isAdministrator)
+ {
+ return false;
}
- internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere
+ if (!restrictUserPreferences || isAdministrator)
{
- var userId = httpContext.User.GetUserId();
- var user = userManager.GetUserById(userId);
- var session = await sessionManager.LogSessionActivity(
- httpContext.User.GetClient(),
- httpContext.User.GetVersion(),
- httpContext.User.GetDeviceId(),
- httpContext.User.GetDevice(),
- httpContext.GetNormalizedRemoteIp().ToString(),
- user).ConfigureAwait(false);
-
- if (session == null)
- {
- throw new ArgumentException("Session not found.");
- }
-
- return session;
+ return true;
}
- internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ var user = userManager.GetUserById(userId);
+ if (user is null)
{
- var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false);
+ throw new ResourceNotFoundException();
+ }
+
+ return user.EnableUserPreferenceAccess;
+ }
- return session.Id;
+ internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ {
+ var userId = httpContext.User.GetUserId();
+ var user = userManager.GetUserById(userId);
+ var session = await sessionManager.LogSessionActivity(
+ httpContext.User.GetClient(),
+ httpContext.User.GetVersion(),
+ httpContext.User.GetDeviceId(),
+ httpContext.User.GetDevice(),
+ httpContext.GetNormalizedRemoteIp().ToString(),
+ user).ConfigureAwait(false);
+
+ if (session is null)
+ {
+ throw new ResourceNotFoundException("Session not found.");
}
- internal static QueryResult<BaseItemDto> CreateQueryResult(
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result,
- DtoOptions dtoOptions,
- IDtoService dtoService,
- bool includeItemTypes,
- User? user)
+ return session;
+ }
+
+ internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ {
+ var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false);
+
+ return session.Id;
+ }
+
+ internal static QueryResult<BaseItemDto> CreateQueryResult(
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result,
+ DtoOptions dtoOptions,
+ IDtoService dtoService,
+ bool includeItemTypes,
+ User? user)
+ {
+ var dtos = result.Items.Select(i =>
{
- var dtos = result.Items.Select(i =>
+ var (baseItem, counts) = i;
+ var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+ if (includeItemTypes)
{
- var (baseItem, counts) = i;
- var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- 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>(
- result.StartIndex,
- result.TotalRecordCount,
- dtos.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>(
+ result.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 370573773..9c91dcc6f 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -22,761 +22,766 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// The streaming helpers.
+/// </summary>
+public static class StreamingHelpers
{
/// <summary>
- /// The streaming helpers.
+ /// Gets the current streaming state.
/// </summary>
- public static class StreamingHelpers
+ /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
+ /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <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="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>
+ /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+ /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
+ public static async Task<StreamState> GetStreamingState(
+ StreamingRequestDto streamingRequest,
+ HttpContext httpContext,
+ IMediaSourceManager mediaSourceManager,
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ EncodingHelper encodingHelper,
+ IDlnaManager dlnaManager,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ TranscodingJobType transcodingJobType,
+ CancellationToken cancellationToken)
{
- /// <summary>
- /// Gets the current streaming state.
- /// </summary>
- /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
- /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <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="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>
- /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
- /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
- /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
- public static async Task<StreamState> GetStreamingState(
- StreamingRequestDto streamingRequest,
- HttpContext httpContext,
- IMediaSourceManager mediaSourceManager,
- IUserManager userManager,
- ILibraryManager libraryManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- EncodingHelper encodingHelper,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- TranscodingJobType transcodingJobType,
- CancellationToken cancellationToken)
- {
- var httpRequest = httpContext.Request;
- // Parse the DLNA time seek header
- if (!streamingRequest.StartTimeTicks.HasValue)
- {
- var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
+ var httpRequest = httpContext.Request;
+ // Parse the DLNA time seek header
+ if (!streamingRequest.StartTimeTicks.HasValue)
+ {
+ var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
- streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
- }
+ streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
+ }
- if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
- {
- ParseParams(streamingRequest);
- }
+ if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
+ {
+ ParseParams(streamingRequest);
+ }
- streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
- if (httpRequest.Path.Value == null)
- {
- throw new ResourceNotFoundException(nameof(httpRequest.Path));
- }
+ streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
+ if (httpRequest.Path.Value is null)
+ {
+ throw new ResourceNotFoundException(nameof(httpRequest.Path));
+ }
- var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
+ var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
- if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
- {
- streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
- }
+ if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
+ {
+ streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
+ }
- var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
- streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
- string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
+ var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
+ streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
+ string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
- var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
- {
- Request = streamingRequest,
- RequestedUrl = url,
- UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
- EnableDlnaHeaders = enableDlnaHeaders
- };
-
- var userId = httpContext.User.GetUserId();
- if (!userId.Equals(default))
- {
- state.User = userManager.GetUserById(userId);
- }
+ var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
+ {
+ Request = streamingRequest,
+ RequestedUrl = url,
+ UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
+ EnableDlnaHeaders = enableDlnaHeaders
+ };
+
+ var userId = httpContext.User.GetUserId();
+ if (!userId.Equals(default))
+ {
+ state.User = userManager.GetUserById(userId);
+ }
- if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
- {
- state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
- }
+ if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
+ {
+ state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+ }
- if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
- {
- state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
- ?? state.SupportedAudioCodecs.FirstOrDefault();
- }
+ if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
+ {
+ state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ 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(mediaEncoder.CanEncodeToSubtitleCodec)
- ?? state.SupportedSubtitleCodecs.FirstOrDefault();
- }
+ if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
+ {
+ state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)
+ ?? state.SupportedSubtitleCodecs.FirstOrDefault();
+ }
- var item = libraryManager.GetItemById(streamingRequest.Id);
+ var item = libraryManager.GetItemById(streamingRequest.Id);
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
- MediaSourceInfo? mediaSource = null;
- if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
- {
- var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
- ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
- : null;
+ MediaSourceInfo? mediaSource = null;
+ if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
+ {
+ var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
+ ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
+ : null;
- if (currentJob != null)
- {
- mediaSource = currentJob.MediaSource;
- }
+ if (currentJob is not null)
+ {
+ mediaSource = currentJob.MediaSource;
+ }
- if (mediaSource == null)
- {
- var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
+ if (mediaSource is null)
+ {
+ var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
- mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
- ? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
+ mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
+ ? mediaSources[0]
+ : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
- if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
- {
- mediaSource = mediaSources[0];
- }
+ if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
+ {
+ mediaSource = mediaSources[0];
}
}
- else
- {
- var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
- mediaSource = liveStreamInfo.Item1;
- state.DirectStreamProvider = liveStreamInfo.Item2;
- }
+ }
+ else
+ {
+ var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
+ mediaSource = liveStreamInfo.Item1;
+ state.DirectStreamProvider = liveStreamInfo.Item2;
+ }
- var encodingOptions = serverConfigurationManager.GetEncodingOptions();
+ var encodingOptions = serverConfigurationManager.GetEncodingOptions();
- encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
+ encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
- string? containerInternal = Path.GetExtension(state.RequestedUrl);
+ string? containerInternal = Path.GetExtension(state.RequestedUrl);
- if (!string.IsNullOrEmpty(streamingRequest.Container))
- {
- containerInternal = streamingRequest.Container;
- }
+ if (!string.IsNullOrEmpty(streamingRequest.Container))
+ {
+ containerInternal = streamingRequest.Container;
+ }
- if (string.IsNullOrEmpty(containerInternal))
- {
- containerInternal = streamingRequest.Static ?
- StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
- : GetOutputFileExtension(state, mediaSource);
- }
+ if (string.IsNullOrEmpty(containerInternal))
+ {
+ containerInternal = streamingRequest.Static ?
+ StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
+ : GetOutputFileExtension(state, mediaSource);
+ }
- state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
+ var outputAudioCodec = streamingRequest.AudioCodec;
+ if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
+ {
+ state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
+ }
+ else
+ {
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
+ }
- state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
+ state.OutputAudioCodec = outputAudioCodec;
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
+ state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
- state.OutputAudioCodec = streamingRequest.AudioCodec;
+ if (state.VideoRequest is not null)
+ {
+ state.OutputVideoCodec = state.Request.VideoCodec;
+ state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
- state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
+ encodingHelper.TryStreamCopy(state);
- if (state.VideoRequest != null)
+ if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{
- state.OutputVideoCodec = state.Request.VideoCodec;
- state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
-
- encodingHelper.TryStreamCopy(state);
+ var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
+ && !state.VideoRequest.Height.HasValue
+ && !state.VideoRequest.MaxWidth.HasValue
+ && !state.VideoRequest.MaxHeight.HasValue;
- if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
+ if (isVideoResolutionNotRequested
+ && state.VideoStream is not null
+ && state.VideoRequest.VideoBitRate.HasValue
+ && state.VideoStream.BitRate.HasValue
+ && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
{
- 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
+ // 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)
{
- var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream?.BitRate,
- state.OutputVideoBitrate.Value,
- state.VideoRequest.MaxWidth,
- state.VideoRequest.MaxHeight);
-
- state.VideoRequest.MaxWidth = resolution.MaxWidth;
- state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ state.VideoRequest.MaxWidth = state.VideoStream?.Width;
+ state.VideoRequest.MaxHeight = state.VideoStream?.Height;
}
}
+ else
+ {
+ var resolution = ResolutionNormalizer.Normalize(
+ state.VideoStream?.BitRate,
+ state.OutputVideoBitrate.Value,
+ state.VideoRequest.MaxWidth,
+ state.VideoRequest.MaxHeight);
+
+ state.VideoRequest.MaxWidth = resolution.MaxWidth;
+ state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ }
}
+ }
- ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
+ ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
- var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
- ? GetOutputFileExtension(state, mediaSource)
- : ("." + state.OutputContainer);
+ var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
+ ? GetOutputFileExtension(state, mediaSource)
+ : ("." + state.OutputContainer);
- state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+ state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
- return state;
- }
+ return state;
+ }
- /// <summary>
- /// Adds the dlna headers.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- /// <param name="request">The <see cref="HttpRequest"/>.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public static void AddDlnaHeaders(
- StreamState state,
- IHeaderDictionary responseHeaders,
- bool isStaticallyStreamed,
- long? startTimeTicks,
- HttpRequest request,
- IDlnaManager dlnaManager)
+ /// <summary>
+ /// Adds the dlna headers.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="responseHeaders">The response headers.</param>
+ /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ /// <param name="request">The <see cref="HttpRequest"/>.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ public static void AddDlnaHeaders(
+ StreamState state,
+ IHeaderDictionary responseHeaders,
+ bool isStaticallyStreamed,
+ long? startTimeTicks,
+ HttpRequest request,
+ IDlnaManager dlnaManager)
+ {
+ if (!state.EnableDlnaHeaders)
{
- if (!state.EnableDlnaHeaders)
- {
- return;
- }
+ return;
+ }
- var profile = state.DeviceProfile;
+ var profile = state.DeviceProfile;
- StringValues transferMode = request.Headers["transferMode.dlna.org"];
- responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
- responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
+ StringValues transferMode = request.Headers["transferMode.dlna.org"];
+ responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
+ responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
- if (state.RunTimeTicks.HasValue)
+ if (state.RunTimeTicks.HasValue)
+ {
+ if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
- {
- var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
- responseHeaders.Add("MediaInfo.sec", string.Format(
- CultureInfo.InvariantCulture,
- "SEC_Duration={0};",
- Convert.ToInt32(ms)));
- }
+ var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
+ responseHeaders.Add("MediaInfo.sec", string.Format(
+ CultureInfo.InvariantCulture,
+ "SEC_Duration={0};",
+ Convert.ToInt32(ms)));
+ }
- if (!isStaticallyStreamed && profile != null)
- {
- AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
- }
+ if (!isStaticallyStreamed && profile is not null)
+ {
+ AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
}
+ }
- profile ??= dlnaManager.GetDefaultProfile();
+ profile ??= dlnaManager.GetDefaultProfile();
- var audioCodec = state.ActualOutputAudioCodec;
+ var audioCodec = state.ActualOutputAudioCodec;
- if (!state.IsVideoRequest)
- {
- responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
- profile,
- state.OutputContainer,
- audioCodec,
- state.OutputAudioBitrate,
- state.OutputAudioSampleRate,
- state.OutputAudioChannels,
- state.OutputAudioBitDepth,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TranscodeSeekInfo));
- }
- else
- {
- var videoCodec = state.ActualOutputVideoCodec;
+ if (!state.IsVideoRequest)
+ {
+ responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
+ profile,
+ state.OutputContainer,
+ audioCodec,
+ state.OutputAudioBitrate,
+ state.OutputAudioSampleRate,
+ state.OutputAudioChannels,
+ state.OutputAudioBitDepth,
+ isStaticallyStreamed,
+ state.RunTimeTicks,
+ state.TranscodeSeekInfo));
+ }
+ else
+ {
+ var videoCodec = state.ActualOutputVideoCodec;
- responseHeaders.Add(
- "contentFeatures.dlna.org",
- ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, 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);
- }
+ responseHeaders.Add(
+ "contentFeatures.dlna.org",
+ ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, 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);
}
+ }
- /// <summary>
- /// Parses the time seek header.
- /// </summary>
- /// <param name="value">The time seek header string.</param>
- /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
- private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
+ /// <summary>
+ /// Parses the time seek header.
+ /// </summary>
+ /// <param name="value">The time seek header string.</param>
+ /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
+ private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
+ {
+ if (value.IsEmpty)
{
- if (value.IsEmpty)
- {
- return null;
- }
+ return null;
+ }
- const string npt = "npt=";
- if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Invalid timeseek header");
- }
+ const string npt = "npt=";
+ if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException("Invalid timeseek header");
+ }
- var index = value.IndexOf('-');
- value = index == -1
- ? value.Slice(npt.Length)
- : value.Slice(npt.Length, index - npt.Length);
- if (value.IndexOf(':') == -1)
+ var index = value.IndexOf('-');
+ value = index == -1
+ ? value.Slice(npt.Length)
+ : value.Slice(npt.Length, index - npt.Length);
+ if (!value.Contains(':'))
+ {
+ // Parses npt times in the format of '417.33'
+ if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds))
{
- // Parses npt times in the format of '417.33'
- if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
- {
- return TimeSpan.FromSeconds(seconds).Ticks;
- }
-
- throw new ArgumentException("Invalid timeseek header");
+ return TimeSpan.FromSeconds(seconds).Ticks;
}
- try
- {
- // Parses npt times in the format of '10:19:25.7'
- return TimeSpan.Parse(value).Ticks;
- }
- catch
- {
- throw new ArgumentException("Invalid timeseek header");
- }
+ throw new ArgumentException("Invalid timeseek header");
}
- /// <summary>
- /// Parses query parameters as StreamOptions.
- /// </summary>
- /// <param name="queryString">The query string.</param>
- /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
- private static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
+ try
+ {
+ // Parses npt times in the format of '10:19:25.7'
+ return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks;
+ }
+ catch
{
- Dictionary<string, string> streamOptions = new Dictionary<string, string>();
- foreach (var param in queryString)
+ throw new ArgumentException("Invalid timeseek header");
+ }
+ }
+
+ /// <summary>
+ /// Parses query parameters as StreamOptions.
+ /// </summary>
+ /// <param name="queryString">The query string.</param>
+ /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
+ private static Dictionary<string, string?> ParseStreamOptions(IQueryCollection queryString)
+ {
+ Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
+ foreach (var param in queryString)
+ {
+ if (char.IsLower(param.Key[0]))
{
- if (char.IsLower(param.Key[0]))
- {
- // This was probably not parsed initially and should be a StreamOptions
- // or the generated URL should correctly serialize it
- // TODO: This should be incorporated either in the lower framework for parsing requests
- streamOptions[param.Key] = param.Value;
- }
+ // This was probably not parsed initially and should be a StreamOptions
+ // or the generated URL should correctly serialize it
+ // TODO: This should be incorporated either in the lower framework for parsing requests
+ streamOptions[param.Key] = param.Value;
}
-
- return streamOptions;
}
- /// <summary>
- /// Adds the dlna time seek headers to the response.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
- {
- var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
- var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ return streamOptions;
+ }
- responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
- CultureInfo.InvariantCulture,
- "npt={0}-{1}/{1}",
- startSeconds,
- runtimeSeconds));
- responseHeaders.Add("X-AvailableSeekRange", string.Format(
- CultureInfo.InvariantCulture,
- "1 npt={0}-{1}",
- startSeconds,
- runtimeSeconds));
+ /// <summary>
+ /// Adds the dlna time seek headers to the response.
+ /// </summary>
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
+ /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
+ {
+ var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+
+ responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
+ CultureInfo.InvariantCulture,
+ "npt={0}-{1}/{1}",
+ startSeconds,
+ runtimeSeconds));
+ responseHeaders.Add("X-AvailableSeekRange", string.Format(
+ CultureInfo.InvariantCulture,
+ "1 npt={0}-{1}",
+ startSeconds,
+ runtimeSeconds));
+ }
+
+ /// <summary>
+ /// Gets the output file extension.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <returns>System.String.</returns>
+ private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ {
+ var ext = Path.GetExtension(state.RequestedUrl);
+
+ if (!string.IsNullOrEmpty(ext))
+ {
+ return ext;
}
- /// <summary>
- /// Gets the output file extension.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="mediaSource">The mediaSource.</param>
- /// <returns>System.String.</returns>
- private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ // Try to infer based on the desired video codec
+ if (state.IsVideoRequest)
{
- var ext = Path.GetExtension(state.RequestedUrl);
+ var videoCodec = state.Request.VideoCodec;
- if (!string.IsNullOrEmpty(ext))
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- return ext;
+ return ".ts";
}
- // Try to infer based on the desired video codec
- if (state.IsVideoRequest)
+ if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{
- var videoCodec = state.Request.VideoCodec;
-
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- return ".ts";
- }
-
- if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
- {
- return ".ogv";
- }
-
- if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
- {
- return ".webm";
- }
-
- if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
- {
- return ".asf";
- }
+ return ".ogv";
}
- // Try to infer based on the desired audio codec
- if (!state.IsVideoRequest)
+ if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
- var audioCodec = state.Request.AudioCodec;
+ return ".webm";
+ }
- if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".aac";
- }
+ if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".asf";
+ }
+ }
- if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".mp3";
- }
+ // Try to infer based on the desired audio codec
+ if (!state.IsVideoRequest)
+ {
+ var audioCodec = state.Request.AudioCodec;
- if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".ogg";
- }
+ if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".aac";
+ }
- if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return ".wma";
- }
+ if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".mp3";
}
- // Fallback to the container of mediaSource
- if (!string.IsNullOrEmpty(mediaSource?.Container))
+ if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
{
- var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
- return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
+ return ".ogg";
}
- return null;
+ if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return ".wma";
+ }
}
- /// <summary>
- /// Gets the output file path for transcoding.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="outputFileExtension">The file extension of the output file.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="deviceId">The device id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
- private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
+ // Fallback to the container of mediaSource
+ if (!string.IsNullOrEmpty(mediaSource?.Container))
{
- var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
+ var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
+ return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
+ }
- var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
- var folder = serverConfigurationManager.GetTranscodePath();
+ return null;
+ }
- return Path.Combine(folder, filename + ext);
- }
+ /// <summary>
+ /// Gets the output file path for transcoding.
+ /// </summary>
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
+ /// <param name="outputFileExtension">The file extension of the output file.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
+ private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
+ {
+ var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
- private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
- {
- if (!string.IsNullOrWhiteSpace(deviceProfileId))
- {
- state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
+ var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ var ext = outputFileExtension?.ToLowerInvariant();
+ var folder = serverConfigurationManager.GetTranscodePath();
- if (state.DeviceProfile == null)
- {
- var caps = deviceManager.GetCapabilities(deviceProfileId);
- state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
- }
- }
+ return Path.Combine(folder, filename + ext);
+ }
- var profile = state.DeviceProfile;
+ private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
+ {
+ if (!string.IsNullOrWhiteSpace(deviceProfileId))
+ {
+ state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
- if (profile == null)
+ if (state.DeviceProfile is null)
{
- // Don't use settings from the default profile.
- // Only use a specific profile if it was requested.
- return;
+ var caps = deviceManager.GetCapabilities(deviceProfileId);
+ state.DeviceProfile = caps is null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
}
+ }
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
+ var profile = state.DeviceProfile;
- var mediaProfile = !state.IsVideoRequest
- ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
- : profile.GetVideoMediaProfile(
- state.OutputContainer,
- audioCodec,
- videoCodec,
- state.OutputWidth,
- state.OutputHeight,
- state.TargetVideoBitDepth,
- state.OutputVideoBitrate,
- state.TargetVideoProfile,
- state.TargetVideoRangeType,
- state.TargetVideoLevel,
- state.TargetFramerate,
- state.TargetPacketLength,
- state.TargetTimestamp,
- state.IsTargetAnamorphic,
- state.IsTargetInterlaced,
- state.TargetRefFrames,
- state.TargetVideoStreamCount,
- state.TargetAudioStreamCount,
- state.TargetVideoCodecTag,
- state.IsTargetAVC);
-
- if (mediaProfile != null)
- {
- state.MimeType = mediaProfile.MimeType;
- }
+ if (profile is null)
+ {
+ // Don't use settings from the default profile.
+ // Only use a specific profile if it was requested.
+ return;
+ }
+
+ var audioCodec = state.ActualOutputAudioCodec;
+ var videoCodec = state.ActualOutputVideoCodec;
+
+ var mediaProfile = !state.IsVideoRequest
+ ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
+ : profile.GetVideoMediaProfile(
+ state.OutputContainer,
+ audioCodec,
+ videoCodec,
+ state.OutputWidth,
+ state.OutputHeight,
+ state.TargetVideoBitDepth,
+ state.OutputVideoBitrate,
+ state.TargetVideoProfile,
+ state.TargetVideoRangeType,
+ state.TargetVideoLevel,
+ state.TargetFramerate,
+ state.TargetPacketLength,
+ state.TargetTimestamp,
+ state.IsTargetAnamorphic,
+ state.IsTargetInterlaced,
+ state.TargetRefFrames,
+ state.TargetVideoStreamCount,
+ state.TargetAudioStreamCount,
+ state.TargetVideoCodecTag,
+ state.IsTargetAVC);
+
+ if (mediaProfile is not null)
+ {
+ state.MimeType = mediaProfile.MimeType;
+ }
- if (!(@static.HasValue && @static.Value))
+ if (!(@static.HasValue && @static.Value))
+ {
+ var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
+
+ if (transcodingProfile is not null)
{
- var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
+ state.EstimateContentLength = transcodingProfile.EstimateContentLength;
+ // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+ state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
- if (transcodingProfile != null)
+ if (state.VideoRequest is not null)
{
- state.EstimateContentLength = transcodingProfile.EstimateContentLength;
- // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
- state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
-
- if (state.VideoRequest != null)
- {
- state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
- state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
- }
+ state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
+ state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
}
}
}
+ }
- /// <summary>
- /// Parses the parameters.
- /// </summary>
- /// <param name="request">The request.</param>
- private static void ParseParams(StreamingRequestDto request)
+ /// <summary>
+ /// Parses the parameters.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ private static void ParseParams(StreamingRequestDto request)
+ {
+ if (string.IsNullOrEmpty(request.Params))
{
- if (string.IsNullOrEmpty(request.Params))
- {
- return;
- }
+ return;
+ }
- var vals = request.Params.Split(';');
+ var vals = request.Params.Split(';');
- var videoRequest = request as VideoRequestDto;
+ var videoRequest = request as VideoRequestDto;
- for (var i = 0; i < vals.Length; i++)
- {
- var val = vals[i];
+ for (var i = 0; i < vals.Length; i++)
+ {
+ var val = vals[i];
- if (string.IsNullOrWhiteSpace(val))
- {
- continue;
- }
+ if (string.IsNullOrWhiteSpace(val))
+ {
+ continue;
+ }
- switch (i)
- {
- case 0:
- request.DeviceProfileId = val;
- break;
- case 1:
- request.DeviceId = val;
- break;
- case 2:
- request.MediaSourceId = val;
- break;
- case 3:
- request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- break;
- case 4:
- if (videoRequest != null)
- {
- videoRequest.VideoCodec = val;
- }
+ switch (i)
+ {
+ case 0:
+ request.DeviceProfileId = val;
+ break;
+ case 1:
+ request.DeviceId = val;
+ break;
+ case 2:
+ request.MediaSourceId = val;
+ break;
+ case 3:
+ request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ break;
+ case 4:
+ if (videoRequest is not null)
+ {
+ videoRequest.VideoCodec = val;
+ }
- break;
- case 5:
- request.AudioCodec = val;
- break;
- case 6:
- if (videoRequest != null)
- {
- videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 5:
+ request.AudioCodec = val;
+ break;
+ case 6:
+ if (videoRequest is not null)
+ {
+ videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 7:
- if (videoRequest != null)
- {
- videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 7:
+ if (videoRequest is not null)
+ {
+ videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 8:
- if (videoRequest != null)
- {
- videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 8:
+ if (videoRequest is not null)
+ {
+ videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 9:
- request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 10:
- request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 11:
- if (videoRequest != null)
- {
- videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 9:
+ request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 10:
+ request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 11:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 12:
- if (videoRequest != null)
- {
- videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 12:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 13:
- if (videoRequest != null)
- {
- videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 13:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 14:
- request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 15:
- if (videoRequest != null)
- {
- videoRequest.Level = val;
- }
+ break;
+ case 14:
+ request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 15:
+ if (videoRequest is not null)
+ {
+ videoRequest.Level = val;
+ }
- break;
- case 16:
- if (videoRequest != null)
- {
- videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 16:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 17:
- if (videoRequest != null)
- {
- videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
- }
+ break;
+ case 17:
+ if (videoRequest is not null)
+ {
+ videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
+ }
- break;
- case 18:
- if (videoRequest != null)
- {
- videoRequest.Profile = val;
- }
+ break;
+ case 18:
+ if (videoRequest is not null)
+ {
+ videoRequest.Profile = val;
+ }
- break;
- case 19:
- // cabac no longer used
- break;
- case 20:
- request.PlaySessionId = val;
- break;
- case 21:
- // api_key
- break;
- case 22:
- request.LiveStreamId = val;
- break;
- case 23:
- // Duplicating ItemId because of MediaMonkey
- break;
- case 24:
- if (videoRequest != null)
- {
- videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 19:
+ // cabac no longer used
+ break;
+ case 20:
+ request.PlaySessionId = val;
+ break;
+ case 21:
+ // api_key
+ break;
+ case 22:
+ request.LiveStreamId = val;
+ break;
+ case 23:
+ // Duplicating ItemId because of MediaMonkey
+ break;
+ case 24:
+ if (videoRequest is not null)
+ {
+ videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 25:
- if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
+ break;
+ case 25:
+ if (!string.IsNullOrWhiteSpace(val) && videoRequest is not null)
+ {
+ if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
{
- if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
- {
- videoRequest.SubtitleMethod = method;
- }
+ videoRequest.SubtitleMethod = method;
}
+ }
- break;
- case 26:
- request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 27:
- if (videoRequest != null)
- {
- videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 26:
+ request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+ break;
+ case 27:
+ if (videoRequest is not null)
+ {
+ videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 28:
- request.Tag = val;
- break;
- case 29:
- if (videoRequest != null)
- {
- videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 28:
+ request.Tag = val;
+ break;
+ case 29:
+ if (videoRequest is not null)
+ {
+ videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 30:
- request.SubtitleCodec = val;
- break;
- case 31:
- if (videoRequest != null)
- {
- videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 30:
+ request.SubtitleCodec = val;
+ break;
+ case 31:
+ if (videoRequest is not null)
+ {
+ videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 32:
- if (videoRequest != null)
- {
- videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ break;
+ case 32:
+ if (videoRequest is not null)
+ {
+ videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
- break;
- case 33:
- request.TranscodeReasons = val;
- break;
- }
+ break;
+ case 33:
+ request.TranscodeReasons = val;
+ break;
}
}
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index f54f9eadf..cee8e0f9b 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -27,894 +27,898 @@ using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Helpers
+namespace Jellyfin.Api.Helpers;
+
+/// <summary>
+/// Transcoding job helpers.
+/// </summary>
+public class TranscodingJobHelper : IDisposable
{
/// <summary>
- /// Transcoding job helpers.
+ /// The active transcoding jobs.
+ /// </summary>
+ private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>();
+
+ /// <summary>
+ /// The transcoding locks.
/// </summary>
- public class TranscodingJobHelper : IDisposable
+ private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
+
+ private readonly IAttachmentExtractor _attachmentExtractor;
+ private readonly IApplicationPaths _appPaths;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<TranscodingJobHelper> _logger;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly IUserManager _userManager;
+
+ /// <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>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public TranscodingJobHelper(
+ IAttachmentExtractor attachmentExtractor,
+ IApplicationPaths appPaths,
+ ILogger<TranscodingJobHelper> logger,
+ IMediaSourceManager mediaSourceManager,
+ IFileSystem fileSystem,
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager serverConfigurationManager,
+ ISessionManager sessionManager,
+ EncodingHelper encodingHelper,
+ ILoggerFactory loggerFactory,
+ IUserManager userManager)
{
- /// <summary>
- /// The active transcoding jobs.
- /// </summary>
- private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>();
-
- /// <summary>
- /// The transcoding locks.
- /// </summary>
- private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
-
- private readonly IAttachmentExtractor _attachmentExtractor;
- private readonly IApplicationPaths _appPaths;
- private readonly EncodingHelper _encodingHelper;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<TranscodingJobHelper> _logger;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly ISessionManager _sessionManager;
- private readonly ILoggerFactory _loggerFactory;
- private readonly IUserManager _userManager;
-
- /// <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>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public TranscodingJobHelper(
- IAttachmentExtractor attachmentExtractor,
- IApplicationPaths appPaths,
- ILogger<TranscodingJobHelper> logger,
- IMediaSourceManager mediaSourceManager,
- IFileSystem fileSystem,
- IMediaEncoder mediaEncoder,
- IServerConfigurationManager serverConfigurationManager,
- ISessionManager sessionManager,
- EncodingHelper encodingHelper,
- ILoggerFactory loggerFactory,
- IUserManager userManager)
- {
- _attachmentExtractor = attachmentExtractor;
- _appPaths = appPaths;
- _logger = logger;
- _mediaSourceManager = mediaSourceManager;
- _fileSystem = fileSystem;
- _mediaEncoder = mediaEncoder;
- _serverConfigurationManager = serverConfigurationManager;
- _sessionManager = sessionManager;
- _encodingHelper = encodingHelper;
- _loggerFactory = loggerFactory;
- _userManager = userManager;
-
- DeleteEncodedMediaCache();
-
- sessionManager.PlaybackProgress += OnPlaybackProgress;
- sessionManager.PlaybackStart += OnPlaybackProgress;
- }
-
- /// <summary>
- /// Get transcoding job.
- /// </summary>
- /// <param name="playSessionId">Playback session id.</param>
- /// <returns>The transcoding job.</returns>
- public TranscodingJobDto? GetTranscodingJob(string playSessionId)
- {
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
- }
- }
+ _attachmentExtractor = attachmentExtractor;
+ _appPaths = appPaths;
+ _logger = logger;
+ _mediaSourceManager = mediaSourceManager;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _serverConfigurationManager = serverConfigurationManager;
+ _sessionManager = sessionManager;
+ _encodingHelper = encodingHelper;
+ _loggerFactory = loggerFactory;
+ _userManager = userManager;
+
+ DeleteEncodedMediaCache();
+
+ sessionManager.PlaybackProgress += OnPlaybackProgress;
+ sessionManager.PlaybackStart += OnPlaybackProgress;
+ }
- /// <summary>
- /// Get transcoding job.
- /// </summary>
- /// <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)
+ /// <summary>
+ /// Get transcoding job.
+ /// </summary>
+ /// <param name="playSessionId">Playback session id.</param>
+ /// <returns>The transcoding job.</returns>
+ public TranscodingJobDto? GetTranscodingJob(string playSessionId)
+ {
+ lock (_activeTranscodingJobs)
{
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- }
+ return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
}
+ }
- /// <summary>
- /// Ping transcoding job.
- /// </summary>
- /// <param name="playSessionId">Play session id.</param>
- /// <param name="isUserPaused">Is user paused.</param>
- /// <exception cref="ArgumentNullException">Play session id is null.</exception>
- public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
+ /// <summary>
+ /// Get transcoding job.
+ /// </summary>
+ /// <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)
+ {
+ lock (_activeTranscodingJobs)
{
- if (string.IsNullOrEmpty(playSessionId))
- {
- throw new ArgumentNullException(nameof(playSessionId));
- }
+ return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+
+ /// <summary>
+ /// Ping transcoding job.
+ /// </summary>
+ /// <param name="playSessionId">Play session id.</param>
+ /// <param name="isUserPaused">Is user paused.</param>
+ /// <exception cref="ArgumentNullException">Play session id is null.</exception>
+ public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(playSessionId);
- _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
+ _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
- List<TranscodingJobDto> jobs;
+ List<TranscodingJobDto> jobs;
- lock (_activeTranscodingJobs)
+ lock (_activeTranscodingJobs)
+ {
+ // This is really only needed for HLS.
+ // Progressive streams can stop on their own reliably.
+ jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+
+ foreach (var job in jobs)
+ {
+ if (isUserPaused.HasValue)
{
- // This is really only needed for HLS.
- // Progressive streams can stop on their own reliably.
- jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+ _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
+ job.IsUserPaused = isUserPaused.Value;
}
- foreach (var job in jobs)
- {
- if (isUserPaused.HasValue)
- {
- _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
- job.IsUserPaused = isUserPaused.Value;
- }
+ PingTimer(job, true);
+ }
+ }
- PingTimer(job, true);
- }
+ private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
+ {
+ if (job.HasExited)
+ {
+ job.StopKillTimer();
+ return;
}
- private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
+ var timerDuration = 10000;
+
+ if (job.Type != TranscodingJobType.Progressive)
{
- if (job.HasExited)
- {
- job.StopKillTimer();
- return;
- }
+ timerDuration = 60000;
+ }
- var timerDuration = 10000;
+ job.PingTimeout = timerDuration;
+ job.LastPingDate = DateTime.UtcNow;
- if (job.Type != TranscodingJobType.Progressive)
- {
- timerDuration = 60000;
- }
+ // Don't start the timer for playback checkins with progressive streaming
+ if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+ {
+ job.StartKillTimer(OnTranscodeKillTimerStopped);
+ }
+ else
+ {
+ job.ChangeKillTimerIfStarted();
+ }
+ }
- job.PingTimeout = timerDuration;
- job.LastPingDate = DateTime.UtcNow;
+ /// <summary>
+ /// Called when [transcode kill timer stopped].
+ /// </summary>
+ /// <param name="state">The state.</param>
+ private async void OnTranscodeKillTimerStopped(object? 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;
- // Don't start the timer for playback checkins with progressive streaming
- if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped);
- }
- else
+ if (timeSinceLastPing < job.PingTimeout)
{
- job.ChangeKillTimerIfStarted();
+ job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
+ return;
}
}
- /// <summary>
- /// Called when [transcode kill timer stopped].
- /// </summary>
- /// <param name="state">The state.</param>
- private async void OnTranscodeKillTimerStopped(object? 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;
+ _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
- if (timeSinceLastPing < job.PingTimeout)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
- return;
- }
- }
+ await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
+ }
- _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+ /// <summary>
+ /// Kills the single transcoding job.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="playSessionId">The play session identifier.</param>
+ /// <param name="deleteFiles">The delete files.</param>
+ /// <returns>Task.</returns>
+ public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
+ {
+ return KillTranscodingJobs(
+ j => string.IsNullOrWhiteSpace(playSessionId)
+ ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
+ : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase),
+ deleteFiles);
+ }
- await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Kills the transcoding jobs.
+ /// </summary>
+ /// <param name="killJob">The kill job.</param>
+ /// <param name="deleteFiles">The delete files.</param>
+ /// <returns>Task.</returns>
+ private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles)
+ {
+ var jobs = new List<TranscodingJobDto>();
- /// <summary>
- /// Kills the single transcoding job.
- /// </summary>
- /// <param name="deviceId">The device id.</param>
- /// <param name="playSessionId">The play session identifier.</param>
- /// <param name="deleteFiles">The delete files.</param>
- /// <returns>Task.</returns>
- public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
+ lock (_activeTranscodingJobs)
{
- return KillTranscodingJobs(
- j => string.IsNullOrWhiteSpace(playSessionId)
- ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
- : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase),
- deleteFiles);
+ // This is really only needed for HLS.
+ // Progressive streams can stop on their own reliably.
+ jobs.AddRange(_activeTranscodingJobs.Where(killJob));
}
- /// <summary>
- /// Kills the transcoding jobs.
- /// </summary>
- /// <param name="killJob">The kill job.</param>
- /// <param name="deleteFiles">The delete files.</param>
- /// <returns>Task.</returns>
- private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles)
+ if (jobs.Count == 0)
{
- var jobs = new List<TranscodingJobDto>();
+ return Task.CompletedTask;
+ }
- lock (_activeTranscodingJobs)
+ IEnumerable<Task> GetKillJobs()
+ {
+ foreach (var job in jobs)
{
- // This is really only needed for HLS.
- // Progressive streams can stop on their own reliably.
- jobs.AddRange(_activeTranscodingJobs.Where(killJob));
+ yield return KillTranscodingJob(job, false, deleteFiles);
}
+ }
- if (jobs.Count == 0)
- {
- return Task.CompletedTask;
- }
+ return Task.WhenAll(GetKillJobs());
+ }
- IEnumerable<Task> GetKillJobs()
- {
- foreach (var job in jobs)
- {
- yield return KillTranscodingJob(job, false, deleteFiles);
- }
- }
+ /// <summary>
+ /// Kills the transcoding job.
+ /// </summary>
+ /// <param name="job">The job.</param>
+ /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
+ /// <param name="delete">The delete.</param>
+ private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete)
+ {
+ job.DisposeKillTimer();
- return Task.WhenAll(GetKillJobs());
- }
+ _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
- /// <summary>
- /// Kills the transcoding job.
- /// </summary>
- /// <param name="job">The job.</param>
- /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
- /// <param name="delete">The delete.</param>
- private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete)
+ lock (_activeTranscodingJobs)
{
- job.DisposeKillTimer();
-
- _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+ _activeTranscodingJobs.Remove(job);
- lock (_activeTranscodingJobs)
+ if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
- _activeTranscodingJobs.Remove(job);
-
- if (job.CancellationTokenSource?.IsCancellationRequested == false)
- {
- job.CancellationTokenSource.Cancel();
- }
+ job.CancellationTokenSource.Cancel();
}
+ }
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path!);
- }
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(job.Path!);
+ }
- lock (job.ProcessLock!)
- {
- #pragma warning disable CA1849 // Can't await in lock block
- job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+ lock (job.ProcessLock!)
+ {
+#pragma warning disable CA1849 // Can't await in lock block
+ job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
- var process = job.Process;
+ var process = job.Process;
- var hasExited = job.HasExited;
+ var hasExited = job.HasExited;
- if (!hasExited)
+ if (!hasExited)
+ {
+ try
{
- try
- {
- _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
+ _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
- process!.StandardInput.WriteLine("q");
+ process!.StandardInput.WriteLine("q");
- // Need to wait because killing is asynchronous.
- if (!process.WaitForExit(5000))
- {
- _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
- process.Kill();
- }
- }
- catch (InvalidOperationException)
+ // Need to wait because killing is asynchronous.
+ if (!process.WaitForExit(5000))
{
+ _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
+ process.Kill();
}
}
- #pragma warning restore CA1849
- }
-
- if (delete(job.Path!))
- {
- await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+ catch (InvalidOperationException)
+ {
+ }
}
+#pragma warning restore CA1849
+ }
- if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
+ if (delete(job.Path!))
+ {
+ await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+ if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay)
{
- try
- {
- await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
+ var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat");
+ if (File.Exists(concatFilePath))
{
- _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
+ _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath);
+ File.Delete(concatFilePath);
}
}
}
- private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+ if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
{
- if (retryCount >= 10)
+ try
{
- return;
+ await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
}
+ }
+ }
- _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
+ private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+ {
+ if (retryCount >= 10)
+ {
+ return;
+ }
- await Task.Delay(delayMs).ConfigureAwait(false);
+ _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
- try
- {
- if (jobType == TranscodingJobType.Progressive)
- {
- DeleteProgressivePartialStreamFiles(path);
- }
- else
- {
- DeleteHlsPartialStreamFiles(path);
- }
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+ await Task.Delay(delayMs).ConfigureAwait(false);
- await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+ try
+ {
+ if (jobType == TranscodingJobType.Progressive)
+ {
+ DeleteProgressivePartialStreamFiles(path);
}
- catch (Exception ex)
+ else
{
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+ DeleteHlsPartialStreamFiles(path);
}
}
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- /// <summary>
- /// Deletes the progressive partial stream files.
- /// </summary>
- /// <param name="outputFilePath">The output file path.</param>
- private void DeleteProgressivePartialStreamFiles(string outputFilePath)
+ await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+ }
+ catch (Exception ex)
{
- if (File.Exists(outputFilePath))
- {
- _fileSystem.DeleteFile(outputFilePath);
- }
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
}
+ }
- /// <summary>
- /// Deletes the HLS partial stream files.
- /// </summary>
- /// <param name="outputFilePath">The output file path.</param>
- private void DeleteHlsPartialStreamFiles(string outputFilePath)
+ /// <summary>
+ /// Deletes the progressive partial stream files.
+ /// </summary>
+ /// <param name="outputFilePath">The output file path.</param>
+ private void DeleteProgressivePartialStreamFiles(string outputFilePath)
+ {
+ if (File.Exists(outputFilePath))
{
- var directory = Path.GetDirectoryName(outputFilePath)
- ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+ _fileSystem.DeleteFile(outputFilePath);
+ }
+ }
- var name = Path.GetFileNameWithoutExtension(outputFilePath);
+ /// <summary>
+ /// Deletes the HLS partial stream files.
+ /// </summary>
+ /// <param name="outputFilePath">The output file path.</param>
+ private void DeleteHlsPartialStreamFiles(string outputFilePath)
+ {
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
- var filesToDelete = _fileSystem.GetFilePaths(directory)
- .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+ var name = Path.GetFileNameWithoutExtension(outputFilePath);
- List<Exception>? exs = null;
- foreach (var file in filesToDelete)
+ var filesToDelete = _fileSystem.GetFilePaths(directory)
+ .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+
+ List<Exception>? exs = null;
+ foreach (var file in filesToDelete)
+ {
+ try
{
- try
- {
- _logger.LogDebug("Deleting HLS file {0}", file);
- _fileSystem.DeleteFile(file);
- }
- catch (IOException ex)
- {
- (exs ??= new List<Exception>(4)).Add(ex);
- _logger.LogError(ex, "Error deleting HLS file {Path}", file);
- }
+ _logger.LogDebug("Deleting HLS file {0}", file);
+ _fileSystem.DeleteFile(file);
}
-
- if (exs != null)
+ catch (IOException ex)
{
- throw new AggregateException("Error deleting HLS files", exs);
+ (exs ??= new List<Exception>(4)).Add(ex);
+ _logger.LogError(ex, "Error deleting HLS file {Path}", file);
}
}
- /// <summary>
- /// Report the transcoding progress to the session manager.
- /// </summary>
- /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
- /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
- /// <param name="transcodingPosition">The current transcoding position.</param>
- /// <param name="framerate">The framerate of the transcoding job.</param>
- /// <param name="percentComplete">The completion percentage of the transcode.</param>
- /// <param name="bytesTranscoded">The number of bytes transcoded.</param>
- /// <param name="bitRate">The bitrate of the transcoding job.</param>
- public void ReportTranscodingProgress(
- TranscodingJobDto job,
- StreamState state,
- TimeSpan? transcodingPosition,
- float? framerate,
- double? percentComplete,
- long? bytesTranscoded,
- int? bitRate)
- {
- var ticks = transcodingPosition?.Ticks;
-
- if (job != null)
- {
- job.Framerate = framerate;
- job.CompletionPercentage = percentComplete;
- job.TranscodingPositionTicks = ticks;
- job.BytesTranscoded = bytesTranscoded;
- job.BitRate = bitRate;
- }
+ if (exs is not null)
+ {
+ throw new AggregateException("Error deleting HLS files", exs);
+ }
+ }
- var deviceId = state.Request.DeviceId;
+ /// <summary>
+ /// Report the transcoding progress to the session manager.
+ /// </summary>
+ /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
+ /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
+ /// <param name="transcodingPosition">The current transcoding position.</param>
+ /// <param name="framerate">The framerate of the transcoding job.</param>
+ /// <param name="percentComplete">The completion percentage of the transcode.</param>
+ /// <param name="bytesTranscoded">The number of bytes transcoded.</param>
+ /// <param name="bitRate">The bitrate of the transcoding job.</param>
+ public void ReportTranscodingProgress(
+ TranscodingJobDto job,
+ StreamState state,
+ TimeSpan? transcodingPosition,
+ float? framerate,
+ double? percentComplete,
+ long? bytesTranscoded,
+ int? bitRate)
+ {
+ var ticks = transcodingPosition?.Ticks;
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
- var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
- HardwareEncodingType? hardwareAccelerationType = null;
- if (!string.IsNullOrEmpty(hardwareAccelerationTypeString)
- && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
- {
- hardwareAccelerationType = parsedHardwareAccelerationType;
- }
+ if (job is not null)
+ {
+ job.Framerate = framerate;
+ job.CompletionPercentage = percentComplete;
+ job.TranscodingPositionTicks = ticks;
+ job.BytesTranscoded = bytesTranscoded;
+ job.BitRate = bitRate;
+ }
- _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
- {
- Bitrate = bitRate ?? state.TotalOutputBitrate,
- AudioCodec = audioCodec,
- VideoCodec = videoCodec,
- Container = state.OutputContainer,
- Framerate = framerate,
- CompletionPercentage = percentComplete,
- Width = state.OutputWidth,
- Height = state.OutputHeight,
- AudioChannels = state.OutputAudioChannels,
- IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
- IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
- HardwareAccelerationType = hardwareAccelerationType,
- TranscodeReasons = state.TranscodeReasons
- });
- }
+ var deviceId = state.Request.DeviceId;
+
+ if (!string.IsNullOrWhiteSpace(deviceId))
+ {
+ var audioCodec = state.ActualOutputAudioCodec;
+ var videoCodec = state.ActualOutputVideoCodec;
+ var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
+ HardwareEncodingType? hardwareAccelerationType = null;
+ if (Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
+ {
+ hardwareAccelerationType = parsedHardwareAccelerationType;
+ }
+
+ _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
+ {
+ Bitrate = bitRate ?? state.TotalOutputBitrate,
+ AudioCodec = audioCodec,
+ VideoCodec = videoCodec,
+ Container = state.OutputContainer,
+ Framerate = framerate,
+ CompletionPercentage = percentComplete,
+ Width = state.OutputWidth,
+ Height = state.OutputHeight,
+ AudioChannels = state.OutputAudioChannels,
+ IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
+ IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+ HardwareAccelerationType = hardwareAccelerationType,
+ TranscodeReasons = state.TranscodeReasons
+ });
}
+ }
- /// <summary>
- /// 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="request">The <see cref="HttpRequest"/>.</param>
- /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <param name="workingDirectory">The working directory.</param>
- /// <returns>Task.</returns>
- public async Task<TranscodingJobDto> StartFfMpeg(
- StreamState state,
- string outputPath,
- string commandLineArguments,
- HttpRequest request,
- TranscodingJobType transcodingJobType,
- CancellationTokenSource cancellationTokenSource,
- string? workingDirectory = null)
- {
- 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);
-
- if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- var userId = request.HttpContext.User.GetUserId();
- var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
- if (user != null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
- {
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+ /// <summary>
+ /// 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="request">The <see cref="HttpRequest"/>.</param>
+ /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
+ /// <param name="workingDirectory">The working directory.</param>
+ /// <returns>Task.</returns>
+ public async Task<TranscodingJobDto> StartFfMpeg(
+ StreamState state,
+ string outputPath,
+ string commandLineArguments,
+ HttpRequest request,
+ TranscodingJobType transcodingJobType,
+ CancellationTokenSource cancellationTokenSource,
+ string? workingDirectory = null)
+ {
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(directory);
- throw new ArgumentException("User does not have access to video transcoding.");
- }
- }
+ await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
- if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
+ if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ var userId = request.HttpContext.User.GetUserId();
+ var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+ if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
- throw new ArgumentException("FFmpeg path not set.");
+ this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+
+ throw new ArgumentException("User does not have access to video transcoding.");
}
+ }
- // If subtitles get burned in fonts may need to be extracted from the media file
- if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
+
+ // If subtitles get burned in fonts may need to be extracted from the media file
+ if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+ if (state.VideoType != VideoType.Dvd)
{
- 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
+ if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
{
- StartInfo = new ProcessStartInfo
- {
- WindowStyle = ProcessWindowStyle.Hidden,
- CreateNoWindow = true,
- UseShellExecute = false,
-
- // Must consume both stdout and stderr or deadlocks may occur
- // RedirectStandardOutput = true,
- RedirectStandardError = true,
- RedirectStandardInput = true,
- FileName = _mediaEncoder.EncoderPath,
- Arguments = commandLineArguments,
- WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- };
+ 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);
- var transcodingJob = this.OnTranscodeBeginning(
- outputPath,
- state.Request.PlaySessionId,
- state.MediaSource.LiveStreamId,
- Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- transcodingJobType,
- process,
- state.Request.DeviceId,
- state,
- cancellationTokenSource);
-
- _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- var logFilePrefix = "FFmpeg.Transcode-";
- if (state.VideoRequest != null
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
- ? "FFmpeg.Remux-"
- : "FFmpeg.DirectStream-";
+ await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
+ }
- var logFilePath = Path.Combine(
- _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
- $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ WindowStyle = ProcessWindowStyle.Hidden,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+
+ // Must consume both stdout and stderr or deadlocks may occur
+ // RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ RedirectStandardInput = true,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = commandLineArguments,
+ WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ };
+
+ var transcodingJob = this.OnTranscodeBeginning(
+ outputPath,
+ state.Request.PlaySessionId,
+ state.MediaSource.LiveStreamId,
+ Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
+ transcodingJobType,
+ process,
+ state.Request.DeviceId,
+ state,
+ cancellationTokenSource);
+
+ _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ var logFilePrefix = "FFmpeg.Transcode-";
+ if (state.VideoRequest is not null
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
+ ? "FFmpeg.Remux-"
+ : "FFmpeg.DirectStream-";
+ }
- // 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, FileOptions.Asynchronous);
+ var logFilePath = Path.Combine(
+ _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
+ $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
+ // 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, FileOptions.Asynchronous);
- process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
+ var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+ var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
+ await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
- try
- {
- process.Start();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting FFmpeg");
+ process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting FFmpeg");
- throw;
- }
+ this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
- _logger.LogDebug("Launched FFmpeg process");
- state.TranscodingJob = transcodingJob;
+ throw;
+ }
- // 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);
+ _logger.LogDebug("Launched FFmpeg process");
+ state.TranscodingJob = transcodingJob;
- // 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)
- {
- await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
- }
+ // 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);
- _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
+ // 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)
+ {
+ await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
- if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
- {
- await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
+ _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
- if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
- {
- await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
+ if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
+ {
+ await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
- if (!transcodingJob.HasExited)
+ if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
{
- StartThrottler(state, transcodingJob);
+ await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
}
- else if (transcodingJob.ExitCode != 0)
- {
- throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
- }
-
- _logger.LogDebug("StartFfMpeg() finished successfully");
+ }
- return transcodingJob;
+ if (!transcodingJob.HasExited)
+ {
+ StartThrottler(state, transcodingJob);
}
+ else if (transcodingJob.ExitCode != 0)
+ {
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
+ }
+
+ _logger.LogDebug("StartFfMpeg() finished successfully");
- private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
+ return transcodingJob;
+ }
+
+ private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
+ {
+ if (EnableThrottling(state))
{
- if (EnableThrottling(state))
- {
- transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
- transcodingJob.TranscodingThrottler.Start();
- }
+ transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
+ transcodingJob.TranscodingThrottler.Start();
}
+ }
+
+ private bool EnableThrottling(StreamState state)
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- private bool EnableThrottling(StreamState state)
+ return state.InputProtocol == MediaProtocol.File &&
+ state.RunTimeTicks.HasValue &&
+ state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
+ state.IsInputVideo &&
+ state.VideoType == VideoType.VideoFile;
+ }
+
+ /// <summary>
+ /// Called when [transcode beginning].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="playSessionId">The play session identifier.</param>
+ /// <param name="liveStreamId">The live stream identifier.</param>
+ /// <param name="transcodingJobId">The transcoding job identifier.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="process">The process.</param>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="state">The state.</param>
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
+ /// <returns>TranscodingJob.</returns>
+ public TranscodingJobDto OnTranscodeBeginning(
+ string path,
+ string? playSessionId,
+ string? liveStreamId,
+ string transcodingJobId,
+ TranscodingJobType type,
+ Process process,
+ string? deviceId,
+ StreamState state,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ lock (_activeTranscodingJobs)
{
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
+ {
+ Type = type,
+ Path = path,
+ Process = process,
+ ActiveRequestCount = 1,
+ DeviceId = deviceId,
+ CancellationTokenSource = cancellationTokenSource,
+ Id = transcodingJobId,
+ PlaySessionId = playSessionId,
+ LiveStreamId = liveStreamId,
+ MediaSource = state.MediaSource
+ };
- return state.InputProtocol == MediaProtocol.File &&
- state.RunTimeTicks.HasValue &&
- state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
- state.IsInputVideo &&
- state.VideoType == VideoType.VideoFile;
- }
-
- /// <summary>
- /// Called when [transcode beginning].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="playSessionId">The play session identifier.</param>
- /// <param name="liveStreamId">The live stream identifier.</param>
- /// <param name="transcodingJobId">The transcoding job identifier.</param>
- /// <param name="type">The type.</param>
- /// <param name="process">The process.</param>
- /// <param name="deviceId">The device id.</param>
- /// <param name="state">The state.</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <returns>TranscodingJob.</returns>
- public TranscodingJobDto OnTranscodeBeginning(
- string path,
- string? playSessionId,
- string? liveStreamId,
- string transcodingJobId,
- TranscodingJobType type,
- Process process,
- string? deviceId,
- StreamState state,
- CancellationTokenSource cancellationTokenSource)
- {
- lock (_activeTranscodingJobs)
- {
- var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
- {
- Type = type,
- Path = path,
- Process = process,
- ActiveRequestCount = 1,
- DeviceId = deviceId,
- CancellationTokenSource = cancellationTokenSource,
- Id = transcodingJobId,
- PlaySessionId = playSessionId,
- LiveStreamId = liveStreamId,
- MediaSource = state.MediaSource
- };
-
- _activeTranscodingJobs.Add(job);
-
- ReportTranscodingProgress(job, state, null, null, null, null, null);
-
- return job;
- }
+ _activeTranscodingJobs.Add(job);
+
+ ReportTranscodingProgress(job, state, null, null, null, null, null);
+
+ return job;
}
+ }
- /// <summary>
- /// Called when [transcode end].
- /// </summary>
- /// <param name="job">The transcode job.</param>
- public void OnTranscodeEndRequest(TranscodingJobDto job)
+ /// <summary>
+ /// Called when [transcode end].
+ /// </summary>
+ /// <param name="job">The transcode job.</param>
+ public void OnTranscodeEndRequest(TranscodingJobDto job)
+ {
+ job.ActiveRequestCount--;
+ _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
+ if (job.ActiveRequestCount <= 0)
{
- job.ActiveRequestCount--;
- _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
- if (job.ActiveRequestCount <= 0)
- {
- PingTimer(job, false);
- }
+ PingTimer(job, false);
}
+ }
- /// <summary>
- /// <summary>
- /// The progressive
- /// </summary>
- /// Called when [transcode failed to start].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- /// <param name="state">The state.</param>
- public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
+ /// <summary>
+ /// <summary>
+ /// The progressive
+ /// </summary>
+ /// Called when [transcode failed to start].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="state">The state.</param>
+ public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
+ {
+ lock (_activeTranscodingJobs)
{
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+ var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- if (job != null)
- {
- _activeTranscodingJobs.Remove(job);
- }
- }
-
- lock (_transcodingLocks)
+ if (job is not null)
{
- _transcodingLocks.Remove(path);
+ _activeTranscodingJobs.Remove(job);
}
+ }
- if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
- {
- _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
- }
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(path);
}
- /// <summary>
- /// Processes the exited.
- /// </summary>
- /// <param name="process">The process.</param>
- /// <param name="job">The job.</param>
- /// <param name="state">The state.</param>
- private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
+ if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
- job.HasExited = true;
- job.ExitCode = process.ExitCode;
+ _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
+ }
+ }
- ReportTranscodingProgress(job, state, null, null, null, null, null);
+ /// <summary>
+ /// Processes the exited.
+ /// </summary>
+ /// <param name="process">The process.</param>
+ /// <param name="job">The job.</param>
+ /// <param name="state">The state.</param>
+ private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
+ {
+ job.HasExited = true;
+ job.ExitCode = process.ExitCode;
- _logger.LogDebug("Disposing stream resources");
- state.Dispose();
+ ReportTranscodingProgress(job, state, null, null, null, null, null);
- if (process.ExitCode == 0)
- {
- _logger.LogInformation("FFmpeg exited with code 0");
- }
- else
- {
- _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
- }
+ _logger.LogDebug("Disposing stream resources");
+ state.Dispose();
- job.Dispose();
+ if (process.ExitCode == 0)
+ {
+ _logger.LogInformation("FFmpeg exited with code 0");
}
-
- private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+ else
{
- if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
- {
- var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
- new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
+ }
- _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
+ job.Dispose();
+ }
- if (state.VideoRequest != null)
- {
- _encodingHelper.TryStreamCopy(state);
- }
- }
+ private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+ {
+ if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
+ {
+ var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
+ new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+
+ _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
- if (state.MediaSource.BufferMs.HasValue)
+ if (state.VideoRequest is not null)
{
- await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
+ _encodingHelper.TryStreamCopy(state);
}
}
- /// <summary>
- /// Called when [transcode begin request].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- /// <returns>The <see cref="TranscodingJobDto"/>.</returns>
- public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
+ if (state.MediaSource.BufferMs.HasValue)
{
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job == null)
- {
- return null;
- }
-
- OnTranscodeBeginRequest(job);
-
- return job;
- }
+ await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
}
+ }
- private void OnTranscodeBeginRequest(TranscodingJobDto job)
+ /// <summary>
+ /// Called when [transcode begin request].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>The <see cref="TranscodingJobDto"/>.</returns>
+ public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
+ {
+ lock (_activeTranscodingJobs)
{
- job.ActiveRequestCount++;
+ var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
- if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+ if (job is null)
{
- job.StopKillTimer();
+ return null;
}
+
+ OnTranscodeBeginRequest(job);
+
+ return job;
}
+ }
- /// <summary>
- /// Gets the transcoding lock.
- /// </summary>
- /// <param name="outputPath">The output path of the transcoded file.</param>
- /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
- public SemaphoreSlim GetTranscodingLock(string outputPath)
- {
- lock (_transcodingLocks)
- {
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
- {
- result = new SemaphoreSlim(1, 1);
- _transcodingLocks[outputPath] = result;
- }
+ private void OnTranscodeBeginRequest(TranscodingJobDto job)
+ {
+ job.ActiveRequestCount++;
- return result;
- }
+ if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+ {
+ job.StopKillTimer();
}
+ }
- private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
+ /// <summary>
+ /// Gets the transcoding lock.
+ /// </summary>
+ /// <param name="outputPath">The output path of the transcoded file.</param>
+ /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
+ public SemaphoreSlim GetTranscodingLock(string outputPath)
+ {
+ lock (_transcodingLocks)
{
- if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
+ if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
{
- PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+ result = new SemaphoreSlim(1, 1);
+ _transcodingLocks[outputPath] = result;
}
+
+ return result;
}
+ }
- /// <summary>
- /// Deletes the encoded media cache.
- /// </summary>
- private void DeleteEncodedMediaCache()
+ private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
+ {
+ if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{
- var path = _serverConfigurationManager.GetTranscodePath();
- if (!Directory.Exists(path))
- {
- return;
- }
+ PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+ }
+ }
- foreach (var file in _fileSystem.GetFilePaths(path, true))
- {
- _fileSystem.DeleteFile(file);
- }
+ /// <summary>
+ /// Deletes the encoded media cache.
+ /// </summary>
+ private void DeleteEncodedMediaCache()
+ {
+ var path = _serverConfigurationManager.GetTranscodePath();
+ if (!Directory.Exists(path))
+ {
+ return;
}
- /// <summary>
- /// Dispose transcoding job helper.
- /// </summary>
- public void Dispose()
+ foreach (var file in _fileSystem.GetFilePaths(path, true))
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ _fileSystem.DeleteFile(file);
}
+ }
- /// <summary>
- /// Dispose throttler.
- /// </summary>
- /// <param name="disposing">Disposing.</param>
- protected virtual void Dispose(bool disposing)
+ /// <summary>
+ /// Dispose transcoding job helper.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose throttler.
+ /// </summary>
+ /// <param name="disposing">Disposing.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
- if (disposing)
- {
- _loggerFactory.Dispose();
- _sessionManager.PlaybackProgress -= OnPlaybackProgress;
- _sessionManager.PlaybackStart -= OnPlaybackProgress;
- }
+ _loggerFactory.Dispose();
+ _sessionManager.PlaybackProgress -= OnPlaybackProgress;
+ _sessionManager.PlaybackStart -= OnPlaybackProgress;
}
}
}