Move AudioService to Jellyfin.Api

This commit is contained in:
David
2020-07-22 16:57:06 +02:00
parent 5580df38e6
commit 2ce97c022e
7 changed files with 236 additions and 164 deletions

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
@@ -141,10 +142,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="streamOptions">Optional. The streaming options.</param>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream.{container}")] [HttpGet("{itemId}/{stream=stream}.{container?}")]
[HttpGet("{itemId}/stream")]
[HttpHead("{itemId}/stream.{container}")]
[HttpGet("{itemId}/stream")] [HttpGet("{itemId}/stream")]
[HttpHead("{itemId}/{stream=stream}.{container?}")]
[HttpHead("{itemId}/stream")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetAudioStream( public async Task<ActionResult> GetAudioStream(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
@@ -201,21 +202,61 @@ namespace Jellyfin.Api.Controllers
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
Id = itemId,
Container = container,
Static = @static.HasValue ? @static.Value : true,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
MinSegments = minSegments,
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
EnableAutoStreamCopy = enableAutoStreamCopy.HasValue ? enableAutoStreamCopy.Value : true,
AllowAudioStreamCopy = allowAudioStreamCopy.HasValue ? allowAudioStreamCopy.Value : true,
AllowVideoStreamCopy = allowVideoStreamCopy.HasValue ? allowVideoStreamCopy.Value : true,
BreakOnNonKeyFrames = breakOnNonKeyFrames.HasValue ? breakOnNonKeyFrames.Value : false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc.HasValue ? requireAvc.Value : true,
DeInterlace = deInterlace.HasValue ? deInterlace.Value : true,
RequireNonAnamorphic = requireNonAnamorphic.HasValue ? requireNonAnamorphic.Value : true,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
StreamOptions = streamOptions
};
var state = await StreamingHelpers.GetStreamingState( var state = await StreamingHelpers.GetStreamingState(
itemId, streamingRequest,
startTimeTicks,
audioCodec,
subtitleCodec,
videoCodec,
@params,
@static,
container,
liveStreamId,
playSessionId,
mediaSourceId,
deviceId,
deviceProfileId,
audioBitRate,
Request, Request,
_authContext, _authContext,
_mediaSourceManager, _mediaSourceManager,
@@ -230,7 +271,6 @@ namespace Jellyfin.Api.Controllers
_deviceManager, _deviceManager,
_transcodingJobHelper, _transcodingJobHelper,
_transcodingJobType, _transcodingJobType,
false,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -255,7 +295,7 @@ namespace Jellyfin.Api.Controllers
using (state) using (state)
{ {
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, cancellationTokenSource).ConfigureAwait(false); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this).ConfigureAwait(false);
} }
} }
@@ -297,8 +337,6 @@ namespace Jellyfin.Api.Controllers
return FileStreamResponseHelpers.GetStaticFileResult( return FileStreamResponseHelpers.GetStaticFileResult(
state.MediaPath, state.MediaPath,
contentType, contentType,
_fileSystem.GetLastWriteTimeUtc(state.MediaPath),
cacheDuration,
isHeadRequest, isHeadRequest,
this); this);
} }

View File

