|  | @@ -0,0 +1,773 @@
 | 
	
		
			
				|  |  | +using System;
 | 
	
		
			
				|  |  | +using System.Buffers;
 | 
	
		
			
				|  |  | +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.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;
 | 
	
		
			
				|  |  | +using Microsoft.Extensions.Logging;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +namespace Jellyfin.Api.Controllers
 | 
	
		
			
				|  |  | +{
 | 
	
		
			
				|  |  | +    /// <summary>
 | 
	
		
			
				|  |  | +    /// The media info controller.
 | 
	
		
			
				|  |  | +    /// </summary>
 | 
	
		
			
				|  |  | +    [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
	
		
			
				|  |  | +    public class MediaInfoController : BaseJellyfinApiController
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        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 _logger;
 | 
	
		
			
				|  |  | +        private readonly IServerConfigurationManager _serverConfigurationManager;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <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>
 | 
	
		
			
				|  |  | +        public MediaInfoController(
 | 
	
		
			
				|  |  | +            IMediaSourceManager mediaSourceManager,
 | 
	
		
			
				|  |  | +            IDeviceManager deviceManager,
 | 
	
		
			
				|  |  | +            ILibraryManager libraryManager,
 | 
	
		
			
				|  |  | +            INetworkManager networkManager,
 | 
	
		
			
				|  |  | +            IMediaEncoder mediaEncoder,
 | 
	
		
			
				|  |  | +            IUserManager userManager,
 | 
	
		
			
				|  |  | +            IAuthorizationContext authContext,
 | 
	
		
			
				|  |  | +            ILogger<MediaInfoController> logger,
 | 
	
		
			
				|  |  | +            IServerConfigurationManager serverConfigurationManager)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            _mediaSourceManager = mediaSourceManager;
 | 
	
		
			
				|  |  | +            _deviceManager = deviceManager;
 | 
	
		
			
				|  |  | +            _libraryManager = libraryManager;
 | 
	
		
			
				|  |  | +            _networkManager = networkManager;
 | 
	
		
			
				|  |  | +            _mediaEncoder = mediaEncoder;
 | 
	
		
			
				|  |  | +            _userManager = userManager;
 | 
	
		
			
				|  |  | +            _authContext = authContext;
 | 
	
		
			
				|  |  | +            _logger = logger;
 | 
	
		
			
				|  |  | +            _serverConfigurationManager = serverConfigurationManager;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets live playback media info for an item.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <param name="itemId">The item id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="userId">The user id.</param>
 | 
	
		
			
				|  |  | +        /// <response code="200">Playback info returned.</response>
 | 
	
		
			
				|  |  | +        /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
 | 
	
		
			
				|  |  | +        [HttpGet("/Items/{itemId}/PlaybackInfo")]
 | 
	
		
			
				|  |  | +        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
	
		
			
				|  |  | +        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid userId)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            return await GetPlaybackInfoInternal(itemId, userId, null, null).ConfigureAwait(false);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets live playback media info for an item.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <param name="itemId">The item id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="userId">The user id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
 | 
	
		
			
				|  |  | +        /// <param name="startTimeTicks">The start time in ticks.</param>
 | 
	
		
			
				|  |  | +        /// <param name="audioStreamIndex">The audio stream index.</param>
 | 
	
		
			
				|  |  | +        /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
 | 
	
		
			
				|  |  | +        /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
 | 
	
		
			
				|  |  | +        /// <param name="mediaSourceId">The media source id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="liveStreamId">The livestream id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="deviceProfile">The device profile.</param>
 | 
	
		
			
				|  |  | +        /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
 | 
	
		
			
				|  |  | +        /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <response code="200">Playback info returned.</response>
 | 
	
		
			
				|  |  | +        /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
 | 
	
		
			
				|  |  | +        [HttpPost("/Items/{itemId}/PlaybackInfo")]
 | 
	
		
			
				|  |  | +        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
	
		
			
				|  |  | +        public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
 | 
	
		
			
				|  |  | +            [FromRoute] Guid itemId,
 | 
	
		
			
				|  |  | +            [FromQuery] Guid userId,
 | 
	
		
			
				|  |  | +            [FromQuery] long? maxStreamingBitrate,
 | 
	
		
			
				|  |  | +            [FromQuery] long? startTimeTicks,
 | 
	
		
			
				|  |  | +            [FromQuery] int? audioStreamIndex,
 | 
	
		
			
				|  |  | +            [FromQuery] int? subtitleStreamIndex,
 | 
	
		
			
				|  |  | +            [FromQuery] int? maxAudioChannels,
 | 
	
		
			
				|  |  | +            [FromQuery] string mediaSourceId,
 | 
	
		
			
				|  |  | +            [FromQuery] string liveStreamId,
 | 
	
		
			
				|  |  | +            [FromQuery] DeviceProfile deviceProfile,
 | 
	
		
			
				|  |  | +            [FromQuery] bool autoOpenLiveStream,
 | 
	
		
			
				|  |  | +            [FromQuery] bool enableDirectPlay = true,
 | 
	
		
			
				|  |  | +            [FromQuery] bool enableDirectStream = true,
 | 
	
		
			
				|  |  | +            [FromQuery] bool enableTranscoding = true,
 | 
	
		
			
				|  |  | +            [FromQuery] bool allowVideoStreamCopy = true,
 | 
	
		
			
				|  |  | +            [FromQuery] bool allowAudioStreamCopy = true)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            var authInfo = _authContext.GetAuthorizationInfo(Request);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            var profile = 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 GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (profile != null)
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                // set device specific data
 | 
	
		
			
				|  |  | +                var item = _libraryManager.GetItemById(itemId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                foreach (var mediaSource in info.MediaSources)
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    SetDeviceSpecificData(
 | 
	
		
			
				|  |  | +                        item,
 | 
	
		
			
				|  |  | +                        mediaSource,
 | 
	
		
			
				|  |  | +                        profile,
 | 
	
		
			
				|  |  | +                        authInfo,
 | 
	
		
			
				|  |  | +                        maxStreamingBitrate ?? profile.MaxStreamingBitrate,
 | 
	
		
			
				|  |  | +                        startTimeTicks ?? 0,
 | 
	
		
			
				|  |  | +                        mediaSourceId,
 | 
	
		
			
				|  |  | +                        audioStreamIndex,
 | 
	
		
			
				|  |  | +                        subtitleStreamIndex,
 | 
	
		
			
				|  |  | +                        maxAudioChannels,
 | 
	
		
			
				|  |  | +                        info!.PlaySessionId!,
 | 
	
		
			
				|  |  | +                        userId,
 | 
	
		
			
				|  |  | +                        enableDirectPlay,
 | 
	
		
			
				|  |  | +                        enableDirectStream,
 | 
	
		
			
				|  |  | +                        enableTranscoding,
 | 
	
		
			
				|  |  | +                        allowVideoStreamCopy,
 | 
	
		
			
				|  |  | +                        allowAudioStreamCopy);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                SortMediaSources(info, maxStreamingBitrate);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (autoOpenLiveStream)
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    var openStreamResult = await OpenMediaSource(new LiveStreamRequest
 | 
	
		
			
				|  |  | +                    {
 | 
	
		
			
				|  |  | +                        AudioStreamIndex = audioStreamIndex,
 | 
	
		
			
				|  |  | +                        DeviceProfile = deviceProfile,
 | 
	
		
			
				|  |  | +                        EnableDirectPlay = enableDirectPlay,
 | 
	
		
			
				|  |  | +                        EnableDirectStream = enableDirectStream,
 | 
	
		
			
				|  |  | +                        ItemId = itemId,
 | 
	
		
			
				|  |  | +                        MaxAudioChannels = maxAudioChannels,
 | 
	
		
			
				|  |  | +                        MaxStreamingBitrate = maxStreamingBitrate,
 | 
	
		
			
				|  |  | +                        PlaySessionId = info.PlaySessionId,
 | 
	
		
			
				|  |  | +                        StartTimeTicks = startTimeTicks,
 | 
	
		
			
				|  |  | +                        SubtitleStreamIndex = subtitleStreamIndex,
 | 
	
		
			
				|  |  | +                        UserId = 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;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Opens a media source.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <param name="openToken">The open token.</param>
 | 
	
		
			
				|  |  | +        /// <param name="userId">The user id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="playSessionId">The play session id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
 | 
	
		
			
				|  |  | +        /// <param name="startTimeTicks">The start time in ticks.</param>
 | 
	
		
			
				|  |  | +        /// <param name="audioStreamIndex">The audio stream index.</param>
 | 
	
		
			
				|  |  | +        /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
 | 
	
		
			
				|  |  | +        /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
 | 
	
		
			
				|  |  | +        /// <param name="itemId">The item id.</param>
 | 
	
		
			
				|  |  | +        /// <param name="deviceProfile">The device profile.</param>
 | 
	
		
			
				|  |  | +        /// <param name="directPlayProtocols">The direct play protocols. Default: <see cref="MediaProtocol.Http"/>.</param>
 | 
	
		
			
				|  |  | +        /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
 | 
	
		
			
				|  |  | +        /// <response code="200">Media source opened.</response>
 | 
	
		
			
				|  |  | +        /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
 | 
	
		
			
				|  |  | +        [HttpPost("/LiveStreams/Open")]
 | 
	
		
			
				|  |  | +        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
	
		
			
				|  |  | +        public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
 | 
	
		
			
				|  |  | +            [FromQuery] string openToken,
 | 
	
		
			
				|  |  | +            [FromQuery] Guid userId,
 | 
	
		
			
				|  |  | +            [FromQuery] string playSessionId,
 | 
	
		
			
				|  |  | +            [FromQuery] long? maxStreamingBitrate,
 | 
	
		
			
				|  |  | +            [FromQuery] long? startTimeTicks,
 | 
	
		
			
				|  |  | +            [FromQuery] int? audioStreamIndex,
 | 
	
		
			
				|  |  | +            [FromQuery] int? subtitleStreamIndex,
 | 
	
		
			
				|  |  | +            [FromQuery] int? maxAudioChannels,
 | 
	
		
			
				|  |  | +            [FromQuery] Guid itemId,
 | 
	
		
			
				|  |  | +            [FromQuery] DeviceProfile deviceProfile,
 | 
	
		
			
				|  |  | +            [FromQuery] MediaProtocol[] directPlayProtocols,
 | 
	
		
			
				|  |  | +            [FromQuery] bool enableDirectPlay = true,
 | 
	
		
			
				|  |  | +            [FromQuery] bool enableDirectStream = true)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            var request = new LiveStreamRequest
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                OpenToken = openToken,
 | 
	
		
			
				|  |  | +                UserId = userId,
 | 
	
		
			
				|  |  | +                PlaySessionId = playSessionId,
 | 
	
		
			
				|  |  | +                MaxStreamingBitrate = maxStreamingBitrate,
 | 
	
		
			
				|  |  | +                StartTimeTicks = startTimeTicks,
 | 
	
		
			
				|  |  | +                AudioStreamIndex = audioStreamIndex,
 | 
	
		
			
				|  |  | +                SubtitleStreamIndex = subtitleStreamIndex,
 | 
	
		
			
				|  |  | +                MaxAudioChannels = maxAudioChannels,
 | 
	
		
			
				|  |  | +                ItemId = itemId,
 | 
	
		
			
				|  |  | +                DeviceProfile = deviceProfile,
 | 
	
		
			
				|  |  | +                EnableDirectPlay = enableDirectPlay,
 | 
	
		
			
				|  |  | +                EnableDirectStream = enableDirectStream,
 | 
	
		
			
				|  |  | +                DirectPlayProtocols = directPlayProtocols ?? new[] { MediaProtocol.Http }
 | 
	
		
			
				|  |  | +            };
 | 
	
		
			
				|  |  | +            return await OpenMediaSource(request).ConfigureAwait(false);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Closes a media source.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <param name="liveStreamId">The livestream id.</param>
 | 
	
		
			
				|  |  | +        /// <response code="204">Livestream closed.</response>
 | 
	
		
			
				|  |  | +        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
 | 
	
		
			
				|  |  | +        [HttpPost("/LiveStreams/Close")]
 | 
	
		
			
				|  |  | +        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
	
		
			
				|  |  | +        public ActionResult CloseLiveStream([FromQuery] string liveStreamId)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
 | 
	
		
			
				|  |  | +            return NoContent();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Tests the network with a request with the size of the bitrate.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <param name="size">The bitrate. Defaults to 102400.</param>
 | 
	
		
			
				|  |  | +        /// <response code="200">Test buffer returned.</response>
 | 
	
		
			
				|  |  | +        /// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
 | 
	
		
			
				|  |  | +        /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
 | 
	
		
			
				|  |  | +        [HttpGet("/Playback/BitrateTest")]
 | 
	
		
			
				|  |  | +        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
	
		
			
				|  |  | +        [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
	
		
			
				|  |  | +        [Produces(MediaTypeNames.Application.Octet)]
 | 
	
		
			
				|  |  | +        public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            const int MaxSize = 10_000_000;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (size <= 0)
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                return BadRequest($"The requested size ({size}) is equal to or smaller than 0.");
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (size > MaxSize)
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).");
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
 | 
	
		
			
				|  |  | +            try
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                new Random().NextBytes(buffer);
 | 
	
		
			
				|  |  | +                return File(buffer, MediaTypeNames.Application.Octet);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            finally
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                ArrayPool<byte>.Shared.Return(buffer);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
 | 
	
		
			
				|  |  | +            Guid id,
 | 
	
		
			
				|  |  | +            Guid userId,
 | 
	
		
			
				|  |  | +            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 (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();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 |