mirror of https://github.com/jellyfin/jellyfin.git
commit
634ee2d1e9
|
@ -632,6 +632,9 @@ namespace Emby.Server.Implementations
|
||||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||||
|
serviceCollection.AddScoped<MediaInfoHelper>();
|
||||||
|
serviceCollection.AddScoped<AudioHelper>();
|
||||||
|
serviceCollection.AddScoped<DynamicHlsHelper>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,93 +1,32 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.Models.StreamingDtos;
|
using 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.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The audio controller.
|
/// The audio controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
// TODO: In order to authenticate this in the future, Dlna playback will require updating
|
||||||
public class AudioController : BaseJellyfinApiController
|
public class AudioController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IDlnaManager _dlnaManager;
|
private readonly AudioHelper _audioHelper;
|
||||||
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 IFileSystem _fileSystem;
|
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly IDeviceManager _deviceManager;
|
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
|
||||||
|
|
||||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
||||||
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
|
public AudioController(AudioHelper audioHelper)
|
||||||
/// <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="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="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
public AudioController(
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IUserManager userManger,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
ISubtitleEncoder subtitleEncoder,
|
|
||||||
IConfiguration configuration,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
TranscodingJobHelper transcodingJobHelper,
|
|
||||||
IHttpClientFactory httpClientFactory)
|
|
||||||
{
|
{
|
||||||
_dlnaManager = dlnaManager;
|
_audioHelper = audioHelper;
|
||||||
_authContext = authorizationContext;
|
|
||||||
_userManager = userManger;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_subtitleEncoder = subtitleEncoder;
|
|
||||||
_configuration = configuration;
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
_httpClientFactory = httpClientFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] EncodingContext? context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||||
{
|
{
|
||||||
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
|
||||||
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
|
@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
||||||
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);
|
|
||||||
|
|
||||||
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
|
|
||||||
{
|
|
||||||
AllowEndOfFile = false
|
|
||||||
}.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);
|
|
||||||
|
|
||||||
var httpClient = _httpClientFactory.CreateClient();
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
|
|
||||||
{
|
|
||||||
AllowEndOfFile = false
|
|
||||||
}.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,
|
|
||||||
this,
|
|
||||||
_transcodingJobHelper,
|
|
||||||
ffmpegCommandLineArguments,
|
|
||||||
Request,
|
|
||||||
_transcodingJobType,
|
|
||||||
cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
|
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
|
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
|
||||||
return _deviceManager.GetDevices(deviceQuery);
|
return _deviceManager.GetDevices(deviceQuery);
|
||||||
|
|
|
@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.Models.PlaybackDtos;
|
using Jellyfin.Api.Models.PlaybackDtos;
|
||||||
using Jellyfin.Api.Models.StreamingDtos;
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
|
@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IDeviceManager _deviceManager;
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||||
private readonly INetworkManager _networkManager;
|
|
||||||
private readonly ILogger<DynamicHlsController> _logger;
|
private readonly ILogger<DynamicHlsController> _logger;
|
||||||
private readonly EncodingHelper _encodingHelper;
|
private readonly EncodingHelper _encodingHelper;
|
||||||
|
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||||
|
|
||||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
|
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
|
||||||
|
|
||||||
|
@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||||
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
|
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
|
||||||
|
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
|
||||||
public DynamicHlsController(
|
public DynamicHlsController(
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IDeviceManager deviceManager,
|
IDeviceManager deviceManager,
|
||||||
TranscodingJobHelper transcodingJobHelper,
|
TranscodingJobHelper transcodingJobHelper,
|
||||||
INetworkManager networkManager,
|
ILogger<DynamicHlsController> logger,
|
||||||
ILogger<DynamicHlsController> logger)
|
DynamicHlsHelper dynamicHlsHelper)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_deviceManager = deviceManager;
|
_deviceManager = deviceManager;
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
_networkManager = networkManager;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_dynamicHlsHelper = dynamicHlsHelper;
|
||||||
|
|
||||||
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||||
}
|
}
|
||||||
|
@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||||
{
|
{
|
||||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
var streamingRequest = new HlsVideoRequestDto
|
var streamingRequest = new HlsVideoRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
|
@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||||
};
|
};
|
||||||
|
|
||||||
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
|
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||||
{
|
{
|
||||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
var streamingRequest = new HlsAudioRequestDto
|
var streamingRequest = new HlsAudioRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
|
@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||||
};
|
};
|
||||||
|
|
||||||
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
|
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ActionResult> GetMasterPlaylistInternal(
|
|
||||||
StreamingRequestDto streamingRequest,
|
|
||||||
bool isHeadRequest,
|
|
||||||
bool enableAdaptiveBitrateStreaming,
|
|
||||||
CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
Response.Headers.Add(HeaderNames.Expires, "0");
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
|
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
|
|
||||||
builder.AppendLine("#EXTM3U");
|
|
||||||
|
|
||||||
var isLiveStream = state.IsSegmentedLiveStream;
|
|
||||||
|
|
||||||
var queryString = Request.QueryString.ToString();
|
|
||||||
|
|
||||||
// from universal audio service
|
|
||||||
if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
|
|
||||||
{
|
|
||||||
queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// from universal audio service
|
|
||||||
if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
|
|
||||||
{
|
|
||||||
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main stream
|
|
||||||
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
|
||||||
|
|
||||||
playlistUrl += queryString;
|
|
||||||
|
|
||||||
var subtitleStreams = state.MediaSource
|
|
||||||
.MediaStreams
|
|
||||||
.Where(i => i.IsTextSubtitleStream)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
|
|
||||||
? "subs"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// If we're burning in subtitles then don't add additional subs to the manifest
|
|
||||||
if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
|
||||||
{
|
|
||||||
subtitleGroup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
|
||||||
{
|
|
||||||
AddSubtitles(state, subtitleStreams, builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
|
||||||
|
|
||||||
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming))
|
|
||||||
{
|
|
||||||
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
|
|
||||||
|
|
||||||
// By default, vary by just 200k
|
|
||||||
var variation = GetBitrateVariation(totalBitrate);
|
|
||||||
|
|
||||||
var newBitrate = totalBitrate - variation;
|
|
||||||
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
|
||||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
|
||||||
|
|
||||||
variation *= 2;
|
|
||||||
newBitrate = totalBitrate - variation;
|
|
||||||
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
|
||||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
|
private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
|
||||||
{
|
{
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
using var state = await StreamingHelpers.GetStreamingState(
|
||||||
|
@ -1411,330 +1303,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
|
|
||||||
{
|
|
||||||
var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
|
|
||||||
const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
|
|
||||||
|
|
||||||
foreach (var stream in subtitles)
|
|
||||||
{
|
|
||||||
var name = stream.DisplayTitle;
|
|
||||||
|
|
||||||
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
|
|
||||||
var isForced = stream.IsForced;
|
|
||||||
|
|
||||||
var url = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
|
|
||||||
state.Request.MediaSourceId,
|
|
||||||
stream.Index.ToString(CultureInfo.InvariantCulture),
|
|
||||||
30.ToString(CultureInfo.InvariantCulture),
|
|
||||||
ClaimHelpers.GetToken(Request.HttpContext.User));
|
|
||||||
|
|
||||||
var line = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
Format,
|
|
||||||
name,
|
|
||||||
isDefault ? "YES" : "NO",
|
|
||||||
isForced ? "YES" : "NO",
|
|
||||||
url,
|
|
||||||
stream.Language ?? "Unknown");
|
|
||||||
|
|
||||||
builder.AppendLine(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
|
|
||||||
{
|
|
||||||
builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
|
|
||||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
|
|
||||||
.Append(",AVERAGE-BANDWIDTH=")
|
|
||||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
AppendPlaylistCodecsField(builder, state);
|
|
||||||
|
|
||||||
AppendPlaylistResolutionField(builder, state);
|
|
||||||
|
|
||||||
AppendPlaylistFramerateField(builder, state);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
|
||||||
{
|
|
||||||
builder.Append(",SUBTITLES=\"")
|
|
||||||
.Append(subtitleGroup)
|
|
||||||
.Append('"');
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Append(Environment.NewLine);
|
|
||||||
builder.AppendLine(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Appends a CODECS field containing formatted strings of
|
|
||||||
/// the active streams output video and audio codecs.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
|
||||||
/// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
|
|
||||||
/// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
|
|
||||||
/// <param name="builder">StringBuilder to append the field to.</param>
|
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
|
||||||
private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
|
|
||||||
{
|
|
||||||
// Video
|
|
||||||
string videoCodecs = string.Empty;
|
|
||||||
int? videoCodecLevel = GetOutputVideoCodecLevel(state);
|
|
||||||
if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
|
|
||||||
{
|
|
||||||
videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio
|
|
||||||
string audioCodecs = string.Empty;
|
|
||||||
if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
|
|
||||||
{
|
|
||||||
audioCodecs = GetPlaylistAudioCodecs(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder codecs = new StringBuilder();
|
|
||||||
|
|
||||||
codecs.Append(videoCodecs);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
|
|
||||||
{
|
|
||||||
codecs.Append(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
codecs.Append(audioCodecs);
|
|
||||||
|
|
||||||
if (codecs.Length > 1)
|
|
||||||
{
|
|
||||||
builder.Append(",CODECS=\"")
|
|
||||||
.Append(codecs)
|
|
||||||
.Append('"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Appends a RESOLUTION field containing the resolution of the output stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
|
||||||
/// <param name="builder">StringBuilder to append the field to.</param>
|
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
|
||||||
private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
|
|
||||||
{
|
|
||||||
if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
|
|
||||||
{
|
|
||||||
builder.Append(",RESOLUTION=")
|
|
||||||
.Append(state.OutputWidth.GetValueOrDefault())
|
|
||||||
.Append('x')
|
|
||||||
.Append(state.OutputHeight.GetValueOrDefault());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Appends a FRAME-RATE field containing the framerate of the output stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
|
||||||
/// <param name="builder">StringBuilder to append the field to.</param>
|
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
|
||||||
private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
|
|
||||||
{
|
|
||||||
double? framerate = null;
|
|
||||||
if (state.TargetFramerate.HasValue)
|
|
||||||
{
|
|
||||||
framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
|
|
||||||
}
|
|
||||||
else if (state.VideoStream?.RealFrameRate != null)
|
|
||||||
{
|
|
||||||
framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (framerate.HasValue)
|
|
||||||
{
|
|
||||||
builder.Append(",FRAME-RATE=")
|
|
||||||
.Append(framerate.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming)
|
|
||||||
{
|
|
||||||
// Within the local network this will likely do more harm than good.
|
|
||||||
var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString();
|
|
||||||
if (_networkManager.IsInLocalNetwork(ip))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableAdaptiveBitrateStreaming)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
|
|
||||||
{
|
|
||||||
// Opening live streams is so slow it's not even worth it
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.IsOutputVideo)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Having problems in android
|
|
||||||
return false;
|
|
||||||
// return state.VideoRequest.VideoBitRate.HasValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the H.26X level of the output video stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
|
||||||
/// <returns>H.26X level of the output video stream.</returns>
|
|
||||||
private int? GetOutputVideoCodecLevel(StreamState state)
|
|
||||||
{
|
|
||||||
string? levelString;
|
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& state.VideoStream.Level.HasValue)
|
|
||||||
{
|
|
||||||
levelString = state.VideoStream?.Level.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
|
|
||||||
{
|
|
||||||
return parsedLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
|
|
||||||
/// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
|
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
|
||||||
/// <returns>Formatted audio codec string.</returns>
|
|
||||||
private string GetPlaylistAudioCodecs(StreamState state)
|
|
||||||
{
|
|
||||||
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
|
|
||||||
return HlsCodecStringHelpers.GetAACString(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return HlsCodecStringHelpers.GetMP3String();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return HlsCodecStringHelpers.GetAC3String();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return HlsCodecStringHelpers.GetEAC3String();
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a formatted string of the output video codec, for use in the CODECS field.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
|
|
||||||
/// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
|
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
|
||||||
/// <param name="codec">Video codec.</param>
|
|
||||||
/// <param name="level">Video level.</param>
|
|
||||||
/// <returns>Formatted video codec string.</returns>
|
|
||||||
private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
|
|
||||||
{
|
|
||||||
if (level == 0)
|
|
||||||
{
|
|
||||||
// This is 0 when there's no requested H.26X level in the device profile
|
|
||||||
// and the source is not encoded in H.26X
|
|
||||||
_logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
|
|
||||||
return HlsCodecStringHelpers.GetH264String(profile, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
|
|
||||||
|
|
||||||
return HlsCodecStringHelpers.GetH265String(profile, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetBitrateVariation(int bitrate)
|
|
||||||
{
|
|
||||||
// By default, vary by just 50k
|
|
||||||
var variation = 50000;
|
|
||||||
|
|
||||||
if (bitrate >= 10000000)
|
|
||||||
{
|
|
||||||
variation = 2000000;
|
|
||||||
}
|
|
||||||
else if (bitrate >= 5000000)
|
|
||||||
{
|
|
||||||
variation = 1500000;
|
|
||||||
}
|
|
||||||
else if (bitrate >= 3000000)
|
|
||||||
{
|
|
||||||
variation = 1000000;
|
|
||||||
}
|
|
||||||
else if (bitrate >= 2000000)
|
|
||||||
{
|
|
||||||
variation = 500000;
|
|
||||||
}
|
|
||||||
else if (bitrate >= 1000000)
|
|
||||||
{
|
|
||||||
variation = 300000;
|
|
||||||
}
|
|
||||||
else if (bitrate >= 600000)
|
|
||||||
{
|
|
||||||
variation = 200000;
|
|
||||||
}
|
|
||||||
else if (bitrate >= 400000)
|
|
||||||
{
|
|
||||||
variation = 100000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return variation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ReplaceBitrate(string url, int oldValue, int newValue)
|
|
||||||
{
|
|
||||||
return url.Replace(
|
|
||||||
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
|
|
||||||
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double[] GetSegmentLengths(StreamState state)
|
private double[] GetSegmentLengths(StreamState state)
|
||||||
{
|
{
|
||||||
var result = new List<double>();
|
var result = new List<double>();
|
||||||
|
@ -2089,7 +1657,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this);
|
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private long GetEndPositionTicks(StreamState state, int requestedIndex)
|
private long GetEndPositionTicks(StreamState state, int requestedIndex)
|
||||||
|
|
|
@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = segmentId + Path.GetExtension(Request.Path);
|
||||||
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
|
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this);
|
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this);
|
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -592,11 +592,11 @@ namespace Jellyfin.Api.Controllers
|
||||||
GenreIds = RequestHelpers.GetGuids(genreIds)
|
GenreIds = RequestHelpers.GetGuids(genreIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!librarySeriesId.Equals(Guid.Empty))
|
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
|
||||||
{
|
{
|
||||||
query.IsSeries = true;
|
query.IsSeries = true;
|
||||||
|
|
||||||
if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series)
|
if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
|
||||||
{
|
{
|
||||||
query.Name = series.Name;
|
query.Name = series.Name;
|
||||||
}
|
}
|
||||||
|
@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="validateLogin">Validate login.</param>
|
/// <param name="validateLogin">Validate login.</param>
|
||||||
/// <response code="200">Created listings provider returned.</response>
|
/// <response code="200">Created listings provider returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||||
[HttpGet("ListingProviders")]
|
[HttpPost("ListingProviders")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||||
|
|
|
@ -1,30 +1,18 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.Models.MediaInfoDtos;
|
using Jellyfin.Api.Models.MediaInfoDtos;
|
||||||
using Jellyfin.Api.Models.VideoDtos;
|
using Jellyfin.Api.Models.VideoDtos;
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly INetworkManager _networkManager;
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly IAuthorizationContext _authContext;
|
private readonly IAuthorizationContext _authContext;
|
||||||
private readonly ILogger<MediaInfoController> _logger;
|
private readonly ILogger<MediaInfoController> _logger;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly MediaInfoHelper _mediaInfoHelper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
|
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
|
||||||
|
@ -55,32 +40,23 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
|
||||||
public MediaInfoController(
|
public MediaInfoController(
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IDeviceManager deviceManager,
|
IDeviceManager deviceManager,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
INetworkManager networkManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IUserManager userManager,
|
|
||||||
IAuthorizationContext authContext,
|
IAuthorizationContext authContext,
|
||||||
ILogger<MediaInfoController> logger,
|
ILogger<MediaInfoController> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
MediaInfoHelper mediaInfoHelper)
|
||||||
{
|
{
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_deviceManager = deviceManager;
|
_deviceManager = deviceManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_networkManager = networkManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_userManager = userManager;
|
|
||||||
_authContext = authContext;
|
_authContext = authContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_mediaInfoHelper = mediaInfoHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
|
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
|
||||||
{
|
{
|
||||||
return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
|
return await _mediaInfoHelper.GetPlaybackInfo(
|
||||||
|
itemId,
|
||||||
|
userId)
|
||||||
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -153,7 +132,12 @@ namespace Jellyfin.Api.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
|
var info = await _mediaInfoHelper.GetPlaybackInfo(
|
||||||
|
itemId,
|
||||||
|
userId,
|
||||||
|
mediaSourceId,
|
||||||
|
liveStreamId)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (profile != null)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
|
@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
|
|
||||||
foreach (var mediaSource in info.MediaSources)
|
foreach (var mediaSource in info.MediaSources)
|
||||||
{
|
{
|
||||||
SetDeviceSpecificData(
|
_mediaInfoHelper.SetDeviceSpecificData(
|
||||||
item,
|
item,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
profile,
|
profile,
|
||||||
|
@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers
|
||||||
enableDirectStream,
|
enableDirectStream,
|
||||||
enableTranscoding,
|
enableTranscoding,
|
||||||
allowVideoStreamCopy,
|
allowVideoStreamCopy,
|
||||||
allowAudioStreamCopy);
|
allowAudioStreamCopy,
|
||||||
|
Request.HttpContext.Connection.RemoteIpAddress.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
SortMediaSources(info, maxStreamingBitrate);
|
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoOpenLiveStream)
|
if (autoOpenLiveStream)
|
||||||
|
@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers
|
||||||
|
|
||||||
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
||||||
{
|
{
|
||||||
var openStreamResult = await OpenMediaSource(new LiveStreamRequest
|
var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
|
||||||
{
|
Request,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
new LiveStreamRequest
|
||||||
DeviceProfile = deviceProfile?.DeviceProfile,
|
{
|
||||||
EnableDirectPlay = enableDirectPlay,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
EnableDirectStream = enableDirectStream,
|
DeviceProfile = deviceProfile?.DeviceProfile,
|
||||||
ItemId = itemId,
|
EnableDirectPlay = enableDirectPlay,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
EnableDirectStream = enableDirectStream,
|
||||||
MaxStreamingBitrate = maxStreamingBitrate,
|
ItemId = itemId,
|
||||||
PlaySessionId = info.PlaySessionId,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
StartTimeTicks = startTimeTicks,
|
MaxStreamingBitrate = maxStreamingBitrate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
PlaySessionId = info.PlaySessionId,
|
||||||
UserId = userId ?? Guid.Empty,
|
StartTimeTicks = startTimeTicks,
|
||||||
OpenToken = mediaSource.OpenToken
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
}).ConfigureAwait(false);
|
UserId = userId ?? Guid.Empty,
|
||||||
|
OpenToken = mediaSource.OpenToken
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
info.MediaSources = new[] { openStreamResult.MediaSource };
|
info.MediaSources = new[] { openStreamResult.MediaSource };
|
||||||
}
|
}
|
||||||
|
@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
foreach (var mediaSource in info.MediaSources)
|
foreach (var mediaSource in info.MediaSources)
|
||||||
{
|
{
|
||||||
NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
|
_mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +258,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
EnableDirectStream = enableDirectStream,
|
EnableDirectStream = enableDirectStream,
|
||||||
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
||||||
};
|
};
|
||||||
return await OpenMediaSource(request).ConfigureAwait(false);
|
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
|
|
||||||
Guid id,
|
|
||||||
Guid? userId,
|
|
||||||
string? mediaSourceId = null,
|
|
||||||
string? liveStreamId = null)
|
|
||||||
{
|
|
||||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
|
||||||
? _userManager.GetUserById(userId.Value)
|
|
||||||
: null;
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
var result = new PlaybackInfoResponse();
|
|
||||||
|
|
||||||
MediaSourceInfo[] mediaSources;
|
|
||||||
if (string.IsNullOrWhiteSpace(liveStreamId))
|
|
||||||
{
|
|
||||||
// TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
|
|
||||||
var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mediaSourceId))
|
|
||||||
{
|
|
||||||
mediaSources = mediaSourcesList.ToArray();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mediaSources = mediaSourcesList
|
|
||||||
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
mediaSources = new[] { mediaSource };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaSources.Length == 0)
|
|
||||||
{
|
|
||||||
result.MediaSources = Array.Empty<MediaSourceInfo>();
|
|
||||||
|
|
||||||
result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
|
|
||||||
// Should we move this directly into MediaSourceManager?
|
|
||||||
result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
|
|
||||||
|
|
||||||
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
|
|
||||||
{
|
|
||||||
mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetDeviceSpecificData(
|
|
||||||
BaseItem item,
|
|
||||||
MediaSourceInfo mediaSource,
|
|
||||||
DeviceProfile profile,
|
|
||||||
AuthorizationInfo auth,
|
|
||||||
long? maxBitrate,
|
|
||||||
long startTimeTicks,
|
|
||||||
string mediaSourceId,
|
|
||||||
int? audioStreamIndex,
|
|
||||||
int? subtitleStreamIndex,
|
|
||||||
int? maxAudioChannels,
|
|
||||||
string playSessionId,
|
|
||||||
Guid userId,
|
|
||||||
bool enableDirectPlay,
|
|
||||||
bool enableDirectStream,
|
|
||||||
bool enableTranscoding,
|
|
||||||
bool allowVideoStreamCopy,
|
|
||||||
bool allowAudioStreamCopy)
|
|
||||||
{
|
|
||||||
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
|
|
||||||
|
|
||||||
var options = new VideoOptions
|
|
||||||
{
|
|
||||||
MediaSources = new[] { mediaSource },
|
|
||||||
Context = EncodingContext.Streaming,
|
|
||||||
DeviceId = auth.DeviceId,
|
|
||||||
ItemId = item.Id,
|
|
||||||
Profile = profile,
|
|
||||||
MaxAudioChannels = maxAudioChannels
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
options.MediaSourceId = mediaSourceId;
|
|
||||||
options.AudioStreamIndex = audioStreamIndex;
|
|
||||||
options.SubtitleStreamIndex = subtitleStreamIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
|
||||||
|
|
||||||
if (!enableDirectPlay)
|
|
||||||
{
|
|
||||||
mediaSource.SupportsDirectPlay = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableDirectStream)
|
|
||||||
{
|
|
||||||
mediaSource.SupportsDirectStream = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableTranscoding)
|
|
||||||
{
|
|
||||||
mediaSource.SupportsTranscoding = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is Audio)
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
|
|
||||||
user.Username,
|
|
||||||
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
|
|
||||||
user.Username,
|
|
||||||
user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
|
|
||||||
user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
|
|
||||||
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Beginning of Playback Determination: Attempt DirectPlay first
|
|
||||||
if (mediaSource.SupportsDirectPlay)
|
|
||||||
{
|
|
||||||
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
|
||||||
{
|
|
||||||
mediaSource.SupportsDirectPlay = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var supportsDirectStream = mediaSource.SupportsDirectStream;
|
|
||||||
|
|
||||||
// Dummy this up to fool StreamBuilder
|
|
||||||
mediaSource.SupportsDirectStream = true;
|
|
||||||
options.MaxBitrate = maxBitrate;
|
|
||||||
|
|
||||||
if (item is Audio)
|
|
||||||
{
|
|
||||||
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
|
|
||||||
{
|
|
||||||
options.ForceDirectPlay = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (item is Video)
|
|
||||||
{
|
|
||||||
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
|
||||||
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
|
||||||
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
|
||||||
{
|
|
||||||
options.ForceDirectPlay = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
|
||||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|
|
||||||
? streamBuilder.BuildAudioItem(options)
|
|
||||||
: streamBuilder.BuildVideoItem(options);
|
|
||||||
|
|
||||||
if (streamInfo == null || !streamInfo.IsDirectStream)
|
|
||||||
{
|
|
||||||
mediaSource.SupportsDirectPlay = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set this back to what it was
|
|
||||||
mediaSource.SupportsDirectStream = supportsDirectStream;
|
|
||||||
|
|
||||||
if (streamInfo != null)
|
|
||||||
{
|
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaSource.SupportsDirectStream)
|
|
||||||
{
|
|
||||||
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
|
||||||
{
|
|
||||||
mediaSource.SupportsDirectStream = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
|
|
||||||
|
|
||||||
if (item is Audio)
|
|
||||||
{
|
|
||||||
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
|
|
||||||
{
|
|
||||||
options.ForceDirectStream = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (item is Video)
|
|
||||||
{
|
|
||||||
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
|
||||||
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
|
||||||
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
|
||||||
{
|
|
||||||
options.ForceDirectStream = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
|
||||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|
|
||||||
? streamBuilder.BuildAudioItem(options)
|
|
||||||
: streamBuilder.BuildVideoItem(options);
|
|
||||||
|
|
||||||
if (streamInfo == null || !streamInfo.IsDirectStream)
|
|
||||||
{
|
|
||||||
mediaSource.SupportsDirectStream = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamInfo != null)
|
|
||||||
{
|
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaSource.SupportsTranscoding)
|
|
||||||
{
|
|
||||||
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
|
|
||||||
|
|
||||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
|
||||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|
|
||||||
? streamBuilder.BuildAudioItem(options)
|
|
||||||
: streamBuilder.BuildVideoItem(options);
|
|
||||||
|
|
||||||
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
|
||||||
{
|
|
||||||
if (streamInfo != null)
|
|
||||||
{
|
|
||||||
streamInfo.PlaySessionId = playSessionId;
|
|
||||||
streamInfo.StartPositionTicks = startTimeTicks;
|
|
||||||
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
|
||||||
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
|
|
||||||
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
|
||||||
mediaSource.TranscodingContainer = streamInfo.Container;
|
|
||||||
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
|
||||||
|
|
||||||
// Do this after the above so that StartPositionTicks is set
|
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (streamInfo != null)
|
|
||||||
{
|
|
||||||
streamInfo.PlaySessionId = playSessionId;
|
|
||||||
|
|
||||||
if (streamInfo.PlayMethod == PlayMethod.Transcode)
|
|
||||||
{
|
|
||||||
streamInfo.StartPositionTicks = startTimeTicks;
|
|
||||||
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
|
||||||
|
|
||||||
if (!allowVideoStreamCopy)
|
|
||||||
{
|
|
||||||
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowAudioStreamCopy)
|
|
||||||
{
|
|
||||||
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSource.TranscodingContainer = streamInfo.Container;
|
|
||||||
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowAudioStreamCopy)
|
|
||||||
{
|
|
||||||
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSource.TranscodingContainer = streamInfo.Container;
|
|
||||||
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
|
||||||
|
|
||||||
// Do this after the above so that StartPositionTicks is set
|
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attachment in mediaSource.MediaAttachments)
|
|
||||||
{
|
|
||||||
attachment.DeliveryUrl = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"/Videos/{0}/{1}/Attachments/{2}",
|
|
||||||
item.Id,
|
|
||||||
mediaSource.Id,
|
|
||||||
attachment.Index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
|
|
||||||
{
|
|
||||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
|
||||||
|
|
||||||
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var profile = request.DeviceProfile;
|
|
||||||
if (profile == null)
|
|
||||||
{
|
|
||||||
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
|
||||||
if (caps != null)
|
|
||||||
{
|
|
||||||
profile = caps.DeviceProfile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(request.ItemId);
|
|
||||||
|
|
||||||
SetDeviceSpecificData(
|
|
||||||
item,
|
|
||||||
result.MediaSource,
|
|
||||||
profile,
|
|
||||||
authInfo,
|
|
||||||
request.MaxStreamingBitrate,
|
|
||||||
request.StartTimeTicks ?? 0,
|
|
||||||
result.MediaSource.Id,
|
|
||||||
request.AudioStreamIndex,
|
|
||||||
request.SubtitleStreamIndex,
|
|
||||||
request.MaxAudioChannels,
|
|
||||||
request.PlaySessionId,
|
|
||||||
request.UserId,
|
|
||||||
request.EnableDirectPlay,
|
|
||||||
request.EnableDirectStream,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
|
|
||||||
{
|
|
||||||
result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// here was a check if (result.MediaSource != null) but Rider said it will never be null
|
|
||||||
NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
|
|
||||||
{
|
|
||||||
var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
|
|
||||||
mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
|
|
||||||
|
|
||||||
mediaSource.TranscodeReasons = info.TranscodeReasons;
|
|
||||||
|
|
||||||
foreach (var profile in profiles)
|
|
||||||
{
|
|
||||||
foreach (var stream in mediaSource.MediaStreams)
|
|
||||||
{
|
|
||||||
if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
|
|
||||||
{
|
|
||||||
stream.DeliveryMethod = profile.DeliveryMethod;
|
|
||||||
|
|
||||||
if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
|
|
||||||
{
|
|
||||||
stream.DeliveryUrl = profile.Url.TrimStart('-');
|
|
||||||
stream.IsExternalUrl = profile.IsExternalUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
|
|
||||||
{
|
|
||||||
var maxBitrate = clientMaxBitrate;
|
|
||||||
var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
|
|
||||||
|
|
||||||
if (remoteClientMaxBitrate <= 0)
|
|
||||||
{
|
|
||||||
remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteClientMaxBitrate > 0)
|
|
||||||
{
|
|
||||||
var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
|
|
||||||
|
|
||||||
_logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
|
|
||||||
if (!isInLocalNetwork)
|
|
||||||
{
|
|
||||||
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxBitrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
|
|
||||||
{
|
|
||||||
var originalList = result.MediaSources.ToList();
|
|
||||||
|
|
||||||
result.MediaSources = result.MediaSources.OrderBy(i =>
|
|
||||||
{
|
|
||||||
// Nothing beats direct playing a file
|
|
||||||
if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.ThenBy(i =>
|
|
||||||
{
|
|
||||||
// Let's assume direct streaming a file is just as desirable as direct playing a remote url
|
|
||||||
if (i.SupportsDirectPlay || i.SupportsDirectStream)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.ThenBy(i =>
|
|
||||||
{
|
|
||||||
return i.Protocol switch
|
|
||||||
{
|
|
||||||
MediaProtocol.File => 0,
|
|
||||||
_ => 1,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ThenBy(i =>
|
|
||||||
{
|
|
||||||
if (maxBitrate.HasValue && i.Bitrate.HasValue)
|
|
||||||
{
|
|
||||||
return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.ThenBy(originalList.IndexOf)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="itemId">Item id.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <response code="200">Item marked as unplayed.</response>
|
/// <response code="200">Item marked as unplayed.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
|
||||||
[HttpDelete("Users/{userId}/PlayedItem/{itemId}")]
|
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
|
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
|
||||||
{
|
{
|
||||||
|
|
|
@ -413,7 +413,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult PostFullCapabilities(
|
public ActionResult PostFullCapabilities(
|
||||||
[FromQuery, Required] string? id,
|
[FromQuery] string? id,
|
||||||
[FromBody, Required] ClientCapabilities capabilities)
|
[FromBody, Required] ClientCapabilities capabilities)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
@ -480,7 +480,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Password reset providers retrieved.</response>
|
/// <response code="200">Password reset providers retrieved.</response>
|
||||||
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
|
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
|
||||||
[HttpGet("Auto/PasswordResetProviders")]
|
[HttpGet("Auth/PasswordResetProviders")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
|
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
|
||||||
|
|
|
@ -2,17 +2,20 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.Models.VideoDtos;
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
|
using MediaBrowser.Controller.Devices;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
|
@ -23,27 +26,39 @@ namespace Jellyfin.Api.Controllers
|
||||||
public class UniversalAudioController : BaseJellyfinApiController
|
public class UniversalAudioController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IAuthorizationContext _authorizationContext;
|
private readonly IAuthorizationContext _authorizationContext;
|
||||||
private readonly MediaInfoController _mediaInfoController;
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly DynamicHlsController _dynamicHlsController;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly AudioController _audioController;
|
private readonly ILogger<UniversalAudioController> _logger;
|
||||||
|
private readonly MediaInfoHelper _mediaInfoHelper;
|
||||||
|
private readonly AudioHelper _audioHelper;
|
||||||
|
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
/// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
|
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||||
/// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
/// <param name="audioController">Instance of the <see cref="AudioController"/>.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
|
||||||
|
/// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
|
||||||
|
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
||||||
|
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
|
||||||
public UniversalAudioController(
|
public UniversalAudioController(
|
||||||
IAuthorizationContext authorizationContext,
|
IAuthorizationContext authorizationContext,
|
||||||
MediaInfoController mediaInfoController,
|
IDeviceManager deviceManager,
|
||||||
DynamicHlsController dynamicHlsController,
|
ILibraryManager libraryManager,
|
||||||
AudioController audioController)
|
ILogger<UniversalAudioController> logger,
|
||||||
|
MediaInfoHelper mediaInfoHelper,
|
||||||
|
AudioHelper audioHelper,
|
||||||
|
DynamicHlsHelper dynamicHlsHelper)
|
||||||
{
|
{
|
||||||
_authorizationContext = authorizationContext;
|
_authorizationContext = authorizationContext;
|
||||||
_mediaInfoController = mediaInfoController;
|
_deviceManager = deviceManager;
|
||||||
_dynamicHlsController = dynamicHlsController;
|
_libraryManager = libraryManager;
|
||||||
_audioController = audioController;
|
_logger = logger;
|
||||||
|
_mediaInfoHelper = mediaInfoHelper;
|
||||||
|
_audioHelper = audioHelper;
|
||||||
|
_dynamicHlsHelper = dynamicHlsHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -95,24 +110,68 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] bool breakOnNonKeyFrames,
|
[FromQuery] bool breakOnNonKeyFrames,
|
||||||
[FromQuery] bool enableRedirection = true)
|
[FromQuery] bool enableRedirection = true)
|
||||||
{
|
{
|
||||||
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
|
||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||||
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
|
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
|
||||||
|
|
||||||
var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
|
var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
|
||||||
itemId,
|
|
||||||
userId,
|
|
||||||
maxStreamingBitrate,
|
|
||||||
startTimeTicks,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
maxAudioChannels,
|
|
||||||
mediaSourceId,
|
|
||||||
null,
|
|
||||||
new DeviceProfileDto { DeviceProfile = deviceProfile })
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
var mediaSource = playbackInfoResult.Value.MediaSources[0];
|
|
||||||
|
|
||||||
|
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
|
||||||
|
|
||||||
|
if (deviceProfile == null)
|
||||||
|
{
|
||||||
|
var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
||||||
|
if (clientCapabilities != null)
|
||||||
|
{
|
||||||
|
deviceProfile = clientCapabilities.DeviceProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = await _mediaInfoHelper.GetPlaybackInfo(
|
||||||
|
itemId,
|
||||||
|
userId,
|
||||||
|
mediaSourceId)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (deviceProfile != null)
|
||||||
|
{
|
||||||
|
// set device specific data
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
|
||||||
|
foreach (var sourceInfo in info.MediaSources)
|
||||||
|
{
|
||||||
|
_mediaInfoHelper.SetDeviceSpecificData(
|
||||||
|
item,
|
||||||
|
sourceInfo,
|
||||||
|
deviceProfile,
|
||||||
|
authInfo,
|
||||||
|
maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
|
||||||
|
startTimeTicks ?? 0,
|
||||||
|
mediaSourceId ?? string.Empty,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
maxAudioChannels,
|
||||||
|
info!.PlaySessionId!,
|
||||||
|
userId ?? Guid.Empty,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
Request.HttpContext.Connection.RemoteIpAddress.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.MediaSources != null)
|
||||||
|
{
|
||||||
|
foreach (var source in info.MediaSources)
|
||||||
|
{
|
||||||
|
_mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaSource = info.MediaSources![0];
|
||||||
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
|
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
|
||||||
{
|
{
|
||||||
if (enableRedirection)
|
if (enableRedirection)
|
||||||
|
@ -127,129 +186,71 @@ namespace Jellyfin.Api.Controllers
|
||||||
var isStatic = mediaSource.SupportsDirectStream;
|
var isStatic = mediaSource.SupportsDirectStream;
|
||||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
|
|
||||||
|
|
||||||
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
||||||
// TODO: remove this when we switch back to the segment muxer
|
// TODO: remove this when we switch back to the segment muxer
|
||||||
var supportedHlsContainers = new[] { "mpegts", "fmp4" };
|
var supportedHlsContainers = new[] { "mpegts", "fmp4" };
|
||||||
|
|
||||||
if (isHeadRequest)
|
var dynamicHlsRequestDto = new HlsAudioRequestDto
|
||||||
{
|
{
|
||||||
_dynamicHlsController.Request.Method = HttpMethod.Head.Method;
|
Id = itemId,
|
||||||
}
|
Container = ".m3u8",
|
||||||
|
Static = isStatic,
|
||||||
return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
|
PlaySessionId = info.PlaySessionId,
|
||||||
itemId,
|
|
||||||
".m3u8",
|
|
||||||
isStatic,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
playbackInfoResult.Value.PlaySessionId,
|
|
||||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
// fallback to mpegts if device reports some weird value unsupported by hls
|
||||||
Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
|
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
|
||||||
null,
|
MediaSourceId = mediaSourceId,
|
||||||
null,
|
DeviceId = deviceId,
|
||||||
mediaSource.Id,
|
AudioCodec = audioCodec,
|
||||||
deviceId,
|
EnableAutoStreamCopy = true,
|
||||||
transcodingProfile.AudioCodec,
|
AllowAudioStreamCopy = true,
|
||||||
null,
|
AllowVideoStreamCopy = true,
|
||||||
null,
|
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
||||||
null,
|
AudioSampleRate = maxAudioSampleRate,
|
||||||
transcodingProfile.BreakOnNonKeyFrames,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
maxAudioSampleRate,
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
maxAudioBitDepth,
|
AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||||
null,
|
StartTimeTicks = startTimeTicks,
|
||||||
isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||||
maxAudioChannels,
|
RequireAvc = true,
|
||||||
null,
|
DeInterlace = true,
|
||||||
null,
|
RequireNonAnamorphic = true,
|
||||||
null,
|
EnableMpegtsM2TsMode = true,
|
||||||
null,
|
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||||
null,
|
Context = EncodingContext.Static,
|
||||||
startTimeTicks,
|
StreamOptions = new Dictionary<string, string>(),
|
||||||
null,
|
EnableAdaptiveBitrateStreaming = true
|
||||||
null,
|
};
|
||||||
null,
|
|
||||||
null,
|
|
||||||
SubtitleDeliveryMethod.Hls,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
EncodingContext.Static,
|
|
||||||
new Dictionary<string, string>())
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
_audioController.Request.Method = HttpMethod.Head.Method;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _audioController.GetAudioStream(
|
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
|
||||||
itemId,
|
|
||||||
isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
|
||||||
isStatic,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
playbackInfoResult.Value.PlaySessionId,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
mediaSource.Id,
|
|
||||||
deviceId,
|
|
||||||
audioCodec,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
breakOnNonKeyFrames,
|
|
||||||
maxAudioSampleRate,
|
|
||||||
maxAudioBitDepth,
|
|
||||||
isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
|
||||||
null,
|
|
||||||
maxAudioChannels,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
startTimeTicks,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
SubtitleDeliveryMethod.Embed,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var audioStreamingDto = new StreamingRequestDto
|
||||||
|
{
|
||||||
|
Id = itemId,
|
||||||
|
Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
||||||
|
Static = isStatic,
|
||||||
|
PlaySessionId = info.PlaySessionId,
|
||||||
|
MediaSourceId = mediaSourceId,
|
||||||
|
DeviceId = deviceId,
|
||||||
|
AudioCodec = audioCodec,
|
||||||
|
EnableAutoStreamCopy = true,
|
||||||
|
AllowAudioStreamCopy = true,
|
||||||
|
AllowVideoStreamCopy = true,
|
||||||
|
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
||||||
|
AudioSampleRate = maxAudioSampleRate,
|
||||||
|
MaxAudioChannels = maxAudioChannels,
|
||||||
|
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||||
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
|
AudioChannels = maxAudioChannels,
|
||||||
|
CopyTimestamps = true,
|
||||||
|
StartTimeTicks = startTimeTicks,
|
||||||
|
SubtitleMethod = SubtitleDeliveryMethod.Embed,
|
||||||
|
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||||
|
Context = EncodingContext.Static
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeviceProfile GetDeviceProfile(
|
private DeviceProfile GetDeviceProfile(
|
||||||
|
|
|
@ -471,7 +471,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||||
|
|
||||||
var httpClient = _httpClientFactory.CreateClient();
|
var httpClient = _httpClientFactory.CreateClient();
|
||||||
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
|
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
|
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
|
||||||
|
@ -507,7 +507,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
state.MediaPath,
|
state.MediaPath,
|
||||||
contentType,
|
contentType,
|
||||||
isHeadRequest,
|
isHeadRequest,
|
||||||
this);
|
HttpContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to start ffmpeg (because media can't be returned directly)
|
// Need to start ffmpeg (because media can't be returned directly)
|
||||||
|
@ -517,10 +517,9 @@ namespace Jellyfin.Api.Controllers
|
||||||
return await FileStreamResponseHelpers.GetTranscodedFile(
|
return await FileStreamResponseHelpers.GetTranscodedFile(
|
||||||
state,
|
state,
|
||||||
isHeadRequest,
|
isHeadRequest,
|
||||||
this,
|
HttpContext,
|
||||||
_transcodingJobHelper,
|
_transcodingJobHelper,
|
||||||
ffmpegCommandLineArguments,
|
ffmpegCommandLineArguments,
|
||||||
Request,
|
|
||||||
_transcodingJobType,
|
_transcodingJobType,
|
||||||
cancellationTokenSource).ConfigureAwait(false);
|
cancellationTokenSource).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
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.IO;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using MediaBrowser.Model.Net;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Audio helper.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioHelper
|
||||||
|
{
|
||||||
|
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 IFileSystem _fileSystem;
|
||||||
|
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AudioHelper"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
|
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
|
/// <param name="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">Instance of <see cref="TranscodingJobHelper"/>.</param>
|
||||||
|
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public AudioHelper(
|
||||||
|
IDlnaManager dlnaManager,
|
||||||
|
IAuthorizationContext authContext,
|
||||||
|
IUserManager userManager,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ISubtitleEncoder subtitleEncoder,
|
||||||
|
IConfiguration configuration,
|
||||||
|
IDeviceManager deviceManager,
|
||||||
|
TranscodingJobHelper transcodingJobHelper,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_dlnaManager = dlnaManager;
|
||||||
|
_authContext = authContext;
|
||||||
|
_userManager = userManager;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_subtitleEncoder = subtitleEncoder;
|
||||||
|
_configuration = configuration;
|
||||||
|
_deviceManager = deviceManager;
|
||||||
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get audio stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transcodingJobType">Transcoding job type.</param>
|
||||||
|
/// <param name="streamingRequest">Streaming controller.Request dto.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
|
||||||
|
public async Task<ActionResult> GetAudioStream(
|
||||||
|
TranscodingJobType transcodingJobType,
|
||||||
|
StreamingRequestDto streamingRequest)
|
||||||
|
{
|
||||||
|
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||||
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
using var state = await StreamingHelpers.GetStreamingState(
|
||||||
|
streamingRequest,
|
||||||
|
_httpContextAccessor.HttpContext.Request,
|
||||||
|
_authContext,
|
||||||
|
_mediaSourceManager,
|
||||||
|
_userManager,
|
||||||
|
_libraryManager,
|
||||||
|
_serverConfigurationManager,
|
||||||
|
_mediaEncoder,
|
||||||
|
_fileSystem,
|
||||||
|
_subtitleEncoder,
|
||||||
|
_configuration,
|
||||||
|
_dlnaManager,
|
||||||
|
_deviceManager,
|
||||||
|
_transcodingJobHelper,
|
||||||
|
transcodingJobType,
|
||||||
|
cancellationTokenSource.Token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (streamingRequest.Static && state.DirectStreamProvider != null)
|
||||||
|
{
|
||||||
|
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
|
||||||
|
|
||||||
|
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
|
||||||
|
{
|
||||||
|
AllowEndOfFile = false
|
||||||
|
}.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
||||||
|
return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static remote stream
|
||||||
|
if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
|
||||||
|
{
|
||||||
|
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
|
||||||
|
|
||||||
|
var httpClient = _httpClientFactory.CreateClient();
|
||||||
|
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
|
||||||
|
{
|
||||||
|
return new BadRequestObjectResult($"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, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
|
||||||
|
|
||||||
|
// Static stream
|
||||||
|
if (streamingRequest.Static)
|
||||||
|
{
|
||||||
|
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
|
||||||
|
|
||||||
|
if (state.MediaSource.IsInfiniteStream)
|
||||||
|
{
|
||||||
|
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
|
||||||
|
{
|
||||||
|
AllowEndOfFile = false
|
||||||
|
}.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileStreamResponseHelpers.GetStaticFileResult(
|
||||||
|
state.MediaPath,
|
||||||
|
contentType,
|
||||||
|
isHeadRequest,
|
||||||
|
_httpContextAccessor.HttpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
_httpContextAccessor.HttpContext,
|
||||||
|
_transcodingJobHelper,
|
||||||
|
ffmpegCommandLineArguments,
|
||||||
|
transcodingJobType,
|
||||||
|
cancellationTokenSource).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,550 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
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.Entities;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.Net;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dynamic hls helper.
|
||||||
|
/// </summary>
|
||||||
|
public class DynamicHlsHelper
|
||||||
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IDlnaManager _dlnaManager;
|
||||||
|
private readonly IAuthorizationContext _authContext;
|
||||||
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly ILogger<DynamicHlsHelper> _logger;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
|
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> 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="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">Instance of <see cref="TranscodingJobHelper"/>.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public DynamicHlsHelper(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IDlnaManager dlnaManager,
|
||||||
|
IAuthorizationContext authContext,
|
||||||
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ISubtitleEncoder subtitleEncoder,
|
||||||
|
IConfiguration configuration,
|
||||||
|
IDeviceManager deviceManager,
|
||||||
|
TranscodingJobHelper transcodingJobHelper,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
ILogger<DynamicHlsHelper> logger,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_dlnaManager = dlnaManager;
|
||||||
|
_authContext = authContext;
|
||||||
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_subtitleEncoder = subtitleEncoder;
|
||||||
|
_configuration = configuration;
|
||||||
|
_deviceManager = deviceManager;
|
||||||
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_logger = logger;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get master hls playlist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transcodingJobType">Transcoding job type.</param>
|
||||||
|
/// <param name="streamingRequest">Streaming request dto.</param>
|
||||||
|
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
|
||||||
|
public async Task<ActionResult> GetMasterHlsPlaylist(
|
||||||
|
TranscodingJobType transcodingJobType,
|
||||||
|
StreamingRequestDto streamingRequest,
|
||||||
|
bool enableAdaptiveBitrateStreaming)
|
||||||
|
{
|
||||||
|
var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head;
|
||||||
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
return await GetMasterPlaylistInternal(
|
||||||
|
streamingRequest,
|
||||||
|
isHeadRequest,
|
||||||
|
enableAdaptiveBitrateStreaming,
|
||||||
|
transcodingJobType,
|
||||||
|
cancellationTokenSource).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionResult> GetMasterPlaylistInternal(
|
||||||
|
StreamingRequestDto streamingRequest,
|
||||||
|
bool isHeadRequest,
|
||||||
|
bool enableAdaptiveBitrateStreaming,
|
||||||
|
TranscodingJobType transcodingJobType,
|
||||||
|
CancellationTokenSource cancellationTokenSource)
|
||||||
|
{
|
||||||
|
using var state = await StreamingHelpers.GetStreamingState(
|
||||||
|
streamingRequest,
|
||||||
|
_httpContextAccessor.HttpContext.Request,
|
||||||
|
_authContext,
|
||||||
|
_mediaSourceManager,
|
||||||
|
_userManager,
|
||||||
|
_libraryManager,
|
||||||
|
_serverConfigurationManager,
|
||||||
|
_mediaEncoder,
|
||||||
|
_fileSystem,
|
||||||
|
_subtitleEncoder,
|
||||||
|
_configuration,
|
||||||
|
_dlnaManager,
|
||||||
|
_deviceManager,
|
||||||
|
_transcodingJobHelper,
|
||||||
|
transcodingJobType,
|
||||||
|
cancellationTokenSource.Token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
|
||||||
|
if (isHeadRequest)
|
||||||
|
{
|
||||||
|
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
builder.AppendLine("#EXTM3U");
|
||||||
|
|
||||||
|
var isLiveStream = state.IsSegmentedLiveStream;
|
||||||
|
|
||||||
|
var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
|
||||||
|
|
||||||
|
// from universal audio service
|
||||||
|
if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
|
||||||
|
{
|
||||||
|
queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// from universal audio service
|
||||||
|
if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
|
||||||
|
{
|
||||||
|
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main stream
|
||||||
|
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
||||||
|
|
||||||
|
playlistUrl += queryString;
|
||||||
|
|
||||||
|
var subtitleStreams = state.MediaSource
|
||||||
|
.MediaStreams
|
||||||
|
.Where(i => i.IsTextSubtitleStream)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
|
||||||
|
? "subs"
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// If we're burning in subtitles then don't add additional subs to the manifest
|
||||||
|
if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
||||||
|
{
|
||||||
|
subtitleGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||||
|
{
|
||||||
|
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||||
|
|
||||||
|
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress))
|
||||||
|
{
|
||||||
|
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
|
||||||
|
|
||||||
|
// By default, vary by just 200k
|
||||||
|
var variation = GetBitrateVariation(totalBitrate);
|
||||||
|
|
||||||
|
var newBitrate = totalBitrate - variation;
|
||||||
|
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||||
|
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||||
|
|
||||||
|
variation *= 2;
|
||||||
|
newBitrate = totalBitrate - variation;
|
||||||
|
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||||
|
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
|
||||||
|
{
|
||||||
|
builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
|
||||||
|
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
|
||||||
|
.Append(",AVERAGE-BANDWIDTH=")
|
||||||
|
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
AppendPlaylistCodecsField(builder, state);
|
||||||
|
|
||||||
|
AppendPlaylistResolutionField(builder, state);
|
||||||
|
|
||||||
|
AppendPlaylistFramerateField(builder, state);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||||
|
{
|
||||||
|
builder.Append(",SUBTITLES=\"")
|
||||||
|
.Append(subtitleGroup)
|
||||||
|
.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(Environment.NewLine);
|
||||||
|
builder.AppendLine(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a CODECS field containing formatted strings of
|
||||||
|
/// the active streams output video and audio codecs.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
||||||
|
/// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
|
||||||
|
/// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
|
||||||
|
/// <param name="builder">StringBuilder to append the field to.</param>
|
||||||
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
|
private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
|
||||||
|
{
|
||||||
|
// Video
|
||||||
|
string videoCodecs = string.Empty;
|
||||||
|
int? videoCodecLevel = GetOutputVideoCodecLevel(state);
|
||||||
|
if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
|
||||||
|
{
|
||||||
|
videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
string audioCodecs = string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
|
||||||
|
{
|
||||||
|
audioCodecs = GetPlaylistAudioCodecs(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder codecs = new StringBuilder();
|
||||||
|
|
||||||
|
codecs.Append(videoCodecs);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
|
||||||
|
{
|
||||||
|
codecs.Append(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
codecs.Append(audioCodecs);
|
||||||
|
|
||||||
|
if (codecs.Length > 1)
|
||||||
|
{
|
||||||
|
builder.Append(",CODECS=\"")
|
||||||
|
.Append(codecs)
|
||||||
|
.Append('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a RESOLUTION field containing the resolution of the output stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
||||||
|
/// <param name="builder">StringBuilder to append the field to.</param>
|
||||||
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
|
private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
|
||||||
|
{
|
||||||
|
if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
|
||||||
|
{
|
||||||
|
builder.Append(",RESOLUTION=")
|
||||||
|
.Append(state.OutputWidth.GetValueOrDefault())
|
||||||
|
.Append('x')
|
||||||
|
.Append(state.OutputHeight.GetValueOrDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a FRAME-RATE field containing the framerate of the output stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
||||||
|
/// <param name="builder">StringBuilder to append the field to.</param>
|
||||||
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
|
private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
|
||||||
|
{
|
||||||
|
double? framerate = null;
|
||||||
|
if (state.TargetFramerate.HasValue)
|
||||||
|
{
|
||||||
|
framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
|
||||||
|
}
|
||||||
|
else if (state.VideoStream?.RealFrameRate != null)
|
||||||
|
{
|
||||||
|
framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (framerate.HasValue)
|
||||||
|
{
|
||||||
|
builder.Append(",FRAME-RATE=")
|
||||||
|
.Append(framerate.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
|
||||||
|
{
|
||||||
|
// Within the local network this will likely do more harm than good.
|
||||||
|
var ip = RequestHelpers.NormalizeIp(ipAddress).ToString();
|
||||||
|
if (_networkManager.IsInLocalNetwork(ip))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableAdaptiveBitrateStreaming)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
|
||||||
|
{
|
||||||
|
// Opening live streams is so slow it's not even worth it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.IsOutputVideo)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Having problems in android
|
||||||
|
return false;
|
||||||
|
// return state.VideoRequest.VideoBitRate.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
|
||||||
|
const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
|
||||||
|
|
||||||
|
foreach (var stream in subtitles)
|
||||||
|
{
|
||||||
|
var name = stream.DisplayTitle;
|
||||||
|
|
||||||
|
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
|
||||||
|
var isForced = stream.IsForced;
|
||||||
|
|
||||||
|
var url = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
|
||||||
|
state.Request.MediaSourceId,
|
||||||
|
stream.Index.ToString(CultureInfo.InvariantCulture),
|
||||||
|
30.ToString(CultureInfo.InvariantCulture),
|
||||||
|
ClaimHelpers.GetToken(user));
|
||||||
|
|
||||||
|
var line = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
Format,
|
||||||
|
name,
|
||||||
|
isDefault ? "YES" : "NO",
|
||||||
|
isForced ? "YES" : "NO",
|
||||||
|
url,
|
||||||
|
stream.Language ?? "Unknown");
|
||||||
|
|
||||||
|
builder.AppendLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the H.26X level of the output video stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
|
/// <returns>H.26X level of the output video stream.</returns>
|
||||||
|
private int? GetOutputVideoCodecLevel(StreamState state)
|
||||||
|
{
|
||||||
|
string? levelString;
|
||||||
|
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& state.VideoStream.Level.HasValue)
|
||||||
|
{
|
||||||
|
levelString = state.VideoStream?.Level.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
|
||||||
|
{
|
||||||
|
return parsedLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
|
||||||
|
/// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
|
||||||
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
|
/// <returns>Formatted audio codec string.</returns>
|
||||||
|
private string GetPlaylistAudioCodecs(StreamState state)
|
||||||
|
{
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
|
||||||
|
return HlsCodecStringHelpers.GetAACString(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return HlsCodecStringHelpers.GetMP3String();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return HlsCodecStringHelpers.GetAC3String();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return HlsCodecStringHelpers.GetEAC3String();
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a formatted string of the output video codec, for use in the CODECS field.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
|
||||||
|
/// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
|
||||||
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
|
/// <param name="codec">Video codec.</param>
|
||||||
|
/// <param name="level">Video level.</param>
|
||||||
|
/// <returns>Formatted video codec string.</returns>
|
||||||
|
private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
|
||||||
|
{
|
||||||
|
if (level == 0)
|
||||||
|
{
|
||||||
|
// This is 0 when there's no requested H.26X level in the device profile
|
||||||
|
// and the source is not encoded in H.26X
|
||||||
|
_logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
|
||||||
|
return HlsCodecStringHelpers.GetH264String(profile, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
|
||||||
|
|
||||||
|
return HlsCodecStringHelpers.GetH265String(profile, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetBitrateVariation(int bitrate)
|
||||||
|
{
|
||||||
|
// By default, vary by just 50k
|
||||||
|
var variation = 50000;
|
||||||
|
|
||||||
|
if (bitrate >= 10000000)
|
||||||
|
{
|
||||||
|
variation = 2000000;
|
||||||
|
}
|
||||||
|
else if (bitrate >= 5000000)
|
||||||
|
{
|
||||||
|
variation = 1500000;
|
||||||
|
}
|
||||||
|
else if (bitrate >= 3000000)
|
||||||
|
{
|
||||||
|
variation = 1000000;
|
||||||
|
}
|
||||||
|
else if (bitrate >= 2000000)
|
||||||
|
{
|
||||||
|
variation = 500000;
|
||||||
|
}
|
||||||
|
else if (bitrate >= 1000000)
|
||||||
|
{
|
||||||
|
variation = 300000;
|
||||||
|
}
|
||||||
|
else if (bitrate >= 600000)
|
||||||
|
{
|
||||||
|
variation = 200000;
|
||||||
|
}
|
||||||
|
else if (bitrate >= 400000)
|
||||||
|
{
|
||||||
|
variation = 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return variation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceBitrate(string url, int oldValue, int newValue)
|
||||||
|
{
|
||||||
|
return url.Replace(
|
||||||
|
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
|
||||||
|
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,14 +22,14 @@ namespace Jellyfin.Api.Helpers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
|
||||||
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
|
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
|
||||||
|
/// <param name="httpContext">The current http context.</param>
|
||||||
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
|
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
|
||||||
public static async Task<ActionResult> GetStaticRemoteStreamResult(
|
public static async Task<ActionResult> GetStaticRemoteStreamResult(
|
||||||
StreamState state,
|
StreamState state,
|
||||||
bool isHeadRequest,
|
bool isHeadRequest,
|
||||||
ControllerBase controller,
|
HttpClient httpClient,
|
||||||
HttpClient httpClient)
|
HttpContext httpContext)
|
||||||
{
|
{
|
||||||
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
||||||
{
|
{
|
||||||
|
@ -40,14 +40,14 @@ namespace Jellyfin.Api.Helpers
|
||||||
var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
|
var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
|
||||||
var contentType = response.Content.Headers.ContentType.ToString();
|
var contentType = response.Content.Headers.ContentType.ToString();
|
||||||
|
|
||||||
controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
return controller.File(Array.Empty<byte>(), contentType);
|
return new FileContentResult(Array.Empty<byte>(), contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
|
return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -56,23 +56,23 @@ namespace Jellyfin.Api.Helpers
|
||||||
/// <param name="path">The path to the file.</param>
|
/// <param name="path">The path to the file.</param>
|
||||||
/// <param name="contentType">The content type of the file.</param>
|
/// <param name="contentType">The content type of the file.</param>
|
||||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
/// <param name="httpContext">The current http context.</param>
|
||||||
/// <returns>An <see cref="ActionResult"/> the file.</returns>
|
/// <returns>An <see cref="ActionResult"/> the file.</returns>
|
||||||
public static ActionResult GetStaticFileResult(
|
public static ActionResult GetStaticFileResult(
|
||||||
string path,
|
string path,
|
||||||
string contentType,
|
string contentType,
|
||||||
bool isHeadRequest,
|
bool isHeadRequest,
|
||||||
ControllerBase controller)
|
HttpContext httpContext)
|
||||||
{
|
{
|
||||||
controller.Response.ContentType = contentType;
|
httpContext.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 the request is a head request, return a NoContent result with the same headers as it would with a GET request
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
return controller.NoContent();
|
return new NoContentResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
return controller.PhysicalFile(path, contentType);
|
return new PhysicalFileResult(path, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -80,34 +80,32 @@ namespace Jellyfin.Api.Helpers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
/// <param name="httpContext">The current http context.</param>
|
||||||
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
|
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
|
||||||
/// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</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="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
|
||||||
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
|
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
|
||||||
/// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
|
/// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
|
||||||
public static async Task<ActionResult> GetTranscodedFile(
|
public static async Task<ActionResult> GetTranscodedFile(
|
||||||
StreamState state,
|
StreamState state,
|
||||||
bool isHeadRequest,
|
bool isHeadRequest,
|
||||||
ControllerBase controller,
|
HttpContext httpContext,
|
||||||
TranscodingJobHelper transcodingJobHelper,
|
TranscodingJobHelper transcodingJobHelper,
|
||||||
string ffmpegCommandLineArguments,
|
string ffmpegCommandLineArguments,
|
||||||
HttpRequest request,
|
|
||||||
TranscodingJobType transcodingJobType,
|
TranscodingJobType transcodingJobType,
|
||||||
CancellationTokenSource cancellationTokenSource)
|
CancellationTokenSource cancellationTokenSource)
|
||||||
{
|
{
|
||||||
// Use the command line args with a dummy playlist path
|
// Use the command line args with a dummy playlist path
|
||||||
var outputPath = state.OutputFilePath;
|
var outputPath = state.OutputFilePath;
|
||||||
|
|
||||||
controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||||
|
|
||||||
var contentType = state.GetMimeType(outputPath);
|
var contentType = state.GetMimeType(outputPath);
|
||||||
|
|
||||||
// Headers only
|
// Headers only
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
return controller.File(Array.Empty<byte>(), contentType);
|
return new FileContentResult(Array.Empty<byte>(), contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
||||||
|
@ -117,7 +115,7 @@ namespace Jellyfin.Api.Helpers
|
||||||
TranscodingJobDto? job;
|
TranscodingJobDto? job;
|
||||||
if (!File.Exists(outputPath))
|
if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
|
job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -128,7 +126,7 @@ namespace Jellyfin.Api.Helpers
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
|
await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
|
||||||
memoryStream.Position = 0;
|
memoryStream.Position = 0;
|
||||||
return controller.File(memoryStream, contentType);
|
return new FileStreamResult(memoryStream, contentType);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,573 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Devices;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
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.MediaInfo;
|
||||||
|
using MediaBrowser.Model.Session;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Media info helper.
|
||||||
|
/// </summary>
|
||||||
|
public class MediaInfoHelper
|
||||||
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly ILogger<MediaInfoHelper> _logger;
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
private readonly IAuthorizationContext _authContext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||||
|
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
|
public MediaInfoHelper(
|
||||||
|
IUserManager userManager,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
|
ILogger<MediaInfoHelper> logger,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IDeviceManager deviceManager,
|
||||||
|
IAuthorizationContext authContext)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_logger = logger;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_deviceManager = deviceManager;
|
||||||
|
_authContext = authContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get playback info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Item id.</param>
|
||||||
|
/// <param name="userId">User Id.</param>
|
||||||
|
/// <param name="mediaSourceId">Media source id.</param>
|
||||||
|
/// <param name="liveStreamId">Live stream id.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
|
||||||
|
public async Task<PlaybackInfoResponse> GetPlaybackInfo(
|
||||||
|
Guid id,
|
||||||
|
Guid? userId,
|
||||||
|
string? mediaSourceId = null,
|
||||||
|
string? liveStreamId = null)
|
||||||
|
{
|
||||||
|
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||||
|
? _userManager.GetUserById(userId.Value)
|
||||||
|
: null;
|
||||||
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
var result = new PlaybackInfoResponse();
|
||||||
|
|
||||||
|
MediaSourceInfo[] mediaSources;
|
||||||
|
if (string.IsNullOrWhiteSpace(liveStreamId))
|
||||||
|
{
|
||||||
|
// TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
|
||||||
|
var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mediaSourceId))
|
||||||
|
{
|
||||||
|
mediaSources = mediaSourcesList.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mediaSources = mediaSourcesList
|
||||||
|
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
mediaSources = new[] { mediaSource };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaSources.Length == 0)
|
||||||
|
{
|
||||||
|
result.MediaSources = Array.Empty<MediaSourceInfo>();
|
||||||
|
|
||||||
|
result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
|
||||||
|
// Should we move this directly into MediaSourceManager?
|
||||||
|
result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
|
||||||
|
|
||||||
|
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SetDeviceSpecificData.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">Item to set data for.</param>
|
||||||
|
/// <param name="mediaSource">Media source info.</param>
|
||||||
|
/// <param name="profile">Device profile.</param>
|
||||||
|
/// <param name="auth">Authorization info.</param>
|
||||||
|
/// <param name="maxBitrate">Max bitrate.</param>
|
||||||
|
/// <param name="startTimeTicks">Start time ticks.</param>
|
||||||
|
/// <param name="mediaSourceId">Media source id.</param>
|
||||||
|
/// <param name="audioStreamIndex">Audio stream index.</param>
|
||||||
|
/// <param name="subtitleStreamIndex">Subtitle stream index.</param>
|
||||||
|
/// <param name="maxAudioChannels">Max audio channels.</param>
|
||||||
|
/// <param name="playSessionId">Play session id.</param>
|
||||||
|
/// <param name="userId">User id.</param>
|
||||||
|
/// <param name="enableDirectPlay">Enable direct play.</param>
|
||||||
|
/// <param name="enableDirectStream">Enable direct stream.</param>
|
||||||
|
/// <param name="enableTranscoding">Enable transcoding.</param>
|
||||||
|
/// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
|
||||||
|
/// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
|
||||||
|
/// <param name="ipAddress">Requesting IP address.</param>
|
||||||
|
public void SetDeviceSpecificData(
|
||||||
|
BaseItem item,
|
||||||
|
MediaSourceInfo mediaSource,
|
||||||
|
DeviceProfile profile,
|
||||||
|
AuthorizationInfo auth,
|
||||||
|
long? maxBitrate,
|
||||||
|
long startTimeTicks,
|
||||||
|
string mediaSourceId,
|
||||||
|
int? audioStreamIndex,
|
||||||
|
int? subtitleStreamIndex,
|
||||||
|
int? maxAudioChannels,
|
||||||
|
string playSessionId,
|
||||||
|
Guid userId,
|
||||||
|
bool enableDirectPlay,
|
||||||
|
bool enableDirectStream,
|
||||||
|
bool enableTranscoding,
|
||||||
|
bool allowVideoStreamCopy,
|
||||||
|
bool allowAudioStreamCopy,
|
||||||
|
string ipAddress)
|
||||||
|
{
|
||||||
|
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
|
||||||
|
|
||||||
|
var options = new VideoOptions
|
||||||
|
{
|
||||||
|
MediaSources = new[] { mediaSource },
|
||||||
|
Context = EncodingContext.Streaming,
|
||||||
|
DeviceId = auth.DeviceId,
|
||||||
|
ItemId = item.Id,
|
||||||
|
Profile = profile,
|
||||||
|
MaxAudioChannels = maxAudioChannels
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
options.MediaSourceId = mediaSourceId;
|
||||||
|
options.AudioStreamIndex = audioStreamIndex;
|
||||||
|
options.SubtitleStreamIndex = subtitleStreamIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = _userManager.GetUserById(userId);
|
||||||
|
|
||||||
|
if (!enableDirectPlay)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectPlay = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableDirectStream)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectStream = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableTranscoding)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsTranscoding = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is Audio)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
|
||||||
|
user.Username,
|
||||||
|
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
|
||||||
|
user.Username,
|
||||||
|
user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
|
||||||
|
user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
|
||||||
|
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beginning of Playback Determination: Attempt DirectPlay first
|
||||||
|
if (mediaSource.SupportsDirectPlay)
|
||||||
|
{
|
||||||
|
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectPlay = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var supportsDirectStream = mediaSource.SupportsDirectStream;
|
||||||
|
|
||||||
|
// Dummy this up to fool StreamBuilder
|
||||||
|
mediaSource.SupportsDirectStream = true;
|
||||||
|
options.MaxBitrate = maxBitrate;
|
||||||
|
|
||||||
|
if (item is Audio)
|
||||||
|
{
|
||||||
|
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
|
||||||
|
{
|
||||||
|
options.ForceDirectPlay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item is Video)
|
||||||
|
{
|
||||||
|
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
||||||
|
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
||||||
|
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
||||||
|
{
|
||||||
|
options.ForceDirectPlay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||||
|
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? streamBuilder.BuildAudioItem(options)
|
||||||
|
: streamBuilder.BuildVideoItem(options);
|
||||||
|
|
||||||
|
if (streamInfo == null || !streamInfo.IsDirectStream)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectPlay = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this back to what it was
|
||||||
|
mediaSource.SupportsDirectStream = supportsDirectStream;
|
||||||
|
|
||||||
|
if (streamInfo != null)
|
||||||
|
{
|
||||||
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaSource.SupportsDirectStream)
|
||||||
|
{
|
||||||
|
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectStream = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
|
||||||
|
|
||||||
|
if (item is Audio)
|
||||||
|
{
|
||||||
|
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
|
||||||
|
{
|
||||||
|
options.ForceDirectStream = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item is Video)
|
||||||
|
{
|
||||||
|
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
||||||
|
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
||||||
|
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
||||||
|
{
|
||||||
|
options.ForceDirectStream = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||||
|
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? streamBuilder.BuildAudioItem(options)
|
||||||
|
: streamBuilder.BuildVideoItem(options);
|
||||||
|
|
||||||
|
if (streamInfo == null || !streamInfo.IsDirectStream)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectStream = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamInfo != null)
|
||||||
|
{
|
||||||
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaSource.SupportsTranscoding)
|
||||||
|
{
|
||||||
|
options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
|
||||||
|
|
||||||
|
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||||
|
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? streamBuilder.BuildAudioItem(options)
|
||||||
|
: streamBuilder.BuildVideoItem(options);
|
||||||
|
|
||||||
|
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
||||||
|
{
|
||||||
|
if (streamInfo != null)
|
||||||
|
{
|
||||||
|
streamInfo.PlaySessionId = playSessionId;
|
||||||
|
streamInfo.StartPositionTicks = startTimeTicks;
|
||||||
|
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
||||||
|
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
|
||||||
|
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
||||||
|
mediaSource.TranscodingContainer = streamInfo.Container;
|
||||||
|
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
||||||
|
|
||||||
|
// Do this after the above so that StartPositionTicks is set
|
||||||
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (streamInfo != null)
|
||||||
|
{
|
||||||
|
streamInfo.PlaySessionId = playSessionId;
|
||||||
|
|
||||||
|
if (streamInfo.PlayMethod == PlayMethod.Transcode)
|
||||||
|
{
|
||||||
|
streamInfo.StartPositionTicks = startTimeTicks;
|
||||||
|
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
||||||
|
|
||||||
|
if (!allowVideoStreamCopy)
|
||||||
|
{
|
||||||
|
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowAudioStreamCopy)
|
||||||
|
{
|
||||||
|
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSource.TranscodingContainer = streamInfo.Container;
|
||||||
|
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowAudioStreamCopy)
|
||||||
|
{
|
||||||
|
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSource.TranscodingContainer = streamInfo.Container;
|
||||||
|
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
||||||
|
|
||||||
|
// Do this after the above so that StartPositionTicks is set
|
||||||
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attachment in mediaSource.MediaAttachments)
|
||||||
|
{
|
||||||
|
attachment.DeliveryUrl = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"/Videos/{0}/{1}/Attachments/{2}",
|
||||||
|
item.Id,
|
||||||
|
mediaSource.Id,
|
||||||
|
attachment.Index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort media source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">Playback info response.</param>
|
||||||
|
/// <param name="maxBitrate">Max bitrate.</param>
|
||||||
|
public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
|
||||||
|
{
|
||||||
|
var originalList = result.MediaSources.ToList();
|
||||||
|
|
||||||
|
result.MediaSources = result.MediaSources.OrderBy(i =>
|
||||||
|
{
|
||||||
|
// Nothing beats direct playing a file
|
||||||
|
if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.ThenBy(i =>
|
||||||
|
{
|
||||||
|
// Let's assume direct streaming a file is just as desirable as direct playing a remote url
|
||||||
|
if (i.SupportsDirectPlay || i.SupportsDirectStream)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.ThenBy(i =>
|
||||||
|
{
|
||||||
|
return i.Protocol switch
|
||||||
|
{
|
||||||
|
MediaProtocol.File => 0,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ThenBy(i =>
|
||||||
|
{
|
||||||
|
if (maxBitrate.HasValue && i.Bitrate.HasValue)
|
||||||
|
{
|
||||||
|
return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.ThenBy(originalList.IndexOf)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open media source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpRequest">Http Request.</param>
|
||||||
|
/// <param name="request">Live stream request.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
|
||||||
|
public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
|
||||||
|
{
|
||||||
|
var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
|
||||||
|
|
||||||
|
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var profile = request.DeviceProfile;
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
||||||
|
if (clientCapabilities != null)
|
||||||
|
{
|
||||||
|
profile = clientCapabilities.DeviceProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile != null)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(request.ItemId);
|
||||||
|
|
||||||
|
SetDeviceSpecificData(
|
||||||
|
item,
|
||||||
|
result.MediaSource,
|
||||||
|
profile,
|
||||||
|
authInfo,
|
||||||
|
request.MaxStreamingBitrate,
|
||||||
|
request.StartTimeTicks ?? 0,
|
||||||
|
result.MediaSource.Id,
|
||||||
|
request.AudioStreamIndex,
|
||||||
|
request.SubtitleStreamIndex,
|
||||||
|
request.MaxAudioChannels,
|
||||||
|
request.PlaySessionId,
|
||||||
|
request.UserId,
|
||||||
|
request.EnableDirectPlay,
|
||||||
|
request.EnableDirectStream,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
httpRequest.HttpContext.Connection.RemoteIpAddress.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
|
||||||
|
{
|
||||||
|
result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// here was a check if (result.MediaSource != null) but Rider said it will never be null
|
||||||
|
NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalize media source container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediaSource">Media source.</param>
|
||||||
|
/// <param name="profile">Device profile.</param>
|
||||||
|
/// <param name="type">Dlna profile type.</param>
|
||||||
|
public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
|
||||||
|
{
|
||||||
|
mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
|
||||||
|
{
|
||||||
|
var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
|
||||||
|
mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
|
||||||
|
|
||||||
|
mediaSource.TranscodeReasons = info.TranscodeReasons;
|
||||||
|
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
{
|
||||||
|
foreach (var stream in mediaSource.MediaStreams)
|
||||||
|
{
|
||||||
|
if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
|
||||||
|
{
|
||||||
|
stream.DeliveryMethod = profile.DeliveryMethod;
|
||||||
|
|
||||||
|
if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
|
||||||
|
{
|
||||||
|
stream.DeliveryUrl = profile.Url.TrimStart('-');
|
||||||
|
stream.IsExternalUrl = profile.IsExternalUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
|
||||||
|
{
|
||||||
|
var maxBitrate = clientMaxBitrate;
|
||||||
|
var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
|
||||||
|
|
||||||
|
if (remoteClientMaxBitrate <= 0)
|
||||||
|
{
|
||||||
|
remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteClientMaxBitrate > 0)
|
||||||
|
{
|
||||||
|
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
|
||||||
|
|
||||||
|
_logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
|
||||||
|
if (!isInLocalNetwork)
|
||||||
|
{
|
||||||
|
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxBitrate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.TypeConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom datetime parser.
|
||||||
|
/// </summary>
|
||||||
|
public class DateTimeTypeConverter : TypeConverter
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
|
{
|
||||||
|
if (sourceType == typeof(string))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.CanConvertFrom(context, sourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
|
{
|
||||||
|
if (value is string dateString)
|
||||||
|
{
|
||||||
|
// Mark Played Item.
|
||||||
|
if (DateTime.TryParseExact(dateString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
|
||||||
|
{
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Activity Logs.
|
||||||
|
if (DateTime.TryParse(dateString, null, DateTimeStyles.RoundtripKind, out dateTime))
|
||||||
|
{
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.ConvertFrom(context, culture, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Net.Http;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Jellyfin.Api.TypeConverters;
|
||||||
using Jellyfin.Server.Extensions;
|
using Jellyfin.Server.Extensions;
|
||||||
using Jellyfin.Server.Middleware;
|
using Jellyfin.Server.Middleware;
|
||||||
using Jellyfin.Server.Models;
|
using Jellyfin.Server.Models;
|
||||||
|
@ -94,6 +96,9 @@ namespace Jellyfin.Server
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
|
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
|
||||||
|
|
||||||
|
// Add type descriptor for legacy datetime parsing.
|
||||||
|
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue