Continute work

pull/3592/head
David 4 years ago
parent 2328ec59c9
commit 3514813eb4

@ -46,6 +46,7 @@ using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.SyncPlay;
using Jellyfin.Api.Helpers;
using MediaBrowser.Api;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@ -637,6 +638,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
serviceCollection.AddSingleton<TranscodingJobHelper>();
}
/// <summary>

@ -3,97 +3,277 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
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.Logging;
using Microsoft.Net.Http.Headers;
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 ILogger _logger;
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 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="logger">Instance of the <see cref="ILogger{AuidoController}"/> interface.</param>
public AudioController(IDlnaManager dlnaManager, ILogger<AudioController> logger)
/// <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>
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)
{
_dlnaManager = dlnaManager;
_logger = logger;
_authContext = authorizationContext;
_userManager = userManger;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_streamHelper = streamHelper;
_fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
}
[HttpGet("{id}/stream.{container}")]
[HttpGet("{id}/stream")]
[HttpHead("{id}/stream.{container}")]
[HttpGet("{id}/stream")]
/// <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.{container}")]
[HttpGet("{itemId}/stream")]
[HttpHead("{itemId}/stream.{container}")]
[HttpGet("{itemId}/stream")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetAudioStream(
[FromRoute] string id,
[FromRoute] string container,
[FromQuery] bool Static,
[FromQuery] string tag)
[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();
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
var state = await StreamingHelpers.GetStreamingState(
itemId,
startTimeTicks,
audioCodec,
subtitleCodec,
videoCodec,
@params,
@static,
container,
liveStreamId,
playSessionId,
mediaSourceId,
deviceId,
deviceProfileId,
audioBitRate,
Request,
_authContext,
_mediaSourceManager,
_userManager,
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
_fileSystem,
_subtitleEncoder,
_configuration,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
_transcodingJobType,
false,
cancellationTokenSource.Token)
.ConfigureAwait(false);
if (Static && state.DirectStreamProvider != null)
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
using (state)
{
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// TODO: Don't hardcode this
outputHeaders[HeaderNames.ContentType] = MimeTypes.GetMimeType("file.ts");
// TODO AllowEndOfFile = false
await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, _logger, CancellationToken.None)
{
AllowEndOfFile = false
};
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
}
}
// Static remote stream
if (Static && state.InputProtocol == MediaProtocol.Http)
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
using (state)
{
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, cancellationTokenSource).ConfigureAwait(false);
}
}
if (Static && state.InputProtocol != MediaProtocol.File)
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
{
throw new ArgumentException(string.Format($"Input protocol {state.InputProtocol} cannot be streamed statically."));
return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
}
var outputPath = state.OutputFilePath;
var outputPathExists = File.Exists(outputPath);
var outputPathExists = System.IO.File.Exists(outputPath);
var transcodingJob = TranscodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, Static || isTranscodeCached, Request, _dlnaManager);
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
// Static stream
if (Static)
if (@static.HasValue && @static.Value)
{
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
@ -101,16 +281,10 @@ namespace Jellyfin.Api.Controllers
{
if (state.MediaSource.IsInfiniteStream)
{
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[HeaderNames.ContentType] = contentType
};
// TODO AllowEndOfFile = false
await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, _logger, CancellationToken.None)
{
AllowEndOfFile = false
};
return File(Response.Body, contentType);
}
TimeSpan? cacheDuration = null;
@ -120,57 +294,65 @@ namespace Jellyfin.Api.Controllers
cacheDuration = TimeSpan.FromDays(365);
}
return FileStreamResponseHelpers.GetStaticFileResult(
state.MediaPath,
contentType,
_fileSystem.GetLastWriteTimeUtc(state.MediaPath),
cacheDuration,
isHeadRequest,
this);
}
}
/*
// Not static but transcode cache file exists
if (isTranscodeCached && state.VideoRequest == null)
{
var contentType = state.GetMimeType(outputPath)
try
{
if (transcodingJob != null)
{
ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
}
return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
ResponseHeaders = responseHeaders,
ContentType = contentType,
IsHeadRequest = isHeadRequest,
Path = state.MediaPath,
CacheDuration = cacheDuration
Path = outputPath,
FileShare = FileShare.ReadWrite,
OnComplete = () =>
{
if (transcodingJob != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
}
}).ConfigureAwait(false);
}
finally
{
state.Dispose();
}
}
*/
//// Not static but transcode cache file exists
//if (isTranscodeCached && state.VideoRequest == null)
//{
// var contentType = state.GetMimeType(outputPath);
// try
// {
// if (transcodingJob != null)
// {
// ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
// }
// return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
// {
// ResponseHeaders = responseHeaders,
// ContentType = contentType,
// IsHeadRequest = isHeadRequest,
// Path = outputPath,
// FileShare = FileShare.ReadWrite,
// OnComplete = () =>
// {
// if (transcodingJob != null)
// {
// ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
// }
// }
// }).ConfigureAwait(false);
// }
// finally
// {
// state.Dispose();
// }
//}
// Need to start ffmpeg
// Need to start ffmpeg (because media can't be returned directly)
try
{
return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
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);
}
catch
{

@ -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>

@ -0,0 +1,236 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
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.Extensions.Primitives;
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="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
public static async Task<ActionResult> GetStaticRemoteStreamResult(
StreamState state,
bool isHeadRequest,
ControllerBase controller,
CancellationTokenSource cancellationTokenSource)
{
HttpClient httpClient = new HttpClient();
var responseHeaders = controller.Response.Headers;
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
}
var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
var contentType = response.Content.Headers.ContentType.ToString();
responseHeaders[HeaderNames.AcceptRanges] = "none";
// Seeing cases of -1 here
if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value >= 0)
{
responseHeaders[HeaderNames.ContentLength] = response.Content.Headers.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
}
if (isHeadRequest)
{
using (response)
{
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="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="controller">The <see cref="ControllerBase"/> managing the response.</param>
/// <returns>An <see cref="ActionResult"/> the file.</returns>
// TODO: caching doesn't work
public static ActionResult GetStaticFileResult(
string path,
string contentType,
DateTime dateLastModified,
TimeSpan? cacheDuration,
bool isHeadRequest,
ControllerBase controller)
{
bool disableCaching = false;
if (controller.Request.Headers.TryGetValue(HeaderNames.CacheControl, out StringValues headerValue))
{
disableCaching = headerValue.FirstOrDefault().Contains("no-cache", StringComparison.InvariantCulture);
}
bool parsingSuccessful = DateTime.TryParseExact(controller.Request.Headers[HeaderNames.IfModifiedSince], "ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false), DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime ifModifiedSinceHeader);
// if the parsing of the IfModifiedSince header was not successfull, disable caching
if (!parsingSuccessful)
{
disableCaching = true;
}
controller.Response.ContentType = contentType;
controller.Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateLastModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
controller.Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
if (disableCaching)
{
controller.Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
controller.Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
}
else
{
if (cacheDuration.HasValue)
{
controller.Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
}
else
{
controller.Response.Headers.Add(HeaderNames.CacheControl, "public");
}
controller.Response.Headers.Add(HeaderNames.LastModified, dateLastModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
if (!(dateLastModified > ifModifiedSinceHeader))
{
if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
{
controller.Response.StatusCode = StatusCodes.Status304NotModified;
return new ContentResult();
}
}
}
// 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();
}
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)
{
IHeaderDictionary responseHeaders = controller.Response.Headers;
// Use the command line args with a dummy playlist path
var outputPath = state.OutputFilePath;
responseHeaders[HeaderNames.AcceptRanges] = "none";
var contentType = state.GetMimeType(outputPath);
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
// TODO (from api-migration): Investigate if this is still neccessary as we migrated away from ServiceStack
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
if (contentLength.HasValue)
{
responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
responseHeaders.Remove(HeaderNames.ContentLength);
}
// 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();
}
Stream stream = new MemoryStream();
await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false);
return controller.File(stream, contentType);
}
finally
{
transcodingLock.Release();
}
}
/// <summary>
/// Gets the length of the estimated content.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.Nullable{System.Int64}.</returns>
private static long? GetEstimatedContentLength(StreamState state)
{
var totalBitrate = state.TotalOutputBitrate ?? 0;
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
{
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
}
return null;
}
}
}

@ -1,32 +1,255 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Api.Models;
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
/// The streaming helpers.
/// </summary>
public class StreamingHelpers
public static class StreamingHelpers
{
public static async Task<StreamState> GetStreamingState(
Guid itemId,
long? startTimeTicks,
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,
IMediaSourceManager mediaSourceManager,
IUserManager userManager,
ILibraryManager libraryManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
ISubtitleEncoder subtitleEncoder,
IConfiguration configuration,
IDlnaManager dlnaManager,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
TranscodingJobType transcodingJobType,
bool isVideoRequest,
CancellationToken cancellationToken)
{
EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
// Parse the DLNA time seek header
if (!startTimeTicks.HasValue)
{
var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
startTimeTicks = ParseTimeSeekHeader(timeSeek);
}
if (!string.IsNullOrWhiteSpace(@params))
{
// What is this?
ParseParams(request);
}
var streamOptions = ParseStreamOptions(request.Query);
var url = request.Path.Value.Split('.').Last();
if (string.IsNullOrEmpty(audioCodec))
{
audioCodec = encodingHelper.InferAudioCodec(url);
}
var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) ||
string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
{
// TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive
Request = request,
RequestedUrl = url,
UserAgent = request.Headers[HeaderNames.UserAgent],
EnableDlnaHeaders = enableDlnaHeaders
};
var auth = authorizationContext.GetAuthorizationInfo(request);
if (!auth.UserId.Equals(Guid.Empty))
{
state.User = userManager.GetUserById(auth.UserId);
}
/*
if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
(Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
(Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
{
state.SegmentLength = 6;
}
*/
if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
{
state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(audioCodec))
{
state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(subtitleCodec))
{
state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
?? state.SupportedSubtitleCodecs.FirstOrDefault();
}
var item = libraryManager.GetItemById(itemId);
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
/*
var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
if (primaryImage != null)
{
state.AlbumCoverPath = primaryImage.Path;
}
*/
MediaSourceInfo? mediaSource = null;
if (string.IsNullOrWhiteSpace(liveStreamId))
{
var currentJob = !string.IsNullOrWhiteSpace(playSessionId)
? transcodingJobHelper.GetTranscodingJob(playSessionId)
: null;
if (currentJob != null)
{
mediaSource = currentJob.MediaSource;
}
if (mediaSource == null)
{
var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false);
mediaSource = string.IsNullOrEmpty(mediaSourceId)
? mediaSources[0]
: mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture));
if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId)
{
mediaSource = mediaSources[0];
}
}
}
else
{
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false);
mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2;
}
encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
var containerInternal = Path.GetExtension(state.RequestedUrl);
if (string.IsNullOrEmpty(container))
{
containerInternal = container;
}
if (string.IsNullOrEmpty(containerInternal))
{
containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
}
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream);
state.OutputAudioCodec = audioCodec;
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (isVideoRequest)
{
state.OutputVideoCodec = state.VideoRequest.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,
videoRequest.MaxWidth,
videoRequest.MaxHeight);
videoRequest.MaxWidth = resolution.MaxWidth;
videoRequest.MaxHeight = resolution.MaxHeight;
}
}
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static);
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state)
: ('.' + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, 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)
{
@ -54,7 +277,7 @@ namespace Jellyfin.Api.Helpers
if (!isStaticallyStreamed && profile != null)
{
AddTimeSeekResponseHeaders(state, responseHeaders);
AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
}
}
@ -82,51 +305,18 @@ namespace Jellyfin.Api.Helpers
{
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 dlna headers.
/// </summary>
/// <param name="startTimeTicks">The start time ticks.</param>
/// <param name="request">The <see cref="HttpRequest"/>.</param>
public void ParseDlnaHeaders(long? startTimeTicks, HttpRequest request)
{
if (!startTimeTicks.HasValue)
{
var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
startTimeTicks = ParseTimeSeekHeader(timeSeek);
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>
public long? ParseTimeSeekHeader(string value)
/// <param name="value">The time seek header string.</param>
/// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
public static long? ParseTimeSeekHeader(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
@ -138,12 +328,13 @@ namespace Jellyfin.Api.Helpers
{
throw new ArgumentException("Invalid timeseek header");
}
int index = value.IndexOf('-');
int index = value.IndexOf('-', StringComparison.InvariantCulture);
value = index == -1
? value.Substring(Npt.Length)
: value.Substring(Npt.Length, index - Npt.Length);
if (value.IndexOf(':') == -1)
if (value.IndexOf(':', StringComparison.InvariantCulture) == -1)
{
// Parses npt times in the format of '417.33'
if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
@ -169,15 +360,45 @@ namespace Jellyfin.Api.Helpers
{
throw new ArgumentException("Invalid timeseek header");
}
timeFactor /= 60;
}
return TimeSpan.FromSeconds(secondsSum).Ticks;
}
public void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders)
/// <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>
public static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
{
var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
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>
public 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,
@ -190,5 +411,369 @@ namespace Jellyfin.Api.Helpers
startSeconds,
runtimeSeconds));
}
/// <summary>
/// Gets the output file extension.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.String.</returns>
public static string? GetOutputFileExtension(StreamState state)
{
var ext = Path.GetExtension(state.RequestedUrl);
if (!string.IsNullOrEmpty(ext))
{
return ext;
}
var isVideoRequest = state.VideoRequest != null;
// Try to infer based on the desired video codec
if (isVideoRequest)
{
var videoCodec = state.VideoRequest.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 (!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.VideoRequest == null
? 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.VideoRequest == null ? 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 void ParseParams(StreamRequest request)
{
var vals = request.Params.Split(';');
var videoRequest = request as VideoStreamRequest;
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,16 +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;
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
@ -30,9 +42,17 @@ namespace Jellyfin.Api.Helpers
/// </summary>
private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
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 IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ISessionManager _sessionManager;
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
@ -40,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>
@ -63,7 +109,13 @@ namespace Jellyfin.Api.Helpers
}
}
public static TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
/// <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)
{
@ -361,14 +413,24 @@ namespace Jellyfin.Api.Helpers
}
}
/// <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(
TranscodingJob job,
StreamState state,
TimeSpan? transcodingPosition,
float? framerate,
double? percentComplete,
long? bytesTranscoded,
int? bitRate)
TranscodingJobDto job,
StreamState state,
TimeSpan? transcodingPosition,
float? framerate,
double? percentComplete,
long? bytesTranscoded,
int? bitRate)
{
var ticks = transcodingPosition?.Ticks;
@ -405,5 +467,374 @@ namespace Jellyfin.Api.Helpers
});
}
}
/// <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);
}
}
}
}

@ -5,36 +5,77 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
namespace Jellyfin.Api.Models
namespace Jellyfin.Api.Models.StreamingDtos
{
/// <summary>
/// The stream state dto.
/// </summary>
public class StreamState : EncodingJobInfo, IDisposable
{
private readonly IMediaSourceManager _mediaSourceManager;
private bool _disposed = false;
public string RequestedUrl { get; set; }
public StreamRequest Request
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="mediaSourceManager" /> 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)
{
get => (StreamRequest)BaseRequest;
set
{
BaseRequest = value;
IsVideoRequest = VideoRequest != null;
}
_mediaSourceManager = mediaSourceManager;
_transcodingJobHelper = transcodingJobHelper;
}
public TranscodingThrottler TranscodingThrottler { get; set; }
/// <summary>
/// Gets or sets the requested url.
/// </summary>
public string? RequestedUrl { get; set; }
// /// <summary>
// /// Gets or sets the request.
// /// </summary>
// public StreamRequest Request
// {
// get => (StreamRequest)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 VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
public IDirectStreamProvider DirectStreamProvider { get; set; }
/// <summary>
/// Gets or sets the direct stream provicer.
/// </summary>
public IDirectStreamProvider? DirectStreamProvider { get; set; }
public string WaitForPath { 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 VideoStreamRequest;
/// <summary>
/// Gets the segment length.
/// </summary>
public int SegmentLength
{
get
@ -74,6 +115,9 @@ namespace Jellyfin.Api.Models
}
}
/// <summary>
/// Gets the minimum number of segments.
/// </summary>
public int MinSegments
{
get
@ -87,35 +131,53 @@ namespace Jellyfin.Api.Models
}
}
public string UserAgent { get; set; }
/// <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; }
public DeviceProfile DeviceProfile { get; set; }
/// <summary>
/// Gets or sets the device profile.
/// </summary>
public DeviceProfile? DeviceProfile { get; set; }
public TranscodingJobDto TranscodingJob { get; set; }
/// <summary>
/// Gets or sets the transcoding job.
/// </summary>
public TranscodingJobDto? TranscodingJob { get; set; }
public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
: base(transcodingType)
/// <inheritdoc />
public void Dispose()
{
_mediaSourceManager = mediaSourceManager;
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);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
_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)

@ -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
{
}

@ -1263,6 +1263,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…
Cancel
Save