mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-16 02:18:54 -07:00
Merge pull request #3592 from Ullmie02/api-audio
Migrate AudioService to Jellyfin.Api
This commit is contained in:
commit
44acb9f7d9
@ -4,7 +4,6 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@ -46,6 +45,7 @@ using Emby.Server.Implementations.Session;
|
||||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Api;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@ -633,6 +633,8 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
350
Jellyfin.Api/Controllers/AudioController.cs
Normal file
350
Jellyfin.Api/Controllers/AudioController.cs
Normal file
@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The audio controller.
|
||||
/// </summary>
|
||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
||||
public class AudioController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> 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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
|
||||
/// <param name="httpClient">Instance of the <see cref="HttpClient"/>.</param>
|
||||
public AudioController(
|
||||
IDlnaManager dlnaManager,
|
||||
IUserManager userManger,
|
||||
IAuthorizationContext authorizationContext,
|
||||
ILibraryManager libraryManager,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IStreamHelper streamHelper,
|
||||
IFileSystem fileSystem,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IConfiguration configuration,
|
||||
IDeviceManager deviceManager,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
HttpClient httpClient)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_authContext = authorizationContext;
|
||||
_userManager = userManger;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_streamHelper = streamHelper;
|
||||
_fileSystem = fileSystem;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_configuration = configuration;
|
||||
_deviceManager = deviceManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an audio stream.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="container">The audio container.</param>
|
||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||
/// <param name="params">The streaming parameters.</param>
|
||||
/// <param name="tag">The tag.</param>
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
/// <param name="maxRefFrames">Optional.</param>
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container?}")]
|
||||
[HttpGet("{itemId}/stream")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container?}")]
|
||||
[HttpHead("{itemId}/stream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
||||
[FromQuery] int? maxRefFrames,
|
||||
[FromQuery] int? maxVideoBitDepth,
|
||||
[FromQuery] bool? requireAvc,
|
||||
[FromQuery] bool? deInterlace,
|
||||
[FromQuery] bool? requireNonAnamorphic,
|
||||
[FromQuery] int? transcodingMaxAudioChannels,
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery] string? transcodingReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
_mediaSourceManager,
|
||||
_userManager,
|
||||
_libraryManager,
|
||||
_serverConfigurationManager,
|
||||
_mediaEncoder,
|
||||
_fileSystem,
|
||||
_subtitleEncoder,
|
||||
_configuration,
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
// TODO AllowEndOfFile = false
|
||||
await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
||||
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
|
||||
}
|
||||
|
||||
// Static remote stream
|
||||
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
|
||||
{
|
||||
return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
|
||||
}
|
||||
|
||||
var outputPath = state.OutputFilePath;
|
||||
var outputPathExists = System.IO.File.Exists(outputPath);
|
||||
|
||||
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
|
||||
var isTranscodeCached = outputPathExists && transcodingJob != null;
|
||||
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
// Static stream
|
||||
if (@static.HasValue && @static.Value)
|
||||
{
|
||||
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
|
||||
|
||||
if (state.MediaSource.IsInfiniteStream)
|
||||
{
|
||||
// TODO AllowEndOfFile = false
|
||||
await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return File(Response.Body, contentType);
|
||||
}
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(
|
||||
state.MediaPath,
|
||||
contentType,
|
||||
isHeadRequest,
|
||||
this);
|
||||
}
|
||||
|
||||
// Need to start ffmpeg (because media can't be returned directly)
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||
var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
|
||||
return await FileStreamResponseHelpers.GetTranscodedFile(
|
||||
state,
|
||||
isHeadRequest,
|
||||
_streamHelper,
|
||||
this,
|
||||
_transcodingJobHelper,
|
||||
ffmpegCommandLineArguments,
|
||||
Request,
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -40,8 +39,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> 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="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
|
||||
public PlaystateController(
|
||||
IUserManager userManager,
|
||||
IUserDataManager userDataRepository,
|
||||
@ -49,8 +47,7 @@ namespace Jellyfin.Api.Controllers
|
||||
ISessionManager sessionManager,
|
||||
IAuthorizationContext authContext,
|
||||
ILoggerFactory loggerFactory,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IFileSystem fileSystem)
|
||||
TranscodingJobHelper transcodingJobHelper)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userDataRepository = userDataRepository;
|
||||
@ -59,10 +56,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_authContext = authContext;
|
||||
_logger = loggerFactory.CreateLogger<PlaystateController>();
|
||||
|
||||
_transcodingJobHelper = new TranscodingJobHelper(
|
||||
loggerFactory.CreateLogger<TranscodingJobHelper>(),
|
||||
mediaSourceManager,
|
||||
fileSystem);
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
139
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
Normal file
139
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
Normal file
@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// The stream response helpers.
|
||||
/// </summary>
|
||||
public static class FileStreamResponseHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a static file from a remote source.
|
||||
/// </summary>
|
||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
||||
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
|
||||
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
|
||||
public static async Task<ActionResult> GetStaticRemoteStreamResult(
|
||||
StreamState state,
|
||||
bool isHeadRequest,
|
||||
ControllerBase controller,
|
||||
HttpClient httpClient)
|
||||
{
|
||||
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
|
||||
}
|
||||
|
||||
using var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
|
||||
var contentType = response.Content.Headers.ContentType.ToString();
|
||||
|
||||
controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return controller.File(Array.Empty<byte>(), contentType);
|
||||
}
|
||||
|
||||
return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a static file from the server.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the file.</param>
|
||||
/// <param name="contentType">The content type of the file.</param>
|
||||
/// <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>
|
||||
/// <returns>An <see cref="ActionResult"/> the file.</returns>
|
||||
public static ActionResult GetStaticFileResult(
|
||||
string path,
|
||||
string contentType,
|
||||
bool isHeadRequest,
|
||||
ControllerBase controller)
|
||||
{
|
||||
controller.Response.ContentType = contentType;
|
||||
|
||||
// if the request is a head request, return a NoContent result with the same headers as it would with a GET request
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return controller.NoContent();
|
||||
}
|
||||
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
return controller.File(stream, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a transcoded file from the server.
|
||||
/// </summary>
|
||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
|
||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
||||
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
|
||||
/// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
|
||||
/// <param name="request">The <see cref="HttpRequest"/> starting the transcoding.</param>
|
||||
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
|
||||
/// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
|
||||
public static async Task<ActionResult> GetTranscodedFile(
|
||||
StreamState state,
|
||||
bool isHeadRequest,
|
||||
IStreamHelper streamHelper,
|
||||
ControllerBase controller,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
string ffmpegCommandLineArguments,
|
||||
HttpRequest request,
|
||||
TranscodingJobType transcodingJobType,
|
||||
CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
// Use the command line args with a dummy playlist path
|
||||
var outputPath = state.OutputFilePath;
|
||||
|
||||
controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
|
||||
var contentType = state.GetMimeType(outputPath);
|
||||
|
||||
// Headers only
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return controller.File(Array.Empty<byte>(), contentType);
|
||||
}
|
||||
|
||||
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
|
||||
state.Dispose();
|
||||
}
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
|
||||
return controller.File(memoryStream, contentType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
transcodingLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
758
Jellyfin.Api/Helpers/StreamingHelpers.cs
Normal file
758
Jellyfin.Api/Helpers/StreamingHelpers.cs
Normal file
@ -0,0 +1,758 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// The streaming helpers.
|
||||
/// </summary>
|
||||
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(
|
||||
StreamingRequestDto streamingRequest,
|
||||
HttpRequest httpRequest,
|
||||
IAuthorizationContext authorizationContext,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IUserManager userManager,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IFileSystem fileSystem,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IConfiguration configuration,
|
||||
IDlnaManager dlnaManager,
|
||||
IDeviceManager deviceManager,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
TranscodingJobType transcodingJobType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
|
||||
// Parse the DLNA time seek header
|
||||
if (!streamingRequest.StartTimeTicks.HasValue)
|
||||
{
|
||||
var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
|
||||
|
||||
streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
|
||||
{
|
||||
ParseParams(streamingRequest);
|
||||
}
|
||||
|
||||
streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
|
||||
|
||||
var url = httpRequest.Path.Value.Split('.').Last();
|
||||
|
||||
if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
|
||||
{
|
||||
streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
|
||||
}
|
||||
|
||||
var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
|
||||
string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
|
||||
{
|
||||
Request = streamingRequest,
|
||||
RequestedUrl = url,
|
||||
UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
|
||||
EnableDlnaHeaders = enableDlnaHeaders
|
||||
};
|
||||
|
||||
var auth = authorizationContext.GetAuthorizationInfo(httpRequest);
|
||||
if (!auth.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
state.User = userManager.GetUserById(auth.UserId);
|
||||
}
|
||||
|
||||
if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
|
||||
{
|
||||
state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
|
||||
{
|
||||
state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
|
||||
?? state.SupportedAudioCodecs.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
|
||||
{
|
||||
state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
|
||||
?? state.SupportedSubtitleCodecs.FirstOrDefault();
|
||||
}
|
||||
|
||||
var item = libraryManager.GetItemById(streamingRequest.Id);
|
||||
|
||||
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
MediaSourceInfo? mediaSource = null;
|
||||
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
|
||||
{
|
||||
var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
|
||||
? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
|
||||
: null;
|
||||
|
||||
if (currentJob != null)
|
||||
{
|
||||
mediaSource = currentJob.MediaSource;
|
||||
}
|
||||
|
||||
if (mediaSource == null)
|
||||
{
|
||||
var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
|
||||
? mediaSources[0]
|
||||
: mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture));
|
||||
|
||||
if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id)
|
||||
{
|
||||
mediaSource = mediaSources[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
||||
mediaSource = liveStreamInfo.Item1;
|
||||
state.DirectStreamProvider = liveStreamInfo.Item2;
|
||||
}
|
||||
|
||||
encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
|
||||
|
||||
string? containerInternal = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(streamingRequest.Container))
|
||||
{
|
||||
containerInternal = streamingRequest.Container;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(containerInternal))
|
||||
{
|
||||
containerInternal = streamingRequest.Static ?
|
||||
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio)
|
||||
: GetOutputFileExtension(state);
|
||||
}
|
||||
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
|
||||
|
||||
state.OutputAudioCodec = streamingRequest.AudioCodec;
|
||||
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
{
|
||||
state.OutputVideoCodec = state.Request.VideoCodec;
|
||||
state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||
|
||||
encodingHelper.TryStreamCopy(state);
|
||||
|
||||
if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
var resolution = ResolutionNormalizer.Normalize(
|
||||
state.VideoStream?.BitRate,
|
||||
state.VideoStream?.Width,
|
||||
state.VideoStream?.Height,
|
||||
state.OutputVideoBitrate.Value,
|
||||
state.VideoStream?.Codec,
|
||||
state.OutputVideoCodec,
|
||||
state.VideoRequest.MaxWidth,
|
||||
state.VideoRequest.MaxHeight);
|
||||
|
||||
state.VideoRequest.MaxWidth = resolution.MaxWidth;
|
||||
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
||||
|
||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||
? GetOutputFileExtension(state)
|
||||
: ('.' + state.OutputContainer);
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the dlna headers.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
|
||||
/// <param name="startTimeTicks">The start time in ticks.</param>
|
||||
/// <param name="request">The <see cref="HttpRequest"/>.</param>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
public static void AddDlnaHeaders(
|
||||
StreamState state,
|
||||
IHeaderDictionary responseHeaders,
|
||||
bool isStaticallyStreamed,
|
||||
long? startTimeTicks,
|
||||
HttpRequest request,
|
||||
IDlnaManager dlnaManager)
|
||||
{
|
||||
if (!state.EnableDlnaHeaders)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = state.DeviceProfile;
|
||||
|
||||
StringValues transferMode = request.Headers["transferMode.dlna.org"];
|
||||
responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
|
||||
responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
|
||||
|
||||
if (state.RunTimeTicks.HasValue)
|
||||
{
|
||||
if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
|
||||
responseHeaders.Add("MediaInfo.sec", string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"SEC_Duration={0};",
|
||||
Convert.ToInt32(ms)));
|
||||
}
|
||||
|
||||
if (!isStaticallyStreamed && profile != null)
|
||||
{
|
||||
AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
|
||||
}
|
||||
}
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
profile = dlnaManager.GetDefaultProfile();
|
||||
}
|
||||
|
||||
var audioCodec = state.ActualOutputAudioCodec;
|
||||
|
||||
if (!state.IsVideoRequest)
|
||||
{
|
||||
responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader(
|
||||
state.OutputContainer,
|
||||
audioCodec,
|
||||
state.OutputAudioBitrate,
|
||||
state.OutputAudioSampleRate,
|
||||
state.OutputAudioChannels,
|
||||
state.OutputAudioBitDepth,
|
||||
isStaticallyStreamed,
|
||||
state.RunTimeTicks,
|
||||
state.TranscodeSeekInfo));
|
||||
}
|
||||
else
|
||||
{
|
||||
var videoCodec = state.ActualOutputVideoCodec;
|
||||
|
||||
responseHeaders.Add(
|
||||
"contentFeatures.dlna.org",
|
||||
new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the time seek header.
|
||||
/// </summary>
|
||||
/// <param name="value">The time seek header string.</param>
|
||||
/// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
|
||||
private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (value.IsEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
const string npt = "npt=";
|
||||
if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Invalid timeseek header");
|
||||
}
|
||||
|
||||
var index = value.IndexOf('-');
|
||||
value = index == -1
|
||||
? value.Slice(npt.Length)
|
||||
: value.Slice(npt.Length, index - npt.Length);
|
||||
if (value.IndexOf(':') == -1)
|
||||
{
|
||||
// Parses npt times in the format of '417.33'
|
||||
if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
|
||||
{
|
||||
return TimeSpan.FromSeconds(seconds).Ticks;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Invalid timeseek header");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parses npt times in the format of '10:19:25.7'
|
||||
return TimeSpan.Parse(value).Ticks;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ArgumentException("Invalid timeseek header");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses query parameters as StreamOptions.
|
||||
/// </summary>
|
||||
/// <param name="queryString">The query string.</param>
|
||||
/// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
|
||||
private static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
|
||||
{
|
||||
Dictionary<string, string> streamOptions = new Dictionary<string, string>();
|
||||
foreach (var param in queryString)
|
||||
{
|
||||
if (char.IsLower(param.Key[0]))
|
||||
{
|
||||
// This was probably not parsed initially and should be a StreamOptions
|
||||
// or the generated URL should correctly serialize it
|
||||
// TODO: This should be incorporated either in the lower framework for parsing requests
|
||||
streamOptions[param.Key] = param.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return streamOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the dlna time seek headers to the response.
|
||||
/// </summary>
|
||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||
/// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
|
||||
/// <param name="startTimeTicks">The start time in ticks.</param>
|
||||
private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
|
||||
{
|
||||
var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"npt={0}-{1}/{1}",
|
||||
startSeconds,
|
||||
runtimeSeconds));
|
||||
responseHeaders.Add("X-AvailableSeekRange", string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"1 npt={0}-{1}",
|
||||
startSeconds,
|
||||
runtimeSeconds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string? GetOutputFileExtension(StreamState state)
|
||||
{
|
||||
var ext = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
{
|
||||
return ext;
|
||||
}
|
||||
|
||||
// Try to infer based on the desired video codec
|
||||
if (state.IsVideoRequest)
|
||||
{
|
||||
var videoCodec = state.Request.VideoCodec;
|
||||
|
||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ogv";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".webm";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".asf";
|
||||
}
|
||||
}
|
||||
|
||||
// Try to infer based on the desired audio codec
|
||||
if (!state.IsVideoRequest)
|
||||
{
|
||||
var audioCodec = state.Request.AudioCodec;
|
||||
|
||||
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".aac";
|
||||
}
|
||||
|
||||
if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".mp3";
|
||||
}
|
||||
|
||||
if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ogg";
|
||||
}
|
||||
|
||||
if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".wma";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output file path for transcoding.
|
||||
/// </summary>
|
||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||
/// <param name="outputFileExtension">The file extension of the output file.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <returns>The complete file path, including the folder, for the transcoding file.</returns>
|
||||
private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
|
||||
{
|
||||
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
||||
|
||||
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
var ext = outputFileExtension?.ToLowerInvariant();
|
||||
var folder = serverConfigurationManager.GetTranscodePath();
|
||||
|
||||
return Path.Combine(folder, filename + ext);
|
||||
}
|
||||
|
||||
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
|
||||
{
|
||||
var headers = request.Headers;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
||||
{
|
||||
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
||||
{
|
||||
var caps = deviceManager.GetCapabilities(deviceProfileId);
|
||||
|
||||
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
|
||||
}
|
||||
|
||||
var profile = state.DeviceProfile;
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
// Don't use settings from the default profile.
|
||||
// Only use a specific profile if it was requested.
|
||||
return;
|
||||
}
|
||||
|
||||
var audioCodec = state.ActualOutputAudioCodec;
|
||||
var videoCodec = state.ActualOutputVideoCodec;
|
||||
|
||||
var mediaProfile = !state.IsVideoRequest
|
||||
? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
|
||||
: profile.GetVideoMediaProfile(
|
||||
state.OutputContainer,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
state.OutputWidth,
|
||||
state.OutputHeight,
|
||||
state.TargetVideoBitDepth,
|
||||
state.OutputVideoBitrate,
|
||||
state.TargetVideoProfile,
|
||||
state.TargetVideoLevel,
|
||||
state.TargetFramerate,
|
||||
state.TargetPacketLength,
|
||||
state.TargetTimestamp,
|
||||
state.IsTargetAnamorphic,
|
||||
state.IsTargetInterlaced,
|
||||
state.TargetRefFrames,
|
||||
state.TargetVideoStreamCount,
|
||||
state.TargetAudioStreamCount,
|
||||
state.TargetVideoCodecTag,
|
||||
state.IsTargetAVC);
|
||||
|
||||
if (mediaProfile != null)
|
||||
{
|
||||
state.MimeType = mediaProfile.MimeType;
|
||||
}
|
||||
|
||||
if (!(@static.HasValue && @static.Value))
|
||||
{
|
||||
var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
|
||||
|
||||
if (transcodingProfile != null)
|
||||
{
|
||||
state.EstimateContentLength = transcodingProfile.EstimateContentLength;
|
||||
// state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
|
||||
state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
{
|
||||
state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
|
||||
state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the parameters.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
private static void ParseParams(StreamingRequestDto request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Params))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var vals = request.Params.Split(';');
|
||||
|
||||
var videoRequest = request as VideoRequestDto;
|
||||
|
||||
for (var i = 0; i < vals.Length; i++)
|
||||
{
|
||||
var val = vals[i];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (i)
|
||||
{
|
||||
case 0:
|
||||
request.DeviceProfileId = val;
|
||||
break;
|
||||
case 1:
|
||||
request.DeviceId = val;
|
||||
break;
|
||||
case 2:
|
||||
request.MediaSourceId = val;
|
||||
break;
|
||||
case 3:
|
||||
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
case 4:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.VideoCodec = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 5:
|
||||
request.AudioCodec = val;
|
||||
break;
|
||||
case 6:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 7:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 8:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 9:
|
||||
request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 10:
|
||||
request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 11:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 12:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 13:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 14:
|
||||
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 15:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.Level = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 16:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 17:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 18:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.Profile = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 19:
|
||||
// cabac no longer used
|
||||
break;
|
||||
case 20:
|
||||
request.PlaySessionId = val;
|
||||
break;
|
||||
case 21:
|
||||
// api_key
|
||||
break;
|
||||
case 22:
|
||||
request.LiveStreamId = val;
|
||||
break;
|
||||
case 23:
|
||||
// Duplicating ItemId because of MediaMonkey
|
||||
break;
|
||||
case 24:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 25:
|
||||
if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
|
||||
{
|
||||
if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
|
||||
{
|
||||
videoRequest.SubtitleMethod = method;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 26:
|
||||
request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 27:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 28:
|
||||
request.Tag = val;
|
||||
break;
|
||||
case 29:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 30:
|
||||
request.SubtitleCodec = val;
|
||||
break;
|
||||
case 31:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 32:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 33:
|
||||
request.TranscodeReasons = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,28 @@
|
||||
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.Models.PlaybackDtos;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Helpers
|
||||
@ -27,9 +42,17 @@ namespace Jellyfin.Api.Helpers
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
|
||||
|
||||
private readonly ILogger<TranscodingJobHelper> _logger;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IAuthorizationContext _authorizationContext;
|
||||
private readonly EncodingHelper _encodingHelper;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IIsoManager _isoManager;
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
|
||||
@ -37,14 +60,40 @@ namespace Jellyfin.Api.Helpers
|
||||
/// <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="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="isoManager">Instance of the <see cref="IIsoManager"/> 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="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public TranscodingJobHelper(
|
||||
ILogger<TranscodingJobHelper> logger,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IFileSystem fileSystem)
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ISessionManager sessionManager,
|
||||
IAuthorizationContext authorizationContext,
|
||||
IIsoManager isoManager,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IConfiguration configuration,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_sessionManager = sessionManager;
|
||||
_authorizationContext = authorizationContext;
|
||||
_isoManager = isoManager;
|
||||
_loggerFactory = loggerFactory;
|
||||
|
||||
_encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
|
||||
|
||||
DeleteEncodedMediaCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -60,6 +109,20 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the transcoding file.</param>
|
||||
/// <param name="type">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <returns>The transcoding job.</returns>
|
||||
public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ping transcoding job.
|
||||
/// </summary>
|
||||
@ -349,5 +412,429 @@ namespace Jellyfin.Api.Helpers
|
||||
throw new AggregateException("Error deleting HLS files", exs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Report the transcoding progress to the session manager.
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
|
||||
/// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
|
||||
/// <param name="transcodingPosition">The current transcoding position.</param>
|
||||
/// <param name="framerate">The framerate of the transcoding job.</param>
|
||||
/// <param name="percentComplete">The completion percentage of the transcode.</param>
|
||||
/// <param name="bytesTranscoded">The number of bytes transcoded.</param>
|
||||
/// <param name="bitRate">The bitrate of the transcoding job.</param>
|
||||
public void ReportTranscodingProgress(
|
||||
TranscodingJobDto job,
|
||||
StreamState state,
|
||||
TimeSpan? transcodingPosition,
|
||||
float? framerate,
|
||||
double? percentComplete,
|
||||
long? bytesTranscoded,
|
||||
int? bitRate)
|
||||
{
|
||||
var ticks = transcodingPosition?.Ticks;
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
job.Framerate = framerate;
|
||||
job.CompletionPercentage = percentComplete;
|
||||
job.TranscodingPositionTicks = ticks;
|
||||
job.BytesTranscoded = bytesTranscoded;
|
||||
job.BitRate = bitRate;
|
||||
}
|
||||
|
||||
var deviceId = state.Request.DeviceId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
var audioCodec = state.ActualOutputAudioCodec;
|
||||
var videoCodec = state.ActualOutputVideoCodec;
|
||||
|
||||
_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),
|
||||
TranscodeReasons = state.TranscodeReasons
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the FFMPEG.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="commandLineArguments">The command line arguments for ffmpeg.</param>
|
||||
/// <param name="request">The <see cref="HttpRequest"/>.</param>
|
||||
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
/// <param name="workingDirectory">The working directory.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task<TranscodingJobDto> StartFfMpeg(
|
||||
StreamState state,
|
||||
string outputPath,
|
||||
string commandLineArguments,
|
||||
HttpRequest request,
|
||||
TranscodingJobType transcodingJobType,
|
||||
CancellationTokenSource cancellationTokenSource,
|
||||
string? workingDirectory = null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
|
||||
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
|
||||
|
||||
if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
|
||||
{
|
||||
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw new ArgumentException("User does not have access to video transcoding");
|
||||
}
|
||||
}
|
||||
|
||||
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) ? null : 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);
|
||||
|
||||
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
|
||||
_logger.LogInformation(commandLineLogMessage);
|
||||
|
||||
var logFilePrefix = "ffmpeg-transcode";
|
||||
if (state.VideoRequest != null
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
|
||||
? "ffmpeg-remux"
|
||||
: "ffmpeg-directstream";
|
||||
}
|
||||
|
||||
var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
|
||||
|
||||
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
||||
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, 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.BaseStream, logStream);
|
||||
|
||||
// Wait for the file to exist before proceeeding
|
||||
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);
|
||||
}
|
||||
|
||||
_logger.LogDebug("StartFfMpeg() finished successfully");
|
||||
|
||||
return transcodingJob;
|
||||
}
|
||||
|
||||
private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
|
||||
{
|
||||
if (EnableThrottling(state))
|
||||
{
|
||||
transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
|
||||
state.TranscodingThrottler.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnableThrottling(StreamState state)
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// enable throttling when NOT using hardware acceleration
|
||||
if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
||||
{
|
||||
return state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||
state.IsInputVideo &&
|
||||
state.VideoType == VideoType.VideoFile &&
|
||||
!EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [transcode beginning].
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="playSessionId">The play session identifier.</param>
|
||||
/// <param name="liveStreamId">The live stream identifier.</param>
|
||||
/// <param name="transcodingJobId">The transcoding job identifier.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="process">The process.</param>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
/// <returns>TranscodingJob.</returns>
|
||||
public TranscodingJobDto OnTranscodeBeginning(
|
||||
string path,
|
||||
string? playSessionId,
|
||||
string? liveStreamId,
|
||||
string transcodingJobId,
|
||||
TranscodingJobType type,
|
||||
Process process,
|
||||
string? deviceId,
|
||||
StreamState state,
|
||||
CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
|
||||
{
|
||||
Type = type,
|
||||
Path = path,
|
||||
Process = process,
|
||||
ActiveRequestCount = 1,
|
||||
DeviceId = deviceId,
|
||||
CancellationTokenSource = cancellationTokenSource,
|
||||
Id = transcodingJobId,
|
||||
PlaySessionId = playSessionId,
|
||||
LiveStreamId = liveStreamId,
|
||||
MediaSource = state.MediaSource
|
||||
};
|
||||
|
||||
_activeTranscodingJobs.Add(job);
|
||||
|
||||
ReportTranscodingProgress(job, state, null, null, null, null, null);
|
||||
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 != 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, TranscodingJobDto job, StreamState state)
|
||||
{
|
||||
if (job != null)
|
||||
{
|
||||
job.HasExited = true;
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
process.Dispose();
|
||||
}
|
||||
|
||||
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath))
|
||||
{
|
||||
state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
|
||||
{
|
||||
var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
|
||||
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
|
||||
|
||||
if (state.VideoRequest != 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="TranscodingJobDto"/>.</returns>
|
||||
public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (job == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
OnTranscodeBeginRequest(job);
|
||||
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTranscodeBeginRequest(TranscodingJobDto 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
201
Jellyfin.Api/Models/StreamingDtos/StreamState.cs
Normal file
201
Jellyfin.Api/Models/StreamingDtos/StreamState.cs
Normal file
@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
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 != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transcoding throttler.
|
||||
/// </summary>
|
||||
public TranscodingThrottler? TranscodingThrottler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video request.
|
||||
/// </summary>
|
||||
public VideoRequestDto? VideoRequest => Request! as VideoRequestDto;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the direct stream provicer.
|
||||
/// </summary>
|
||||
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.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
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 a value indicating whether to enable dlna headers.
|
||||
/// </summary>
|
||||
public bool EnableDlnaHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the device profile.
|
||||
/// </summary>
|
||||
public DeviceProfile? DeviceProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transcoding job.
|
||||
/// </summary>
|
||||
public TranscodingJobDto? 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 beeing 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();
|
||||
}
|
||||
|
||||
TranscodingThrottler?.Dispose();
|
||||
}
|
||||
|
||||
TranscodingThrottler = null;
|
||||
TranscodingJob = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
45
Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
Normal file
45
Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
19
Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
Normal file
19
Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -17,10 +17,6 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
/// <summary>
|
||||
/// Class GetAudioStream.
|
||||
/// </summary>
|
||||
[Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")]
|
||||
[Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")]
|
||||
[Route("/Audio/{Id}/stream.{Container}", "HEAD", Summary = "Gets an audio stream")]
|
||||
[Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
|
||||
public class GetAudioStream : StreamRequest
|
||||
{
|
||||
}
|
||||
|
@ -1323,6 +1323,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return null;
|
||||
}
|
||||
|
||||
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
|
||||
{
|
||||
if (audioBitRate.HasValue)
|
||||
{
|
||||
// Don't encode any higher than this
|
||||
return Math.Min(384000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
|
||||
{
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
Loading…
Reference in New Issue
Block a user