@@ -23,13 +23,11 @@ namespace Jellyfin.Api.Helpers
/// <param name="state">The current <see cref="StreamState"/>.</param> /// <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="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param> /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns> /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
public static async Task<ActionResult> GetStaticRemoteStreamResult( public static async Task<ActionResult> GetStaticRemoteStreamResult(
StreamState state, StreamState state,
bool isHeadRequest, bool isHeadRequest,
ControllerBase controller, ControllerBase controller)
CancellationTokenSource cancellationTokenSource)
{ {
HttpClient httpClient = new HttpClient(); HttpClient httpClient = new HttpClient();
@@ -59,16 +57,12 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
/// <param name="path">The path to the file.</param> /// <param name="path">The path to the file.</param>
/// <param name="contentType">The content type of the file.</param> /// <param name="contentType">The content type of the file.</param>
/// <param name="dateLastModified">The <see cref="DateTime"/> of the last modification of the file.</param>
/// <param name="cacheDuration">The cache duration of the file.</param>
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param> /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
/// <returns>An <see cref="ActionResult"/> the file.</returns> /// <returns>An <see cref="ActionResult"/> the file.</returns>
public static ActionResult GetStaticFileResult( public static ActionResult GetStaticFileResult(
string path, string path,
string contentType, string contentType,
DateTime dateLastModified,
TimeSpan? cacheDuration,
bool isHeadRequest, bool isHeadRequest,
ControllerBase controller) ControllerBase controller)
{ {
@@ -135,10 +129,11 @@ namespace Jellyfin.Api.Helpers
state.Dispose(); state.Dispose();
} }
Stream stream = new MemoryStream(); using (var memoryStream = new MemoryStream())
{
await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false); await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
return controller.File(stream, contentType); return controller.File(memoryStream, contentType);
}
} }
finally finally
{ {

View File

@@ -30,22 +30,29 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
public static class StreamingHelpers public static class StreamingHelpers
{ {
/// <summary>
/// Gets the current streaming state.
/// </summary>
/// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
/// <param name="httpRequest">The <see cref="HttpRequest"/>.</param>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="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( public static async Task<StreamState> GetStreamingState(
Guid itemId, StreamingRequestDto streamingRequest,
long? startTimeTicks, HttpRequest httpRequest,
string? audioCodec,
string? subtitleCodec,
string? videoCodec,
string? @params,
bool? @static,
string? container,
string? liveStreamId,
string? playSessionId,
string? mediaSourceId,
string? deviceId,
string? deviceProfileId,
int? audioBitRate,
HttpRequest request,
IAuthorizationContext authorizationContext, IAuthorizationContext authorizationContext,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IUserManager userManager, IUserManager userManager,
@@ -59,49 +66,43 @@ namespace Jellyfin.Api.Helpers
IDeviceManager deviceManager, IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper, TranscodingJobHelper transcodingJobHelper,
TranscodingJobType transcodingJobType, TranscodingJobType transcodingJobType,
bool isVideoRequest,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
// Parse the DLNA time seek header // Parse the DLNA time seek header
if (!startTimeTicks.HasValue) if (!streamingRequest.StartTimeTicks.HasValue)
{ {
var timeSeek = request.Headers["TimeSeekRange.dlna.org"]; var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
startTimeTicks = ParseTimeSeekHeader(timeSeek); streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
} }
if (!string.IsNullOrWhiteSpace(@params)) if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
{ {
// What is this? ParseParams(streamingRequest);
ParseParams(request);
} }
var streamOptions = ParseStreamOptions(request.Query); streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
var url = request.Path.Value.Split('.').Last(); var url = httpRequest.Path.Value.Split('.').Last();
if (string.IsNullOrEmpty(audioCodec)) if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
{ {
audioCodec = encodingHelper.InferAudioCodec(url); streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
} }
var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) || var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
{ {
// TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive Request = streamingRequest,
// Request = request,
DeviceId = deviceId,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
RequestedUrl = url, RequestedUrl = url,
UserAgent = request.Headers[HeaderNames.UserAgent], UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
EnableDlnaHeaders = enableDlnaHeaders EnableDlnaHeaders = enableDlnaHeaders
}; };
var auth = authorizationContext.GetAuthorizationInfo(request); var auth = authorizationContext.GetAuthorizationInfo(httpRequest);
if (!auth.UserId.Equals(Guid.Empty)) if (!auth.UserId.Equals(Guid.Empty))
{ {
state.User = userManager.GetUserById(auth.UserId); state.User = userManager.GetUserById(auth.UserId);
@@ -116,27 +117,27 @@ namespace Jellyfin.Api.Helpers
} }
*/ */
if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.VideoCodec)) if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
{ {
state.SupportedVideoCodecs = state.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
} }
if (!string.IsNullOrWhiteSpace(audioCodec)) if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
{ {
state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault(); ?? state.SupportedAudioCodecs.FirstOrDefault();
} }
if (!string.IsNullOrWhiteSpace(subtitleCodec)) if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
{ {
state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
?? state.SupportedSubtitleCodecs.FirstOrDefault(); ?? state.SupportedSubtitleCodecs.FirstOrDefault();
} }
var item = libraryManager.GetItemById(itemId); 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);
@@ -150,10 +151,10 @@ namespace Jellyfin.Api.Helpers
*/ */
MediaSourceInfo? mediaSource = null; MediaSourceInfo? mediaSource = null;
if (string.IsNullOrWhiteSpace(liveStreamId)) if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
{ {
var currentJob = !string.IsNullOrWhiteSpace(playSessionId) var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
? transcodingJobHelper.GetTranscodingJob(playSessionId) ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
: null; : null;
if (currentJob != null) if (currentJob != null)
@@ -163,13 +164,13 @@ namespace Jellyfin.Api.Helpers
if (mediaSource == null) if (mediaSource == null)
{ {
var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false); var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
mediaSource = string.IsNullOrEmpty(mediaSourceId) mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0] ? mediaSources[0]
: mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture)); : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture));
if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId) if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id)
{ {
mediaSource = mediaSources[0]; mediaSource = mediaSources[0];
} }
@@ -177,7 +178,7 @@ namespace Jellyfin.Api.Helpers
} }
else else
{ {
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false); var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
mediaSource = liveStreamInfo.Item1; mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2; state.DirectStreamProvider = liveStreamInfo.Item2;
} }
@@ -186,28 +187,28 @@ namespace Jellyfin.Api.Helpers
var containerInternal = Path.GetExtension(state.RequestedUrl); var containerInternal = Path.GetExtension(state.RequestedUrl);
if (string.IsNullOrEmpty(container)) if (string.IsNullOrEmpty(streamingRequest.Container))
{ {
containerInternal = container; containerInternal = streamingRequest.Container;
} }
if (string.IsNullOrEmpty(containerInternal)) if (string.IsNullOrEmpty(containerInternal))
{ {
containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state); containerInternal = (streamingRequest.Static && streamingRequest.Static) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
} }
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream); state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
state.OutputAudioCodec = audioCodec; state.OutputAudioCodec = streamingRequest.AudioCodec;
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (isVideoRequest) if (state.VideoRequest != null)
{ {
state.OutputVideoCodec = state.VideoCodec; state.OutputVideoCodec = state.Request.VideoCodec;
state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
encodingHelper.TryStreamCopy(state); encodingHelper.TryStreamCopy(state);
@@ -220,21 +221,21 @@ namespace Jellyfin.Api.Helpers
state.OutputVideoBitrate.Value, state.OutputVideoBitrate.Value,
state.VideoStream?.Codec, state.VideoStream?.Codec,
state.OutputVideoCodec, state.OutputVideoCodec,
videoRequest.MaxWidth, state.VideoRequest.MaxWidth,
videoRequest.MaxHeight); state.VideoRequest.MaxHeight);
videoRequest.MaxWidth = resolution.MaxWidth; state.VideoRequest.MaxWidth = resolution.MaxWidth;
videoRequest.MaxHeight = resolution.MaxHeight; state.VideoRequest.MaxHeight = resolution.MaxHeight;
} }
} }
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static); ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
var ext = string.IsNullOrWhiteSpace(state.OutputContainer) var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state) ? GetOutputFileExtension(state)
: ('.' + state.OutputContainer); : ('.' + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, playSessionId); state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state; return state;
} }
@@ -319,7 +320,7 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
/// <param name="value">The time seek header string.</param> /// <param name="value">The time seek header string.</param>
/// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns> /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
public static long? ParseTimeSeekHeader(string value) private static long? ParseTimeSeekHeader(string value)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
@@ -375,7 +376,7 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
/// <param name="queryString">The query string.</param> /// <param name="queryString">The query string.</param>
/// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns> /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
public static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString) private static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
{ {
Dictionary<string, string> streamOptions = new Dictionary<string, string>(); Dictionary<string, string> streamOptions = new Dictionary<string, string>();
foreach (var param in queryString) foreach (var param in queryString)
@@ -398,7 +399,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="state">The current <see cref="StreamState"/>.</param> /// <param name="state">The current <see cref="StreamState"/>.</param>
/// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param> /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
/// <param name="startTimeTicks">The start time in ticks.</param> /// <param name="startTimeTicks">The start time in ticks.</param>
public static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks) private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
{ {
var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
@@ -420,7 +421,7 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
/// <param name="state">The state.</param> /// <param name="state">The state.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
public static string? GetOutputFileExtension(StreamState state) private static string? GetOutputFileExtension(StreamState state)
{ {
var ext = Path.GetExtension(state.RequestedUrl); var ext = Path.GetExtension(state.RequestedUrl);
@@ -432,7 +433,7 @@ namespace Jellyfin.Api.Helpers
// Try to infer based on the desired video codec // Try to infer based on the desired video codec
if (state.IsVideoRequest) if (state.IsVideoRequest)
{ {
var videoCodec = state.VideoCodec; var videoCodec = state.Request.VideoCodec;
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)) string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
@@ -459,7 +460,7 @@ namespace Jellyfin.Api.Helpers
// Try to infer based on the desired audio codec // Try to infer based on the desired audio codec
if (!state.IsVideoRequest) if (!state.IsVideoRequest)
{ {
var audioCodec = state.AudioCodec; var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
{ {
@@ -570,7 +571,7 @@ namespace Jellyfin.Api.Helpers
// state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
if (!state.IsVideoRequest) if (state.VideoRequest != null)
{ {
state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
@@ -583,11 +584,16 @@ namespace Jellyfin.Api.Helpers
/// Parses the parameters. /// Parses the parameters.
/// </summary> /// </summary>
/// <param name="request">The request.</param> /// <param name="request">The request.</param>
private void ParseParams(StreamRequest request) private static void ParseParams(StreamingRequestDto request)
{ {
if (string.IsNullOrEmpty(request.Params))
{
return;
}
var vals = request.Params.Split(';'); var vals = request.Params.Split(';');
var videoRequest = request as VideoStreamRequest; var videoRequest = request as VideoRequestDto;
for (var i = 0; i < vals.Length; i++) for (var i = 0; i < vals.Length; i++)
{ {

View File

@@ -443,7 +443,7 @@ namespace Jellyfin.Api.Helpers
job.BitRate = bitRate; job.BitRate = bitRate;
} }
var deviceId = state.DeviceId; var deviceId = state.Request.DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId)) if (!string.IsNullOrWhiteSpace(deviceId))
{ {
@@ -486,7 +486,7 @@ namespace Jellyfin.Api.Helpers
HttpRequest request, HttpRequest request,
TranscodingJobType transcodingJobType, TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource, CancellationTokenSource cancellationTokenSource,
string workingDirectory = null) string? workingDirectory = null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
@@ -525,12 +525,12 @@ namespace Jellyfin.Api.Helpers
var transcodingJob = this.OnTranscodeBeginning( var transcodingJob = this.OnTranscodeBeginning(
outputPath, outputPath,
state.PlaySessionId, state.Request.PlaySessionId,
state.MediaSource.LiveStreamId, state.MediaSource.LiveStreamId,
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
transcodingJobType, transcodingJobType,
process, process,
state.DeviceId, state.Request.DeviceId,
state, state,
cancellationTokenSource); cancellationTokenSource);
@@ -706,9 +706,9 @@ namespace Jellyfin.Api.Helpers
_transcodingLocks.Remove(path); _transcodingLocks.Remove(path);
} }
if (!string.IsNullOrWhiteSpace(state.DeviceId)) if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{ {
_sessionManager.ClearTranscodingInfo(state.DeviceId); _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
} }
} }
@@ -747,7 +747,7 @@ namespace Jellyfin.Api.Helpers
state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
} }
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.LiveStreamId)) if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
{ {
var liveStreamResponse = await _mediaSourceManager.OpenLiveStream( var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },

View File

@@ -19,7 +19,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StreamState" /> class. /// Initializes a new instance of the <see cref="StreamState" /> class.
/// </summary> /// </summary>
/// <param name="mediaSourceManager">Instance of the <see cref="mediaSourceManager" /> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param>
/// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param> /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param>
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param> /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param>
public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper) public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper)
@@ -34,29 +34,28 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// </summary> /// </summary>
public string? RequestedUrl { get; set; } public string? RequestedUrl { get; set; }
// /// <summary> /// <summary>
// /// Gets or sets the request. /// Gets or sets the request.
// /// </summary> /// </summary>
// public StreamRequest Request public StreamingRequestDto Request
// { {
// get => (StreamRequest)BaseRequest; get => (StreamingRequestDto)BaseRequest;
// set set
// { {
// BaseRequest = value; BaseRequest = value;
// IsVideoRequest = VideoRequest != null;
// IsVideoRequest = VideoRequest != null; }
// } }
// }
/// <summary> /// <summary>
/// Gets or sets the transcoding throttler. /// Gets or sets the transcoding throttler.
/// </summary> /// </summary>
public TranscodingThrottler? TranscodingThrottler { get; set; } public TranscodingThrottler? TranscodingThrottler { get; set; }
/*/// <summary> /// <summary>
/// Gets the video request. /// Gets the video request.
/// </summary> /// </summary>
public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;*/ public VideoRequestDto? VideoRequest => Request! as VideoRequestDto;
/// <summary> /// <summary>
/// Gets or sets the direct stream provicer. /// Gets or sets the direct stream provicer.
@@ -68,10 +67,10 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// </summary> /// </summary>
public string? WaitForPath { get; set; } public string? WaitForPath { get; set; }
/*/// <summary> /// <summary>
/// Gets a value indicating whether the request outputs video. /// Gets a value indicating whether the request outputs video.
/// </summary> /// </summary>
public bool IsOutputVideo => Request is VideoStreamRequest;*/ public bool IsOutputVideo => Request is VideoRequestDto;
/// <summary> /// <summary>
/// Gets the segment length. /// Gets the segment length.
@@ -161,36 +160,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// </summary> /// </summary>
public TranscodingJobDto? TranscodingJob { get; set; } public TranscodingJobDto? TranscodingJob { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets the play session id.
/// </summary>
public string? PlaySessionId { get; set; }
/// <summary>
/// Gets or sets the live stream id.
/// </summary>
public string? LiveStreamId { get; set; }
/// <summary>
/// Gets or sets the video coded.
/// </summary>
public string? VideoCodec { get; set; }
/// <summary>
/// Gets or sets the audio codec.
/// </summary>
public string? AudioCodec { get; set; }
/// <summary>
/// Gets or sets the subtitle codec.
/// </summary>
public string? SubtitleCodec { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
@@ -219,7 +188,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
{ {
// REVIEW: Is this the right place for this? // REVIEW: Is this the right place for this?
if (MediaSource.RequiresClosing if (MediaSource.RequiresClosing
&& string.IsNullOrWhiteSpace(LiveStreamId) && string.IsNullOrWhiteSpace(Request.LiveStreamId)
&& !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
{ {
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();

View File

@@ -0,0 +1,45 @@
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 device profile.
/// </summary>
public string? DeviceProfileId { get; set; }
/// <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; }
}
}

View File

@@ -0,0 +1,19 @@
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; }
}
}