commit
634ee2d1e9
|
@ -632,6 +632,9 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
serviceCollection.AddScoped<MediaInfoHelper>();
|
||||
serviceCollection.AddScoped<AudioHelper>();
|
||||
serviceCollection.AddScoped<DynamicHlsHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,93 +1,32 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The audio controller.
|
||||
/// </summary>
|
||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
||||
// TODO: In order to authenticate this in the future, Dlna playback will require updating
|
||||
public class AudioController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly AudioHelper _audioHelper;
|
||||
|
||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="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)
|
||||
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
||||
public AudioController(AudioHelper audioHelper)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_authContext = authorizationContext;
|
||||
_userManager = userManger;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_fileSystem = fileSystem;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_configuration = configuration;
|
||||
_deviceManager = deviceManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_audioHelper = audioHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] EncodingContext? context,
|
||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||
{
|
||||
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
|
@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers
|
|||
StreamOptions = streamOptions
|
||||
};
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
_mediaSourceManager,
|
||||
_userManager,
|
||||
_libraryManager,
|
||||
_serverConfigurationManager,
|
||||
_mediaEncoder,
|
||||
_fileSystem,
|
||||
_subtitleEncoder,
|
||||
_configuration,
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
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);
|
||||
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpGet]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[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 };
|
||||
return _deviceManager.GetDevices(deviceQuery);
|
||||
|
|
|
@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers;
|
|||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
|
@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers
|
|||
private readonly IConfiguration _configuration;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly ILogger<DynamicHlsController> _logger;
|
||||
private readonly EncodingHelper _encodingHelper;
|
||||
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||
|
||||
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="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</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="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
|
||||
public DynamicHlsController(
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
|
@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers
|
|||
IConfiguration configuration,
|
||||
IDeviceManager deviceManager,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
INetworkManager networkManager,
|
||||
ILogger<DynamicHlsController> logger)
|
||||
ILogger<DynamicHlsController> logger,
|
||||
DynamicHlsHelper dynamicHlsHelper)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
|
@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers
|
|||
_configuration = configuration;
|
||||
_deviceManager = deviceManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_networkManager = networkManager;
|
||||
_logger = logger;
|
||||
_dynamicHlsHelper = dynamicHlsHelper;
|
||||
|
||||
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||
}
|
||||
|
@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] Dictionary<string, string> streamOptions,
|
||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||
{
|
||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var streamingRequest = new HlsVideoRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
|
@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers
|
|||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
};
|
||||
|
||||
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
|
||||
.ConfigureAwait(false);
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] Dictionary<string, string> streamOptions,
|
||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||
{
|
||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var streamingRequest = new HlsAudioRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
|
@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers
|
|||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
};
|
||||
|
||||
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
|
||||
.ConfigureAwait(false);
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers
|
|||
.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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var result = new List<double>();
|
||||
|
@ -2089,7 +1657,7 @@ namespace Jellyfin.Api.Controllers
|
|||
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)
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
|
|||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
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>
|
||||
|
@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers
|
|||
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)
|
||||
};
|
||||
|
||||
if (!librarySeriesId.Equals(Guid.Empty))
|
||||
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
|
||||
{
|
||||
query.IsSeries = true;
|
||||
|
||||
if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series)
|
||||
if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
|
||||
{
|
||||
query.Name = series.Name;
|
||||
}
|
||||
|
@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="validateLogin">Validate login.</param>
|
||||
/// <response code="200">Created listings provider returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||
[HttpGet("ListingProviders")]
|
||||
[HttpPost("ListingProviders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||
|
|
|
@ -1,30 +1,18 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.MediaInfoDtos;
|
||||
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.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.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers
|
|||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly ILogger<MediaInfoController> _logger;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly MediaInfoHelper _mediaInfoHelper;
|
||||
|
||||
/// <summary>
|
||||
/// 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="deviceManager">Instance of the <see cref="IDeviceManager"/> 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="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(
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IDeviceManager deviceManager,
|
||||
ILibraryManager libraryManager,
|
||||
INetworkManager networkManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IUserManager userManager,
|
||||
IAuthorizationContext authContext,
|
||||
ILogger<MediaInfoController> logger,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
MediaInfoHelper mediaInfoHelper)
|
||||
{
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceManager = deviceManager;
|
||||
_libraryManager = libraryManager;
|
||||
_networkManager = networkManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_userManager = userManager;
|
||||
_authContext = authContext;
|
||||
_logger = logger;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_mediaInfoHelper = mediaInfoHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers
|
|||
|
||||
foreach (var mediaSource in info.MediaSources)
|
||||
{
|
||||
SetDeviceSpecificData(
|
||||
_mediaInfoHelper.SetDeviceSpecificData(
|
||||
item,
|
||||
mediaSource,
|
||||
profile,
|
||||
|
@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers
|
|||
enableDirectStream,
|
||||
enableTranscoding,
|
||||
allowVideoStreamCopy,
|
||||
allowAudioStreamCopy);
|
||||
allowAudioStreamCopy,
|
||||
Request.HttpContext.Connection.RemoteIpAddress.ToString());
|
||||
}
|
||||
|
||||
SortMediaSources(info, maxStreamingBitrate);
|
||||
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||
}
|
||||
|
||||
if (autoOpenLiveStream)
|
||||
|
@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers
|
|||
|
||||
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
||||
{
|
||||
var openStreamResult = await OpenMediaSource(new LiveStreamRequest
|
||||
{
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
DeviceProfile = deviceProfile?.DeviceProfile,
|
||||
EnableDirectPlay = enableDirectPlay,
|
||||
EnableDirectStream = enableDirectStream,
|
||||
ItemId = itemId,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
MaxStreamingBitrate = maxStreamingBitrate,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleStreamIndex = subtitleStreamIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
OpenToken = mediaSource.OpenToken
|
||||
}).ConfigureAwait(false);
|
||||
var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
|
||||
Request,
|
||||
new LiveStreamRequest
|
||||
{
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
DeviceProfile = deviceProfile?.DeviceProfile,
|
||||
EnableDirectPlay = enableDirectPlay,
|
||||
EnableDirectStream = enableDirectStream,
|
||||
ItemId = itemId,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
MaxStreamingBitrate = maxStreamingBitrate,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleStreamIndex = subtitleStreamIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
OpenToken = mediaSource.OpenToken
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
info.MediaSources = new[] { openStreamResult.MediaSource };
|
||||
}
|
||||
|
@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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,
|
||||
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
||||
};
|
||||
return await OpenMediaSource(request).ConfigureAwait(false);
|
||||
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers
|
|||
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.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="itemId">Item id.</param>
|
||||
/// <response code="200">Item marked as unplayed.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
|
||||
[HttpDelete("Users/{userId}/PlayedItem/{itemId}")]
|
||||
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
|
||||
{
|
||||
|
|
|
@ -413,7 +413,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostFullCapabilities(
|
||||
[FromQuery, Required] string? id,
|
||||
[FromQuery] string? id,
|
||||
[FromBody, Required] ClientCapabilities capabilities)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
|
@ -480,7 +480,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// </summary>
|
||||
/// <response code="200">Password reset providers retrieved.</response>
|
||||
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
|
||||
[HttpGet("Auto/PasswordResetProviders")]
|
||||
[HttpGet("Auth/PasswordResetProviders")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
|
||||
|
|
|
@ -2,17 +2,20 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
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.Model.Dlna;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
|
@ -23,27 +26,39 @@ namespace Jellyfin.Api.Controllers
|
|||
public class UniversalAudioController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IAuthorizationContext _authorizationContext;
|
||||
private readonly MediaInfoController _mediaInfoController;
|
||||
private readonly DynamicHlsController _dynamicHlsController;
|
||||
private readonly AudioController _audioController;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<UniversalAudioController> _logger;
|
||||
private readonly MediaInfoHelper _mediaInfoHelper;
|
||||
private readonly AudioHelper _audioHelper;
|
||||
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
|
||||
/// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
|
||||
/// <param name="audioController">Instance of the <see cref="AudioController"/>.</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="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(
|
||||
IAuthorizationContext authorizationContext,
|
||||
MediaInfoController mediaInfoController,
|
||||
DynamicHlsController dynamicHlsController,
|
||||
AudioController audioController)
|
||||
IDeviceManager deviceManager,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<UniversalAudioController> logger,
|
||||
MediaInfoHelper mediaInfoHelper,
|
||||
AudioHelper audioHelper,
|
||||
DynamicHlsHelper dynamicHlsHelper)
|
||||
{
|
||||
_authorizationContext = authorizationContext;
|
||||
_mediaInfoController = mediaInfoController;
|
||||
_dynamicHlsController = dynamicHlsController;
|
||||
_audioController = audioController;
|
||||
_deviceManager = deviceManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_mediaInfoHelper = mediaInfoHelper;
|
||||
_audioHelper = audioHelper;
|
||||
_dynamicHlsHelper = dynamicHlsHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -95,24 +110,68 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] bool breakOnNonKeyFrames,
|
||||
[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);
|
||||
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
|
||||
|
||||
var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
|
||||
itemId,
|
||||
userId,
|
||||
maxStreamingBitrate,
|
||||
startTimeTicks,
|
||||
null,
|
||||
null,
|
||||
maxAudioChannels,
|
||||
mediaSourceId,
|
||||
null,
|
||||
new DeviceProfileDto { DeviceProfile = deviceProfile })
|
||||
.ConfigureAwait(false);
|
||||
var mediaSource = playbackInfoResult.Value.MediaSources[0];
|
||||
var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
_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 (enableRedirection)
|
||||
|
@ -127,129 +186,71 @@ namespace Jellyfin.Api.Controllers
|
|||
var isStatic = mediaSource.SupportsDirectStream;
|
||||
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
|
||||
// TODO: remove this when we switch back to the segment muxer
|
||||
var supportedHlsContainers = new[] { "mpegts", "fmp4" };
|
||||
|
||||
if (isHeadRequest)
|
||||
var dynamicHlsRequestDto = new HlsAudioRequestDto
|
||||
{
|
||||
_dynamicHlsController.Request.Method = HttpMethod.Head.Method;
|
||||
}
|
||||
|
||||
return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
|
||||
itemId,
|
||||
".m3u8",
|
||||
isStatic,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
playbackInfoResult.Value.PlaySessionId,
|
||||
Id = itemId,
|
||||
Container = ".m3u8",
|
||||
Static = isStatic,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
||||
Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
|
||||
null,
|
||||
null,
|
||||
mediaSource.Id,
|
||||
deviceId,
|
||||
transcodingProfile.AudioCodec,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
transcodingProfile.BreakOnNonKeyFrames,
|
||||
maxAudioSampleRate,
|
||||
maxAudioBitDepth,
|
||||
null,
|
||||
isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||
maxAudioChannels,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
startTimeTicks,
|
||||
null,
|
||||
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;
|
||||
}
|
||||
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
|
||||
MediaSourceId = mediaSourceId,
|
||||
DeviceId = deviceId,
|
||||
AudioCodec = audioCodec,
|
||||
EnableAutoStreamCopy = true,
|
||||
AllowAudioStreamCopy = true,
|
||||
AllowVideoStreamCopy = true,
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
||||
AudioSampleRate = maxAudioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||
RequireAvc = true,
|
||||
DeInterlace = true,
|
||||
RequireNonAnamorphic = true,
|
||||
EnableMpegtsM2TsMode = true,
|
||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||
Context = EncodingContext.Static,
|
||||
StreamOptions = new Dictionary<string, string>(),
|
||||
EnableAdaptiveBitrateStreaming = true
|
||||
};
|
||||
|
||||
return await _audioController.GetAudioStream(
|
||||
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)
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
|
||||
.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(
|
||||
|
|
|
@ -471,7 +471,7 @@ namespace Jellyfin.Api.Controllers
|
|||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
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)
|
||||
|
@ -507,7 +507,7 @@ namespace Jellyfin.Api.Controllers
|
|||
state.MediaPath,
|
||||
contentType,
|
||||
isHeadRequest,
|
||||
this);
|
||||
HttpContext);
|
||||
}
|
||||
|
||||
// Need to start ffmpeg (because media can't be returned directly)
|
||||
|
@ -517,10 +517,9 @@ namespace Jellyfin.Api.Controllers
|
|||
return await FileStreamResponseHelpers.GetTranscodedFile(
|
||||
state,
|
||||
isHeadRequest,
|
||||
this,
|
||||
HttpContext,
|
||||
_transcodingJobHelper,
|
||||
ffmpegCommandLineArguments,
|
||||
Request,
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
|
|
195
Jellyfin.Api/Helpers/AudioHelper.cs
Normal file
195
Jellyfin.Api/Helpers/AudioHelper.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
550
Jellyfin.Api/Helpers/DynamicHlsHelper.cs
Normal file
550
Jellyfin.Api/Helpers/DynamicHlsHelper.cs
Normal file
|
@ -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>
|
||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
||||
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
|
||||
/// <param name="httpContext">The current http context.</param>
|
||||
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
|
||||
public static async Task<ActionResult> GetStaticRemoteStreamResult(
|
||||
StreamState state,
|
||||
bool isHeadRequest,
|
||||
ControllerBase controller,
|
||||
HttpClient httpClient)
|
||||
HttpClient httpClient,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
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 contentType = response.Content.Headers.ContentType.ToString();
|
||||
|
||||
controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
|
||||
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>
|
||||
|
@ -56,23 +56,23 @@ namespace Jellyfin.Api.Helpers
|
|||
/// <param name="path">The path to the file.</param>
|
||||
/// <param name="contentType">The content type of the file.</param>
|
||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
||||
/// <param name="httpContext">The current http context.</param>
|
||||
/// <returns>An <see cref="ActionResult"/> the file.</returns>
|
||||
public static ActionResult GetStaticFileResult(
|
||||
string path,
|
||||
string contentType,
|
||||
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 (isHeadRequest)
|
||||
{
|
||||
return controller.NoContent();
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
return controller.PhysicalFile(path, contentType);
|
||||
return new PhysicalFileResult(path, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -80,34 +80,32 @@ namespace Jellyfin.Api.Helpers
|
|||
/// </summary>
|
||||
/// <param name="state">The current <see cref="StreamState"/>.</param>
|
||||
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
|
||||
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
|
||||
/// <param name="httpContext">The current http context.</param>
|
||||
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
|
||||
/// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
|
||||
/// <param name="request">The <see cref="HttpRequest"/> starting the transcoding.</param>
|
||||
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
|
||||
/// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
|
||||
public static async Task<ActionResult> GetTranscodedFile(
|
||||
StreamState state,
|
||||
bool isHeadRequest,
|
||||
ControllerBase controller,
|
||||
HttpContext httpContext,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
string ffmpegCommandLineArguments,
|
||||
HttpRequest request,
|
||||
TranscodingJobType transcodingJobType,
|
||||
CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
// Use the command line args with a dummy playlist path
|
||||
var outputPath = state.OutputFilePath;
|
||||
|
||||
controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
|
||||
var contentType = state.GetMimeType(outputPath);
|
||||
|
||||
// Headers only
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return controller.File(Array.Empty<byte>(), contentType);
|
||||
return new FileContentResult(Array.Empty<byte>(), contentType);
|
||||
}
|
||||
|
||||
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
||||
|
@ -117,7 +115,7 @@ namespace Jellyfin.Api.Helpers
|
|||
TranscodingJobDto? job;
|
||||
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
|
||||
{
|
||||
|
@ -128,7 +126,7 @@ namespace Jellyfin.Api.Helpers
|
|||
var memoryStream = new MemoryStream();
|
||||
await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
|
||||
memoryStream.Position = 0;
|
||||
return controller.File(memoryStream, contentType);
|
||||
return new FileStreamResult(memoryStream, contentType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
573
Jellyfin.Api/Helpers/MediaInfoHelper.cs
Normal file
573
Jellyfin.Api/Helpers/MediaInfoHelper.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
44
Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
Normal file
44
Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
Normal file
|
@ -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.Middleware;
|
||||
using Jellyfin.Server.Models;
|
||||
|
@ -94,6 +96,9 @@ namespace Jellyfin.Server
|
|||
});
|
||||
|
||||
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
|
||||
|
||||
// Add type descriptor for legacy datetime parsing.
|
||||
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user