aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api
diff options
context:
space:
mode:
authorPatrick Barron <barronpm@gmail.com>2023-10-31 13:26:37 -0400
committerPatrick Barron <barronpm@gmail.com>2023-12-21 12:53:50 -0500
commit9215a4d40ae24e5996a5e16dfa296b09a7befc40 (patch)
tree9412154b3cf4f7a654a8878d23d2f7a3a1d18b5f /Jellyfin.Api
parentc2081955c8b2a81eb214f321697d3462709164e0 (diff)
Add ITranscodeManager service
Diffstat (limited to 'Jellyfin.Api')
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs1
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs46
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs14
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs11
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs17
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs1
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs16
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs16
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs12
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs21
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs1
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs12
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs10
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs896
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs4
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs4
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamState.cs184
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs49
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs23
19 files changed, 102 insertions, 1236 deletions
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 5bc5330861..cd09d2bfab 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 260ed4787b..dda1e9d561 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
@@ -18,6 +19,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
@@ -50,7 +52,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
@@ -66,7 +68,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
@@ -78,7 +80,7 @@ public class DynamicHlsController : BaseJellyfinApiController
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
- TranscodingJobHelper transcodingJobHelper,
+ ITranscodeManager transcodeManager,
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper,
EncodingHelper encodingHelper,
@@ -90,7 +92,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = encodingHelper;
@@ -282,7 +284,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _transcodingJobHelper,
+ _transcodeManager,
TranscodingJobType,
cancellationToken)
.ConfigureAwait(false);
@@ -292,7 +294,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (!System.IO.File.Exists(playlistPath))
{
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
@@ -301,11 +303,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg
try
{
- job = await _transcodingJobHelper.StartFfMpeg(
+ job = await _transcodeManager.StartFfMpeg(
state,
playlistPath,
GetCommandLineArguments(playlistPath, state, true, 0),
- Request,
+ Request.HttpContext.User.GetUserId(),
TranscodingJobType,
cancellationTokenSource)
.ConfigureAwait(false);
@@ -330,11 +332,11 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job is not null)
{
- _transcodingJobHelper.OnTranscodeEndRequest(job);
+ _transcodeManager.OnTranscodeEndRequest(job);
}
var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
@@ -1382,7 +1384,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _transcodingJobHelper,
+ _transcodeManager,
TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -1420,7 +1422,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _transcodingJobHelper,
+ _transcodeManager,
TranscodingJobType,
cancellationToken)
.ConfigureAwait(false);
@@ -1435,12 +1437,12 @@ public class DynamicHlsController : BaseJellyfinApiController
if (System.IO.File.Exists(segmentPath))
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var released = false;
var startTranscoding = false;
@@ -1449,7 +1451,7 @@ public class DynamicHlsController : BaseJellyfinApiController
{
if (System.IO.File.Exists(segmentPath))
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@@ -1487,7 +1489,7 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg
try
{
- await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
+ await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
.ConfigureAwait(false);
if (currentTranscodingIndex.HasValue)
@@ -1498,11 +1500,11 @@ public class DynamicHlsController : BaseJellyfinApiController
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
state.WaitForPath = segmentPath;
- job = await _transcodingJobHelper.StartFfMpeg(
+ job = await _transcodeManager.StartFfMpeg(
state,
playlistPath,
GetCommandLineArguments(playlistPath, state, false, segmentId),
- Request,
+ Request.HttpContext.User.GetUserId(),
TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
@@ -1516,7 +1518,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
else
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler is not null)
{
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@@ -1533,7 +1535,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
_logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -2000,7 +2002,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (transcodingJob is not null)
{
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
- _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+ _transcodeManager.OnTranscodeEndRequest(transcodingJob);
}
return Task.CompletedTask;
@@ -2011,7 +2013,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
- var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
+ var job = _transcodeManager.GetTranscodingJob(playlist, TranscodingJobType);
if (job is null || job.HasExited)
{
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 392d9955fb..1927a332b2 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -24,22 +24,22 @@ public class HlsSegmentController : BaseJellyfinApiController
{
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
/// <summary>
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public HlsSegmentController(
IFileSystem fileSystem,
IServerConfigurationManager serverConfigurationManager,
- TranscodingJobHelper transcodingJobHelper)
+ ITranscodeManager transcodeManager)
{
_fileSystem = fileSystem;
_serverConfigurationManager = serverConfigurationManager;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
}
/// <summary>
@@ -112,7 +112,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId)
{
- _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
+ _transcodeManager.KillTranscodingJobs(deviceId, playSessionId, _ => true);
return NoContent();
}
@@ -174,13 +174,13 @@ public class HlsSegmentController : BaseJellyfinApiController
private ActionResult GetFileResult(string path, string playlistPath)
{
- var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+ var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
Response.OnCompleted(() =>
{
if (transcodingJob is not null)
{
- _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+ _transcodeManager.OnTranscodeEndRequest(transcodingJob);
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 425086895b..a40f273ae4 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -47,7 +48,7 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IDtoService _dtoService;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -59,7 +60,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@@ -68,7 +69,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper)
+ ITranscodeManager transcodeManager)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@@ -77,7 +78,7 @@ public class LiveTvController : BaseJellyfinApiController
_dtoService = dtoService;
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
}
/// <summary>
@@ -1171,7 +1172,7 @@ public class LiveTvController : BaseJellyfinApiController
return NotFound();
}
- var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
+ var stream = new ProgressiveFileStream(path, null, _transcodeManager);
return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 8ad553bcb8..bde2f4d1ac 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
@@ -30,7 +31,7 @@ public class PlaystateController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly ISessionManager _sessionManager;
private readonly ILogger<PlaystateController> _logger;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
/// <summary>
/// Initializes a new instance of the <see cref="PlaystateController"/> class.
@@ -40,14 +41,14 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
+ /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public PlaystateController(
IUserManager userManager,
IUserDataManager userDataRepository,
ILibraryManager libraryManager,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
- TranscodingJobHelper transcodingJobHelper)
+ ITranscodeManager transcodeManager)
{
_userManager = userManager;
_userDataRepository = userDataRepository;
@@ -55,7 +56,7 @@ public class PlaystateController : BaseJellyfinApiController
_sessionManager = sessionManager;
_logger = loggerFactory.CreateLogger<PlaystateController>();
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
}
/// <summary>
@@ -188,7 +189,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{
- _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
+ _transcodeManager.PingTranscodingJob(playSessionId, null);
return NoContent();
}
@@ -205,7 +206,7 @@ public class PlaystateController : BaseJellyfinApiController
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
@@ -354,7 +355,7 @@ public class PlaystateController : BaseJellyfinApiController
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
@@ -388,7 +389,7 @@ public class PlaystateController : BaseJellyfinApiController
{
if (method == PlayMethod.Transcode)
{
- var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
+ var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodeManager.GetTranscodingJob(playSessionId);
if (job is null)
{
return PlayMethod.DirectPlay;
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 7177a04403..0a416aedbe 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -11,6 +11,7 @@ using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 5d9868eb9f..c231c147fc 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -11,7 +11,6 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
-using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -20,6 +19,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -43,7 +43,7 @@ public class VideosController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly EncodingHelper _encodingHelper;
@@ -58,7 +58,7 @@ public class VideosController : BaseJellyfinApiController
/// <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="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public VideosController(
@@ -68,7 +68,7 @@ public class VideosController : BaseJellyfinApiController
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- TranscodingJobHelper transcodingJobHelper,
+ ITranscodeManager transcodeManager,
IHttpClientFactory httpClientFactory,
EncodingHelper encodingHelper)
{
@@ -78,7 +78,7 @@ public class VideosController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
_httpClientFactory = httpClientFactory;
_encodingHelper = encodingHelper;
}
@@ -427,7 +427,7 @@ public class VideosController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _transcodingJobHelper,
+ _transcodeManager,
_transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -466,7 +466,7 @@ public class VideosController : BaseJellyfinApiController
if (state.MediaSource.IsInfiniteStream)
{
- var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager);
return File(liveStream, contentType);
}
@@ -482,7 +482,7 @@ public class VideosController : BaseJellyfinApiController
state,
isHeadRequest,
HttpContext,
- _transcodingJobHelper,
+ _transcodeManager,
ffmpegCommandLineArguments,
_transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index 926ce99dd8..c80a9d582d 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -2,13 +2,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
@@ -26,7 +26,7 @@ public class AudioHelper
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper;
@@ -39,7 +39,7 @@ public class AudioHelper
/// <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="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/> interface.</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>
@@ -49,7 +49,7 @@ public class AudioHelper
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- TranscodingJobHelper transcodingJobHelper,
+ ITranscodeManager transcodeManager,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
EncodingHelper encodingHelper)
@@ -59,7 +59,7 @@ public class AudioHelper
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper;
@@ -94,7 +94,7 @@ public class AudioHelper
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _transcodingJobHelper,
+ _transcodeManager,
transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -133,7 +133,7 @@ public class AudioHelper
if (state.MediaSource.IsInfiniteStream)
{
- var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager);
return new FileStreamResult(stream, contentType);
}
@@ -149,7 +149,7 @@ public class AudioHelper
state,
isHeadRequest,
_httpContextAccessor.HttpContext,
- _transcodingJobHelper,
+ _transcodeManager,
ffmpegCommandLineArguments,
transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 05f7d44bf0..fa81fc284d 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -8,7 +8,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -18,6 +17,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@@ -39,7 +39,7 @@ public class DynamicHlsHelper
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ITranscodeManager _transcodeManager;
private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -54,7 +54,7 @@ public class DynamicHlsHelper
/// <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="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/>.</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>
@@ -66,7 +66,7 @@ public class DynamicHlsHelper
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- TranscodingJobHelper transcodingJobHelper,
+ ITranscodeManager transcodeManager,
INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger,
IHttpContextAccessor httpContextAccessor,
@@ -78,7 +78,7 @@ public class DynamicHlsHelper
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
_networkManager = networkManager;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
@@ -130,7 +130,7 @@ public class DynamicHlsHelper
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _transcodingJobHelper,
+ _transcodeManager,
transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index fafa2c0551..5385979d4a 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -4,8 +4,9 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
@@ -64,7 +65,7 @@ public static class FileStreamResponseHelpers
/// <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="transcodeManager">The <see cref="ITranscodeManager"/> 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>
@@ -73,7 +74,7 @@ public static class FileStreamResponseHelpers
StreamState state,
bool isHeadRequest,
HttpContext httpContext,
- TranscodingJobHelper transcodingJobHelper,
+ ITranscodeManager transcodeManager,
string ffmpegCommandLineArguments,
TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource)
@@ -92,22 +93,28 @@ public static class FileStreamResponseHelpers
return new OkResult();
}
- var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
+ var transcodingLock = transcodeManager.GetTranscodingLock(outputPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
TranscodingJob? job;
if (!File.Exists(outputPath))
{
- job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
+ job = await transcodeManager.StartFfMpeg(
+ state,
+ outputPath,
+ ffmpegCommandLineArguments,
+ httpContext.User.GetUserId(),
+ transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
}
else
{
- job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+ job = transcodeManager.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose();
}
- var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+ var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
return new FileStreamResult(stream, contentType);
}
finally
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index e2d3bfb193..c8a36c562e 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index 18088483d3..98ea844a96 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -15,7 +15,7 @@ public class ProgressiveFileStream : Stream
{
private readonly Stream _stream;
private readonly TranscodingJob? _job;
- private readonly TranscodingJobHelper? _transcodingJobHelper;
+ private readonly ITranscodeManager? _transcodeManager;
private readonly int _timeoutMs;
private bool _disposed;
@@ -24,12 +24,12 @@ public class ProgressiveFileStream : Stream
/// </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="transcodeManager">The transcode manager.</param>
/// <param name="timeoutMs">The timeout duration in milliseconds.</param>
- public ProgressiveFileStream(string filePath, TranscodingJob? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
+ public ProgressiveFileStream(string filePath, TranscodingJob? job, ITranscodeManager transcodeManager, int timeoutMs = 30000)
{
_job = job;
- _transcodingJobHelper = transcodingJobHelper;
+ _transcodeManager = transcodeManager;
_timeoutMs = timeoutMs;
_stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
@@ -43,7 +43,7 @@ public class ProgressiveFileStream : Stream
public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
{
_job = null;
- _transcodingJobHelper = null;
+ _transcodeManager = null;
_timeoutMs = timeoutMs;
_stream = stream;
}
@@ -153,7 +153,7 @@ public class ProgressiveFileStream : Stream
if (_job is not null)
{
- _transcodingJobHelper?.OnTranscodeEndRequest(_job);
+ _transcodeManager?.OnTranscodeEndRequest(_job);
}
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 71c62b2356..78943f7b58 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -6,7 +6,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
@@ -14,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -38,7 +38,7 @@ public static class StreamingHelpers
/// <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="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
+ /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</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>
@@ -51,7 +51,7 @@ public static class StreamingHelpers
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
EncodingHelper encodingHelper,
- TranscodingJobHelper transcodingJobHelper,
+ ITranscodeManager transcodeManager,
TranscodingJobType transcodingJobType,
CancellationToken cancellationToken)
{
@@ -74,7 +74,7 @@ public static class StreamingHelpers
streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
}
- var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
+ var state = new StreamState(mediaSourceManager, transcodingJobType, transcodeManager)
{
Request = streamingRequest,
RequestedUrl = url,
@@ -115,7 +115,7 @@ public static class StreamingHelpers
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
{
var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
- ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
+ ? transcodeManager.GetTranscodingJob(streamingRequest.PlaySessionId)
: null;
if (currentJob is not null)
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
deleted file mode 100644
index 9a6ec17fdb..0000000000
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ /dev/null
@@ -1,896 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Models.StreamingDtos;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Api.Helpers;
-
-/// <summary>
-/// Transcoding job helpers.
-/// </summary>
-public class TranscodingJobHelper : IDisposable
-{
- /// <summary>
- /// The active transcoding jobs.
- /// </summary>
- private static readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
-
- /// <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 TranscodingJob? GetTranscodingJob(string playSessionId)
- {
- lock (_activeTranscodingJobs)
- {
- return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
- }
- }
-
- /// <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 TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type)
- {
- lock (_activeTranscodingJobs)
- {
- 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);
-
- List<TranscodingJob> jobs;
-
- 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)
- {
- _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
- job.IsUserPaused = isUserPaused.Value;
- }
-
- PingTimer(job, true);
- }
- }
-
- private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
- {
- if (job.HasExited)
- {
- job.StopKillTimer();
- return;
- }
-
- var timerDuration = 10000;
-
- if (job.Type != TranscodingJobType.Progressive)
- {
- timerDuration = 60000;
- }
-
- job.PingTimeout = timerDuration;
- job.LastPingDate = DateTime.UtcNow;
-
- // Don't start the timer for playback checkins with progressive streaming
- if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped);
- }
- else
- {
- job.ChangeKillTimerIfStarted();
- }
- }
-
- /// <summary>
- /// Called when [transcode kill timer stopped].
- /// </summary>
- /// <param name="state">The state.</param>
- private async void OnTranscodeKillTimerStopped(object? state)
- {
- var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJob)}", nameof(state));
- if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
- {
- var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
-
- if (timeSinceLastPing < job.PingTimeout)
- {
- job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
- return;
- }
- }
-
- _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
- await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
- }
-
- /// <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);
- }
-
- /// <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<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
- {
- var jobs = new List<TranscodingJob>();
-
- lock (_activeTranscodingJobs)
- {
- // This is really only needed for HLS.
- // Progressive streams can stop on their own reliably.
- jobs.AddRange(_activeTranscodingJobs.Where(killJob));
- }
-
- if (jobs.Count == 0)
- {
- return Task.CompletedTask;
- }
-
- IEnumerable<Task> GetKillJobs()
- {
- foreach (var job in jobs)
- {
- yield return KillTranscodingJob(job, false, deleteFiles);
- }
- }
-
- return Task.WhenAll(GetKillJobs());
- }
-
- /// <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(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
- {
- job.DisposeKillTimer();
-
- _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
- lock (_activeTranscodingJobs)
- {
- _activeTranscodingJobs.Remove(job);
-
- if (job.CancellationTokenSource?.IsCancellationRequested == false)
- {
-#pragma warning disable CA1849 // Can't await in lock block
- job.CancellationTokenSource.Cancel();
-#pragma warning restore CA1849
- }
- }
-
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path!);
- }
-
- job.Stop();
-
- 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)
- {
- var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat");
- if (File.Exists(concatFilePath))
- {
- _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath);
- File.Delete(concatFilePath);
- }
- }
- }
-
- if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
- {
- try
- {
- await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
- }
- }
- }
-
- private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
- {
- if (retryCount >= 10)
- {
- return;
- }
-
- _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
-
- await Task.Delay(delayMs).ConfigureAwait(false);
-
- 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 DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
- }
- catch (Exception 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)
- {
- if (File.Exists(outputFilePath))
- {
- _fileSystem.DeleteFile(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 name = Path.GetFileNameWithoutExtension(outputFilePath);
-
- var filesToDelete = _fileSystem.GetFilePaths(directory)
- .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase));
-
- List<Exception>? exs = null;
- foreach (var file in filesToDelete)
- {
- 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);
- }
- }
-
- if (exs is not null)
- {
- throw new AggregateException("Error deleting HLS files", exs);
- }
- }
-
- /// <summary>
- /// Report the transcoding progress to the session manager.
- /// </summary>
- /// <param name="job">The <see cref="TranscodingJob"/> 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(
- TranscodingJob job,
- StreamState state,
- TimeSpan? transcodingPosition,
- float? framerate,
- double? percentComplete,
- long? bytesTranscoded,
- int? bitRate)
- {
- var ticks = transcodingPosition?.Ticks;
-
- if (job is not null)
- {
- job.Framerate = framerate;
- job.CompletionPercentage = percentComplete;
- job.TranscodingPositionTicks = ticks;
- job.BytesTranscoded = bytesTranscoded;
- job.BitRate = bitRate;
- }
-
- 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<TranscodingJob> 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 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))
- {
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
-
- throw new ArgumentException("User does not have access to video transcoding.");
- }
- }
-
- 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)
- {
- await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
- }
-
- if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
- {
- string subtitlePath = state.SubtitleStream.Path;
- string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
- string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- 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-";
- }
-
- var logFilePath = Path.Combine(
- _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
- $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
-
- // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
-
- 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);
-
- process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
-
- try
- {
- process.Start();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting FFmpeg");
-
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
-
- throw;
- }
-
- _logger.LogDebug("Launched FFmpeg process");
- state.TranscodingJob = transcodingJob;
-
- // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
- _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
-
- // 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);
- }
-
- _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
-
- if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
- {
- await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
-
- if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
- {
- await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- 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");
-
- return transcodingJob;
- }
-
- private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
- {
- if (EnableThrottling(state))
- {
- transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
- transcodingJob.TranscodingThrottler.Start();
- }
- }
-
- private bool EnableThrottling(StreamState state)
- {
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
-
- 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 TranscodingJob OnTranscodeBeginning(
- string path,
- string? playSessionId,
- string? liveStreamId,
- string transcodingJobId,
- TranscodingJobType type,
- Process process,
- string? deviceId,
- StreamState state,
- CancellationTokenSource cancellationTokenSource)
- {
- lock (_activeTranscodingJobs)
- {
- var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>())
- {
- 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;
- }
- }
-
- /// <summary>
- /// Called when [transcode end].
- /// </summary>
- /// <param name="job">The transcode job.</param>
- public void OnTranscodeEndRequest(TranscodingJob job)
- {
- job.ActiveRequestCount--;
- _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
- if (job.ActiveRequestCount <= 0)
- {
- 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)
- {
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job is not null)
- {
- _activeTranscodingJobs.Remove(job);
- }
- }
-
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(path);
- }
-
- if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
- {
- _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
- }
- }
-
- /// <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, TranscodingJob job, StreamState state)
- {
- job.HasExited = true;
- job.ExitCode = process.ExitCode;
-
- ReportTranscodingProgress(job, state, null, null, null, null, null);
-
- _logger.LogDebug("Disposing stream resources");
- state.Dispose();
-
- if (process.ExitCode == 0)
- {
- _logger.LogInformation("FFmpeg exited with code 0");
- }
- else
- {
- _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
- }
-
- job.Dispose();
- }
-
- 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.VideoRequest is not null)
- {
- _encodingHelper.TryStreamCopy(state);
- }
- }
-
- if (state.MediaSource.BufferMs.HasValue)
- {
- await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Called when [transcode begin request].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="type">The type.</param>
- /// <returns>The <see cref="TranscodingJob"/>.</returns>
- public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type)
- {
- lock (_activeTranscodingJobs)
- {
- var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
- if (job is null)
- {
- return null;
- }
-
- OnTranscodeBeginRequest(job);
-
- return job;
- }
- }
-
- private void OnTranscodeBeginRequest(TranscodingJob job)
- {
- job.ActiveRequestCount++;
-
- if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
- {
- job.StopKillTimer();
- }
- }
-
- /// <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;
- }
-
- return result;
- }
- }
-
- private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
- {
- if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
- {
- PingTranscodingJob(e.PlaySessionId, e.IsPaused);
- }
- }
-
- /// <summary>
- /// Deletes the encoded media cache.
- /// </summary>
- private void DeleteEncodedMediaCache()
- {
- var path = _serverConfigurationManager.GetTranscodePath();
- if (!Directory.Exists(path))
- {
- return;
- }
-
- foreach (var file in _fileSystem.GetFilePaths(path, true))
- {
- _fileSystem.DeleteFile(file);
- }
- }
-
- /// <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)
- {
- _loggerFactory.Dispose();
- _sessionManager.PlaybackProgress -= OnPlaybackProgress;
- _sessionManager.PlaybackStart -= OnPlaybackProgress;
- }
- }
-}
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
index 4f1abb1ffb..bd176bb6a5 100644
--- a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
@@ -1,4 +1,6 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.Streaming;
+
+namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary>
/// The hls video request dto.
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
index 1cd3d01323..53b6d7575b 100644
--- a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
@@ -1,4 +1,6 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.Streaming;
+
+namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary>
/// The hls video request dto.
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
deleted file mode 100644
index 439f8052c1..0000000000
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-using System;
-using Jellyfin.Api.Helpers;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Dlna;
-
-namespace Jellyfin.Api.Models.StreamingDtos;
-
-/// <summary>
-/// The stream state dto.
-/// </summary>
-public class StreamState : EncodingJobInfo, IDisposable
-{
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StreamState" /> class.
- /// </summary>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param>
- /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param>
- /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param>
- public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper)
- : base(transcodingType)
- {
- _mediaSourceManager = mediaSourceManager;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- /// <summary>
- /// Gets or sets the requested url.
- /// </summary>
- public string? RequestedUrl { get; set; }
-
- /// <summary>
- /// Gets or sets the request.
- /// </summary>
- public StreamingRequestDto Request
- {
- get => (StreamingRequestDto)BaseRequest;
- set
- {
- BaseRequest = value;
- IsVideoRequest = VideoRequest is not null;
- }
- }
-
- /// <summary>
- /// Gets the video request.
- /// </summary>
- public VideoRequestDto? VideoRequest => Request as VideoRequestDto;
-
- /// <summary>
- /// Gets or sets the direct stream provicer.
- /// </summary>
- /// <remarks>
- /// Deprecated.
- /// </remarks>
- public IDirectStreamProvider? DirectStreamProvider { get; set; }
-
- /// <summary>
- /// Gets or sets the path to wait for.
- /// </summary>
- public string? WaitForPath { get; set; }
-
- /// <summary>
- /// Gets a value indicating whether the request outputs video.
- /// </summary>
- public bool IsOutputVideo => Request is VideoRequestDto;
-
- /// <summary>
- /// Gets the segment length.
- /// </summary>
- public int SegmentLength
- {
- get
- {
- if (Request.SegmentLength.HasValue)
- {
- return Request.SegmentLength.Value;
- }
-
- if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
- {
- var userAgent = UserAgent ?? string.Empty;
-
- if (userAgent.Contains("AppleTV", StringComparison.OrdinalIgnoreCase)
- || userAgent.Contains("cfnetwork", StringComparison.OrdinalIgnoreCase)
- || userAgent.Contains("ipad", StringComparison.OrdinalIgnoreCase)
- || userAgent.Contains("iphone", StringComparison.OrdinalIgnoreCase)
- || userAgent.Contains("ipod", StringComparison.OrdinalIgnoreCase))
- {
- return 6;
- }
-
- if (IsSegmentedLiveStream)
- {
- return 3;
- }
-
- return 6;
- }
-
- return 3;
- }
- }
-
- /// <summary>
- /// Gets the minimum number of segments.
- /// </summary>
- public int MinSegments
- {
- get
- {
- if (Request.MinSegments.HasValue)
- {
- return Request.MinSegments.Value;
- }
-
- return SegmentLength >= 10 ? 2 : 3;
- }
- }
-
- /// <summary>
- /// Gets or sets the user agent.
- /// </summary>
- public string? UserAgent { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to estimate the content length.
- /// </summary>
- public bool EstimateContentLength { get; set; }
-
- /// <summary>
- /// Gets or sets the transcode seek info.
- /// </summary>
- public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
-
- /// <summary>
- /// Gets or sets the transcoding job.
- /// </summary>
- public TranscodingJob? TranscodingJob { get; set; }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <inheritdoc />
- public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
- {
- _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
- }
-
- /// <summary>
- /// Disposes the stream state.
- /// </summary>
- /// <param name="disposing">Whether the object is currently being disposed.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- // REVIEW: Is this the right place for this?
- if (MediaSource.RequiresClosing
- && string.IsNullOrWhiteSpace(Request.LiveStreamId)
- && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
- {
- _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
- }
- }
-
- TranscodingJob = null;
-
- _disposed = true;
- }
-}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
deleted file mode 100644
index a357498d4c..0000000000
--- a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using MediaBrowser.Controller.MediaEncoding;
-
-namespace Jellyfin.Api.Models.StreamingDtos;
-
-/// <summary>
-/// The audio streaming request dto.
-/// </summary>
-public class StreamingRequestDto : BaseEncodingJobOptions
-{
- /// <summary>
- /// Gets or sets the params.
- /// </summary>
- public string? Params { get; set; }
-
- /// <summary>
- /// Gets or sets the play session id.
- /// </summary>
- public string? PlaySessionId { get; set; }
-
- /// <summary>
- /// Gets or sets the tag.
- /// </summary>
- public string? Tag { get; set; }
-
- /// <summary>
- /// Gets or sets the segment container.
- /// </summary>
- public string? SegmentContainer { get; set; }
-
- /// <summary>
- /// Gets or sets the segment length.
- /// </summary>
- public int? SegmentLength { get; set; }
-
- /// <summary>
- /// Gets or sets the min segments.
- /// </summary>
- public int? MinSegments { get; set; }
-
- /// <summary>
- /// Gets or sets the position of the requested segment in ticks.
- /// </summary>
- public long CurrentRuntimeTicks { get; set; }
-
- /// <summary>
- /// Gets or sets the actual segment length in ticks.
- /// </summary>
- public long ActualSegmentLengthTicks { get; set; }
-}
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
deleted file mode 100644
index 8548fec1a1..0000000000
--- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
-
-/// <summary>
-/// The video request dto.
-/// </summary>
-public class VideoRequestDto : StreamingRequestDto
-{
- /// <summary>
- /// Gets a value indicating whether this instance has fixed resolution.
- /// </summary>
- /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
- public bool HasFixedResolution => Width.HasValue || Height.HasValue;
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable subtitles in the manifest.
- /// </summary>
- public bool EnableSubtitlesInManifest { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable trickplay images.
- /// </summary>
- public bool EnableTrickplay { get; set; }
-}