mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-07 03:48:06 +02:00
Merge pull request #3783 from Ullmie02/api-universal-audio
Move UniversalAudioService to Jellyfin.Api
This commit is contained in:
commit
cd515bdbfd
5 changed files with 415 additions and 1169 deletions
|
@ -197,8 +197,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] string? transcodingReasons,
|
[FromQuery] string? transcodingReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||||
{
|
{
|
||||||
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
TranscodeReasons = transcodingReasons,
|
TranscodeReasons = transcodingReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Static,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
412
Jellyfin.Api/Controllers/UniversalAudioController.cs
Normal file
412
Jellyfin.Api/Controllers/UniversalAudioController.cs
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
using System;
|
||||||
|
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 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.IO;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The universal audio controller.
|
||||||
|
/// </summary>
|
||||||
|
public class UniversalAudioController : BaseJellyfinApiController
|
||||||
|
{
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
private readonly IDlnaManager _dlnaManager;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
|
private readonly IAuthorizationContext _authorizationContext;
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
|
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||||
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
|
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> interface.</param>
|
||||||
|
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||||
|
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
|
||||||
|
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||||
|
public UniversalAudioController(
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IDlnaManager dlnaManager,
|
||||||
|
IDeviceManager deviceManager,
|
||||||
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
IAuthorizationContext authorizationContext,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
TranscodingJobHelper transcodingJobHelper,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ISubtitleEncoder subtitleEncoder,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_dlnaManager = dlnaManager;
|
||||||
|
_deviceManager = deviceManager;
|
||||||
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_authorizationContext = authorizationContext;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
|
_configuration = configuration;
|
||||||
|
_subtitleEncoder = subtitleEncoder;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an audio stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <param name="container">Optional. The audio container.</param>
|
||||||
|
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||||
|
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||||
|
/// <param name="userId">Optional. The user id.</param>
|
||||||
|
/// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
|
||||||
|
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
|
||||||
|
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
|
||||||
|
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||||
|
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||||
|
/// <param name="transcodingContainer">Optional. The container to transcode to.</param>
|
||||||
|
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
|
||||||
|
/// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
|
||||||
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
|
/// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
|
||||||
|
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||||
|
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
|
||||||
|
/// <response code="200">Audio stream returned.</response>
|
||||||
|
/// <response code="302">Redirected to remote audio stream.</response>
|
||||||
|
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
|
||||||
|
[HttpGet("/Audio/{itemId}/universal")]
|
||||||
|
[HttpGet("/Audio/{itemId}/{universal=universal}.{container?}")]
|
||||||
|
[HttpHead("/Audio/{itemId}/universal")]
|
||||||
|
[HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")]
|
||||||
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
|
public async Task<ActionResult> GetUniversalAudioStream(
|
||||||
|
[FromRoute] Guid itemId,
|
||||||
|
[FromRoute] string? container,
|
||||||
|
[FromQuery] string? mediaSourceId,
|
||||||
|
[FromQuery] string? deviceId,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] string? audioCodec,
|
||||||
|
[FromQuery] int? maxAudioChannels,
|
||||||
|
[FromQuery] int? transcodingAudioChannels,
|
||||||
|
[FromQuery] long? maxStreamingBitrate,
|
||||||
|
[FromQuery] long? startTimeTicks,
|
||||||
|
[FromQuery] string? transcodingContainer,
|
||||||
|
[FromQuery] string? transcodingProtocol,
|
||||||
|
[FromQuery] int? maxAudioSampleRate,
|
||||||
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
|
[FromQuery] bool? enableRemoteMedia,
|
||||||
|
[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 mediaInfoController = new MediaInfoController(_mediaSourceManager, _deviceManager, _libraryManager, _networkManager, _mediaEncoder, _userManager, _authorizationContext, _loggerFactory.CreateLogger<MediaInfoController>(), _serverConfigurationManager);
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
|
||||||
|
{
|
||||||
|
if (enableRedirection)
|
||||||
|
{
|
||||||
|
if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
|
||||||
|
{
|
||||||
|
return Redirect(mediaSource.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isStatic = mediaSource.SupportsDirectStream;
|
||||||
|
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var dynamicHlsController = new DynamicHlsController(
|
||||||
|
_libraryManager,
|
||||||
|
_userManager,
|
||||||
|
_dlnaManager,
|
||||||
|
_authorizationContext,
|
||||||
|
_mediaSourceManager,
|
||||||
|
_serverConfigurationManager,
|
||||||
|
_mediaEncoder,
|
||||||
|
_fileSystem,
|
||||||
|
_subtitleEncoder,
|
||||||
|
_configuration,
|
||||||
|
_deviceManager,
|
||||||
|
_transcodingJobHelper,
|
||||||
|
_networkManager,
|
||||||
|
_loggerFactory.CreateLogger<DynamicHlsController>());
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
dynamicHlsController.Request.Method = HttpMethod.Head.Method;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dynamicHlsController.GetMasterHlsAudioPlaylist(
|
||||||
|
itemId,
|
||||||
|
".m3u8",
|
||||||
|
isStatic,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
playbackInfoResult.Value.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
|
||||||
|
{
|
||||||
|
var audioController = new AudioController(
|
||||||
|
_dlnaManager,
|
||||||
|
_userManager,
|
||||||
|
_authorizationContext,
|
||||||
|
_libraryManager,
|
||||||
|
_mediaSourceManager,
|
||||||
|
_serverConfigurationManager,
|
||||||
|
_mediaEncoder,
|
||||||
|
_fileSystem,
|
||||||
|
_subtitleEncoder,
|
||||||
|
_configuration,
|
||||||
|
_deviceManager,
|
||||||
|
_transcodingJobHelper,
|
||||||
|
_httpClientFactory);
|
||||||
|
|
||||||
|
if (isHeadRequest)
|
||||||
|
{
|
||||||
|
audioController.Request.Method = HttpMethod.Head.Method;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceProfile GetDeviceProfile(
|
||||||
|
string? container,
|
||||||
|
string? transcodingContainer,
|
||||||
|
string? audioCodec,
|
||||||
|
string? transcodingProtocol,
|
||||||
|
bool? breakOnNonKeyFrames,
|
||||||
|
int? transcodingAudioChannels,
|
||||||
|
int? maxAudioSampleRate,
|
||||||
|
int? maxAudioBitDepth,
|
||||||
|
int? maxAudioChannels)
|
||||||
|
{
|
||||||
|
var deviceProfile = new DeviceProfile();
|
||||||
|
|
||||||
|
var directPlayProfiles = new List<DirectPlayProfile>();
|
||||||
|
|
||||||
|
var containers = RequestHelpers.Split(container, ',', true);
|
||||||
|
|
||||||
|
foreach (var cont in containers)
|
||||||
|
{
|
||||||
|
var parts = RequestHelpers.Split(cont, ',', true);
|
||||||
|
|
||||||
|
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
|
||||||
|
|
||||||
|
directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
|
||||||
|
|
||||||
|
deviceProfile.TranscodingProfiles = new[]
|
||||||
|
{
|
||||||
|
new TranscodingProfile
|
||||||
|
{
|
||||||
|
Type = DlnaProfileType.Audio,
|
||||||
|
Context = EncodingContext.Streaming,
|
||||||
|
Container = transcodingContainer,
|
||||||
|
AudioCodec = audioCodec,
|
||||||
|
Protocol = transcodingProtocol,
|
||||||
|
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||||
|
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var codecProfiles = new List<CodecProfile>();
|
||||||
|
var conditions = new List<ProfileCondition>();
|
||||||
|
|
||||||
|
if (maxAudioSampleRate.HasValue)
|
||||||
|
{
|
||||||
|
// codec profile
|
||||||
|
conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioSampleRate, Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAudioBitDepth.HasValue)
|
||||||
|
{
|
||||||
|
// codec profile
|
||||||
|
conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioBitDepth, Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAudioChannels.HasValue)
|
||||||
|
{
|
||||||
|
// codec profile
|
||||||
|
conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioChannels, Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.Count > 0)
|
||||||
|
{
|
||||||
|
// codec profile
|
||||||
|
codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceProfile.CodecProfiles = codecProfiles.ToArray();
|
||||||
|
|
||||||
|
return deviceProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,674 +0,0 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
#pragma warning disable SA1402
|
|
||||||
#pragma warning disable SA1649
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
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.Services;
|
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
|
||||||
public Guid UserId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CloseMediaSource : IReturnVoid
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
|
||||||
public string LiveStreamId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetBitrateTestBytes
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
|
|
||||||
public int Size { get; set; }
|
|
||||||
|
|
||||||
public GetBitrateTestBytes()
|
|
||||||
{
|
|
||||||
// 100k
|
|
||||||
Size = 102400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authenticated]
|
|
||||||
public class MediaInfoService : BaseApiService
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
public MediaInfoService(
|
|
||||||
ILogger<MediaInfoService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IUserManager userManager,
|
|
||||||
IAuthorizationContext authContext)
|
|
||||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
|
||||||
{
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_networkManager = networkManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_userManager = userManager;
|
|
||||||
_authContext = authContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Get(GetBitrateTestBytes request)
|
|
||||||
{
|
|
||||||
const int MaxSize = 10_000_000;
|
|
||||||
|
|
||||||
var size = request.Size;
|
|
||||||
|
|
||||||
if (size <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"The requested size ({size}) is equal to or smaller than 0.", nameof(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size > MaxSize)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).", nameof(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
new Random().NextBytes(buffer);
|
|
||||||
return ResultFactory.GetResult(null, buffer, "application/octet-stream");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Get(GetPlaybackInfo request)
|
|
||||||
{
|
|
||||||
var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
|
|
||||||
return ToOptimizedResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Post(OpenMediaSource request)
|
|
||||||
{
|
|
||||||
var result = await OpenMediaSource(request).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return ToOptimizedResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<LiveStreamResponse> OpenMediaSource(OpenMediaSource 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, true, request.EnableDirectStream, true, true, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
|
|
||||||
{
|
|
||||||
result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.MediaSource != null)
|
|
||||||
{
|
|
||||||
NormalizeMediaSourceContainer(result.MediaSource, profile, DlnaProfileType.Video);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Post(CloseMediaSource request)
|
|
||||||
{
|
|
||||||
_mediaSourceManager.CloseLiveStream(request.LiveStreamId).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
|
|
||||||
{
|
|
||||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
|
||||||
|
|
||||||
var profile = request.DeviceProfile;
|
|
||||||
|
|
||||||
Logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
|
|
||||||
|
|
||||||
if (profile == null)
|
|
||||||
{
|
|
||||||
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
|
||||||
if (caps != null)
|
|
||||||
{
|
|
||||||
profile = caps.DeviceProfile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (profile != null)
|
|
||||||
{
|
|
||||||
var mediaSourceId = request.MediaSourceId;
|
|
||||||
|
|
||||||
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, request.EnableTranscoding, request.AllowVideoStreamCopy, request.AllowAudioStreamCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.AutoOpenLiveStream)
|
|
||||||
{
|
|
||||||
var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId) ? info.MediaSources.FirstOrDefault() : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
|
||||||
{
|
|
||||||
var openStreamResult = await OpenMediaSource(new OpenMediaSource
|
|
||||||
{
|
|
||||||
AudioStreamIndex = request.AudioStreamIndex,
|
|
||||||
DeviceProfile = request.DeviceProfile,
|
|
||||||
EnableDirectPlay = request.EnableDirectPlay,
|
|
||||||
EnableDirectStream = request.EnableDirectStream,
|
|
||||||
ItemId = request.Id,
|
|
||||||
MaxAudioChannels = request.MaxAudioChannels,
|
|
||||||
MaxStreamingBitrate = request.MaxStreamingBitrate,
|
|
||||||
PlaySessionId = info.PlaySessionId,
|
|
||||||
StartTimeTicks = request.StartTimeTicks,
|
|
||||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
|
||||||
UserId = request.UserId,
|
|
||||||
OpenToken = mediaSource.OpenToken
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
info.MediaSources = new[] { openStreamResult.MediaSource };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.MediaSources != null)
|
|
||||||
{
|
|
||||||
foreach (var mediaSource in info.MediaSources)
|
|
||||||
{
|
|
||||||
NormalizeMediaSourceContainer(mediaSource, profile, DlnaProfileType.Video);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
|
|
||||||
{
|
|
||||||
mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Post(GetPostedPlaybackInfo request)
|
|
||||||
{
|
|
||||||
var result = await GetPlaybackInfo(request).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return ToOptimizedResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
|
|
||||||
{
|
|
||||||
var user = _userManager.GetUserById(userId);
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
var result = new PlaybackInfoResponse();
|
|
||||||
|
|
||||||
MediaSourceInfo[] mediaSources;
|
|
||||||
if (string.IsNullOrWhiteSpace(liveStreamId))
|
|
||||||
{
|
|
||||||
|
|
||||||
// TODO 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>();
|
|
||||||
|
|
||||||
if (!result.ErrorCode.HasValue)
|
|
||||||
{
|
|
||||||
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 SetDeviceSpecificData(
|
|
||||||
Guid itemId,
|
|
||||||
PlaybackInfoResponse result,
|
|
||||||
DeviceProfile profile,
|
|
||||||
AuthorizationInfo auth,
|
|
||||||
long? maxBitrate,
|
|
||||||
long startTimeTicks,
|
|
||||||
string mediaSourceId,
|
|
||||||
int? audioStreamIndex,
|
|
||||||
int? subtitleStreamIndex,
|
|
||||||
int? maxAudioChannels,
|
|
||||||
Guid userId,
|
|
||||||
bool enableDirectPlay,
|
|
||||||
bool forceDirectPlayRemoteMediaSource,
|
|
||||||
bool enableDirectStream,
|
|
||||||
bool enableTranscoding,
|
|
||||||
bool allowVideoStreamCopy,
|
|
||||||
bool allowAudioStreamCopy)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
|
||||||
|
|
||||||
foreach (var mediaSource in result.MediaSources)
|
|
||||||
{
|
|
||||||
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
SortMediaSources(result, maxBitrate);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 forceDirectPlayRemoteMediaSource,
|
|
||||||
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 long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.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.RemoteIp);
|
|
||||||
|
|
||||||
Logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
|
|
||||||
if (!isInLocalNetwork)
|
|
||||||
{
|
|
||||||
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxBitrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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,91 +0,0 @@
|
||||||
using System.Threading.Tasks;
|
|
||||||
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.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class GetAudioStream.
|
|
||||||
/// </summary>
|
|
||||||
public class GetAudioStream : StreamRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class AudioService.
|
|
||||||
/// </summary>
|
|
||||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
|
||||||
//[Authenticated]
|
|
||||||
public class AudioService : BaseProgressiveStreamingService
|
|
||||||
{
|
|
||||||
public AudioService(
|
|
||||||
ILogger<AudioService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IHttpClient httpClient,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
httpClient,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public Task<object> Get(GetAudioStream request)
|
|
||||||
{
|
|
||||||
return ProcessRequest(request, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public Task<object> Head(GetAudioStream request)
|
|
||||||
{
|
|
||||||
return ProcessRequest(request, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
|
|
||||||
{
|
|
||||||
return EncodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,401 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Api.Playback.Hls;
|
|
||||||
using MediaBrowser.Api.Playback.Progressive;
|
|
||||||
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.IO;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
public class BaseUniversalRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The id.</value>
|
|
||||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
|
||||||
public string MediaSourceId { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
|
||||||
public string DeviceId { get; set; }
|
|
||||||
|
|
||||||
public Guid UserId { get; set; }
|
|
||||||
|
|
||||||
public string AudioCodec { get; set; }
|
|
||||||
|
|
||||||
public string Container { get; set; }
|
|
||||||
|
|
||||||
public int? MaxAudioChannels { get; set; }
|
|
||||||
|
|
||||||
public int? TranscodingAudioChannels { get; set; }
|
|
||||||
|
|
||||||
public long? MaxStreamingBitrate { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
|
||||||
public long? StartTimeTicks { get; set; }
|
|
||||||
|
|
||||||
public string TranscodingContainer { get; set; }
|
|
||||||
|
|
||||||
public string TranscodingProtocol { get; set; }
|
|
||||||
|
|
||||||
public int? MaxAudioSampleRate { get; set; }
|
|
||||||
|
|
||||||
public int? MaxAudioBitDepth { get; set; }
|
|
||||||
|
|
||||||
public bool EnableRedirection { get; set; }
|
|
||||||
|
|
||||||
public bool EnableRemoteMedia { get; set; }
|
|
||||||
|
|
||||||
public bool BreakOnNonKeyFrames { get; set; }
|
|
||||||
|
|
||||||
public BaseUniversalRequest()
|
|
||||||
{
|
|
||||||
EnableRedirection = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
|
|
||||||
[Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
|
|
||||||
[Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
|
|
||||||
[Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
|
|
||||||
public class GetUniversalAudioStream : BaseUniversalRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authenticated]
|
|
||||||
public class UniversalAudioService : BaseApiService
|
|
||||||
{
|
|
||||||
private readonly EncodingHelper _encodingHelper;
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
|
||||||
|
|
||||||
public UniversalAudioService(
|
|
||||||
ILogger<UniversalAudioService> logger,
|
|
||||||
ILoggerFactory loggerFactory,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IHttpClient httpClient,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
|
||||||
{
|
|
||||||
HttpClient = httpClient;
|
|
||||||
UserManager = userManager;
|
|
||||||
LibraryManager = libraryManager;
|
|
||||||
IsoManager = isoManager;
|
|
||||||
MediaEncoder = mediaEncoder;
|
|
||||||
FileSystem = fileSystem;
|
|
||||||
DlnaManager = dlnaManager;
|
|
||||||
DeviceManager = deviceManager;
|
|
||||||
MediaSourceManager = mediaSourceManager;
|
|
||||||
JsonSerializer = jsonSerializer;
|
|
||||||
AuthorizationContext = authorizationContext;
|
|
||||||
NetworkManager = networkManager;
|
|
||||||
_encodingHelper = encodingHelper;
|
|
||||||
_loggerFactory = loggerFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected IHttpClient HttpClient { get; private set; }
|
|
||||||
|
|
||||||
protected IUserManager UserManager { get; private set; }
|
|
||||||
|
|
||||||
protected ILibraryManager LibraryManager { get; private set; }
|
|
||||||
|
|
||||||
protected IIsoManager IsoManager { get; private set; }
|
|
||||||
|
|
||||||
protected IMediaEncoder MediaEncoder { get; private set; }
|
|
||||||
|
|
||||||
protected IFileSystem FileSystem { get; private set; }
|
|
||||||
|
|
||||||
protected IDlnaManager DlnaManager { get; private set; }
|
|
||||||
|
|
||||||
protected IDeviceManager DeviceManager { get; private set; }
|
|
||||||
|
|
||||||
protected IMediaSourceManager MediaSourceManager { get; private set; }
|
|
||||||
|
|
||||||
protected IJsonSerializer JsonSerializer { get; private set; }
|
|
||||||
|
|
||||||
protected IAuthorizationContext AuthorizationContext { get; private set; }
|
|
||||||
|
|
||||||
protected INetworkManager NetworkManager { get; private set; }
|
|
||||||
|
|
||||||
public Task<object> Get(GetUniversalAudioStream request)
|
|
||||||
{
|
|
||||||
return GetUniversalStream(request, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<object> Head(GetUniversalAudioStream request)
|
|
||||||
{
|
|
||||||
return GetUniversalStream(request, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
|
|
||||||
{
|
|
||||||
var deviceProfile = new DeviceProfile();
|
|
||||||
|
|
||||||
var directPlayProfiles = new List<DirectPlayProfile>();
|
|
||||||
|
|
||||||
var containers = (request.Container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
foreach (var container in containers)
|
|
||||||
{
|
|
||||||
var parts = container.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
|
|
||||||
|
|
||||||
directPlayProfiles.Add(new DirectPlayProfile
|
|
||||||
{
|
|
||||||
Type = DlnaProfileType.Audio,
|
|
||||||
Container = parts[0],
|
|
||||||
AudioCodec = audioCodecs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
|
|
||||||
|
|
||||||
deviceProfile.TranscodingProfiles = new[]
|
|
||||||
{
|
|
||||||
new TranscodingProfile
|
|
||||||
{
|
|
||||||
Type = DlnaProfileType.Audio,
|
|
||||||
Context = EncodingContext.Streaming,
|
|
||||||
Container = request.TranscodingContainer,
|
|
||||||
AudioCodec = request.AudioCodec,
|
|
||||||
Protocol = request.TranscodingProtocol,
|
|
||||||
BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
|
|
||||||
MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var codecProfiles = new List<CodecProfile>();
|
|
||||||
var conditions = new List<ProfileCondition>();
|
|
||||||
|
|
||||||
if (request.MaxAudioSampleRate.HasValue)
|
|
||||||
{
|
|
||||||
// codec profile
|
|
||||||
conditions.Add(new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
IsRequired = false,
|
|
||||||
Property = ProfileConditionValue.AudioSampleRate,
|
|
||||||
Value = request.MaxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.MaxAudioBitDepth.HasValue)
|
|
||||||
{
|
|
||||||
// codec profile
|
|
||||||
conditions.Add(new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
IsRequired = false,
|
|
||||||
Property = ProfileConditionValue.AudioBitDepth,
|
|
||||||
Value = request.MaxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.MaxAudioChannels.HasValue)
|
|
||||||
{
|
|
||||||
// codec profile
|
|
||||||
conditions.Add(new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
IsRequired = false,
|
|
||||||
Property = ProfileConditionValue.AudioChannels,
|
|
||||||
Value = request.MaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conditions.Count > 0)
|
|
||||||
{
|
|
||||||
// codec profile
|
|
||||||
codecProfiles.Add(new CodecProfile
|
|
||||||
{
|
|
||||||
Type = CodecType.Audio,
|
|
||||||
Container = request.Container,
|
|
||||||
Conditions = conditions.ToArray()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceProfile.CodecProfiles = codecProfiles.ToArray();
|
|
||||||
|
|
||||||
return deviceProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
|
|
||||||
{
|
|
||||||
var deviceProfile = GetDeviceProfile(request);
|
|
||||||
|
|
||||||
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
|
|
||||||
|
|
||||||
var mediaInfoService = new MediaInfoService(
|
|
||||||
_loggerFactory.CreateLogger<MediaInfoService>(),
|
|
||||||
ServerConfigurationManager,
|
|
||||||
ResultFactory,
|
|
||||||
MediaSourceManager,
|
|
||||||
DeviceManager,
|
|
||||||
LibraryManager,
|
|
||||||
NetworkManager,
|
|
||||||
MediaEncoder,
|
|
||||||
UserManager,
|
|
||||||
AuthorizationContext)
|
|
||||||
{
|
|
||||||
Request = Request
|
|
||||||
};
|
|
||||||
|
|
||||||
var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
|
|
||||||
{
|
|
||||||
Id = request.Id,
|
|
||||||
MaxAudioChannels = request.MaxAudioChannels,
|
|
||||||
MaxStreamingBitrate = request.MaxStreamingBitrate,
|
|
||||||
StartTimeTicks = request.StartTimeTicks,
|
|
||||||
UserId = request.UserId,
|
|
||||||
DeviceProfile = deviceProfile,
|
|
||||||
MediaSourceId = request.MediaSourceId
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var mediaSource = playbackInfoResult.MediaSources[0];
|
|
||||||
|
|
||||||
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
|
|
||||||
{
|
|
||||||
if (request.EnableRedirection)
|
|
||||||
{
|
|
||||||
if (mediaSource.IsRemote && request.EnableRemoteMedia)
|
|
||||||
{
|
|
||||||
return ResultFactory.GetRedirectResult(mediaSource.Path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isStatic = mediaSource.SupportsDirectStream;
|
|
||||||
|
|
||||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var service = new DynamicHlsService(
|
|
||||||
_loggerFactory.CreateLogger<DynamicHlsService>(),
|
|
||||||
ServerConfigurationManager,
|
|
||||||
ResultFactory,
|
|
||||||
UserManager,
|
|
||||||
LibraryManager,
|
|
||||||
IsoManager,
|
|
||||||
MediaEncoder,
|
|
||||||
FileSystem,
|
|
||||||
DlnaManager,
|
|
||||||
DeviceManager,
|
|
||||||
MediaSourceManager,
|
|
||||||
JsonSerializer,
|
|
||||||
AuthorizationContext,
|
|
||||||
NetworkManager,
|
|
||||||
_encodingHelper)
|
|
||||||
{
|
|
||||||
Request = Request
|
|
||||||
};
|
|
||||||
|
|
||||||
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" };
|
|
||||||
|
|
||||||
var newRequest = new GetMasterHlsAudioPlaylist
|
|
||||||
{
|
|
||||||
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
|
|
||||||
AudioCodec = transcodingProfile.AudioCodec,
|
|
||||||
Container = ".m3u8",
|
|
||||||
DeviceId = request.DeviceId,
|
|
||||||
Id = request.Id,
|
|
||||||
MaxAudioChannels = request.MaxAudioChannels,
|
|
||||||
MediaSourceId = mediaSource.Id,
|
|
||||||
PlaySessionId = playbackInfoResult.PlaySessionId,
|
|
||||||
StartTimeTicks = request.StartTimeTicks,
|
|
||||||
Static = isStatic,
|
|
||||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
|
||||||
SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
|
|
||||||
AudioSampleRate = request.MaxAudioSampleRate,
|
|
||||||
MaxAudioBitDepth = request.MaxAudioBitDepth,
|
|
||||||
BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
|
|
||||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
return await service.Head(newRequest).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await service.Get(newRequest).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var service = new AudioService(
|
|
||||||
_loggerFactory.CreateLogger<AudioService>(),
|
|
||||||
ServerConfigurationManager,
|
|
||||||
ResultFactory,
|
|
||||||
HttpClient,
|
|
||||||
UserManager,
|
|
||||||
LibraryManager,
|
|
||||||
IsoManager,
|
|
||||||
MediaEncoder,
|
|
||||||
FileSystem,
|
|
||||||
DlnaManager,
|
|
||||||
DeviceManager,
|
|
||||||
MediaSourceManager,
|
|
||||||
JsonSerializer,
|
|
||||||
AuthorizationContext,
|
|
||||||
_encodingHelper)
|
|
||||||
{
|
|
||||||
Request = Request
|
|
||||||
};
|
|
||||||
|
|
||||||
var newRequest = new GetAudioStream
|
|
||||||
{
|
|
||||||
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
|
|
||||||
AudioCodec = request.AudioCodec,
|
|
||||||
Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
|
||||||
DeviceId = request.DeviceId,
|
|
||||||
Id = request.Id,
|
|
||||||
MaxAudioChannels = request.MaxAudioChannels,
|
|
||||||
MediaSourceId = mediaSource.Id,
|
|
||||||
PlaySessionId = playbackInfoResult.PlaySessionId,
|
|
||||||
StartTimeTicks = request.StartTimeTicks,
|
|
||||||
Static = isStatic,
|
|
||||||
AudioSampleRate = request.MaxAudioSampleRate,
|
|
||||||
MaxAudioBitDepth = request.MaxAudioBitDepth,
|
|
||||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
return await service.Head(newRequest).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await service.Get(newRequest).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue