using System; using System.Globalization; using System.Linq; using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Helpers { /// /// Media info helper. /// public class MediaInfoHelper { private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ILogger _logger; private readonly INetworkManager _networkManager; private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public MediaInfoHelper( IUserManager userManager, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerConfigurationManager serverConfigurationManager, ILogger logger, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext) { _userManager = userManager; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; _serverConfigurationManager = serverConfigurationManager; _logger = logger; _networkManager = networkManager; _deviceManager = deviceManager; _authContext = authContext; } /// /// Get playback info. /// /// Item id. /// User Id. /// Media source id. /// Live stream id. /// A containing the . public async Task GetPlaybackInfo( Guid id, Guid? userId, string? mediaSourceId = null, string? liveStreamId = null) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); 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(); 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? var mediaSourcesClone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); if (mediaSourcesClone != null) { result.MediaSources = mediaSourcesClone; } result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } return result; } /// /// SetDeviceSpecificData. /// /// Item to set data for. /// Media source info. /// Device profile. /// Authorization info. /// Max bitrate. /// Start time ticks. /// Media source id. /// Audio stream index. /// Subtitle stream index. /// Max audio channels. /// Play session id. /// User id. /// Enable direct play. /// Enable direct stream. /// Enable transcoding. /// Allow video stream copy. /// Allow audio stream copy. /// Requesting IP address. public void SetDeviceSpecificData( BaseItem item, MediaSourceInfo mediaSource, DeviceProfile profile, AuthorizationInfo auth, int? 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, IPAddress ipAddress) { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); var options = new VideoOptions { MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, DeviceId = auth.DeviceId, ItemId = item.Id, Profile = profile, MaxAudioChannels = maxAudioChannels }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) { options.MediaSourceId = mediaSourceId; options.AudioStreamIndex = audioStreamIndex; options.SubtitleStreamIndex = subtitleStreamIndex; } var user = _userManager.GetUserById(userId); if (!enableDirectPlay) { mediaSource.SupportsDirectPlay = false; } if (!enableDirectStream) { mediaSource.SupportsDirectStream = false; } if (!enableTranscoding) { mediaSource.SupportsTranscoding = false; } if (item is Audio) { _logger.LogInformation( "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Username, user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } else { _logger.LogInformation( "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", user.Username, user.HasPermission(PermissionKind.EnablePlaybackRemuxing), user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } // Beginning of Playback Determination: Attempt DirectPlay first if (mediaSource.SupportsDirectPlay) { if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { mediaSource.SupportsDirectPlay = false; } else { var supportsDirectStream = mediaSource.SupportsDirectStream; // Dummy this up to fool StreamBuilder mediaSource.SupportsDirectStream = true; options.MaxBitrate = maxBitrate; if (item is Audio) { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { options.ForceDirectPlay = true; } } else if (item is Video) { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { options.ForceDirectPlay = true; } } // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : streamBuilder.BuildVideoItem(options); if (streamInfo == null || !streamInfo.IsDirectStream) { mediaSource.SupportsDirectPlay = false; } // Set this back to what it was mediaSource.SupportsDirectStream = supportsDirectStream; if (streamInfo != null) { SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } } if (mediaSource.SupportsDirectStream) { if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { mediaSource.SupportsDirectStream = false; } else { options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); if (item is Audio) { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { options.ForceDirectStream = true; } } else if (item is Video) { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) && user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { options.ForceDirectStream = true; } } // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : streamBuilder.BuildVideoItem(options); if (streamInfo == null || !streamInfo.IsDirectStream) { mediaSource.SupportsDirectStream = false; } if (streamInfo != null) { SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } } if (mediaSource.SupportsTranscoding) { options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : streamBuilder.BuildVideoItem(options); if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { if (streamInfo != null) { streamInfo.PlaySessionId = playSessionId; streamInfo.StartPositionTicks = startTimeTicks; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; // Do this after the above so that StartPositionTicks is set SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } 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); mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } } foreach (var attachment in mediaSource.MediaAttachments) { attachment.DeliveryUrl = string.Format( CultureInfo.InvariantCulture, "/Videos/{0}/{1}/Attachments/{2}", item.Id, mediaSource.Id, attachment.Index); } } /// /// Sort media source. /// /// Playback info response. /// Max bitrate. public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) { var originalList = result.MediaSources.ToList(); result.MediaSources = result.MediaSources.OrderBy(i => { // Nothing beats direct playing a file if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) { return 0; } return 1; }) .ThenBy(i => { // Let's assume direct streaming a file is just as desirable as direct playing a remote url if (i.SupportsDirectPlay || i.SupportsDirectStream) { return 0; } return 1; }) .ThenBy(i => { return i.Protocol switch { MediaProtocol.File => 0, _ => 1, }; }) .ThenBy(i => { if (maxBitrate.HasValue && i.Bitrate.HasValue) { return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; } return 1; }) .ThenBy(originalList.IndexOf) .ToArray(); } /// /// Open media source. /// /// Http Request. /// Live stream request. /// A containing the . public async Task OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request) { var authInfo = await _authContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); var profile = request.DeviceProfile; if (profile == null) { var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId); if (clientCapabilities != null) { profile = clientCapabilities.DeviceProfile; } } if (profile != null) { var item = _libraryManager.GetItemById(request.ItemId); SetDeviceSpecificData( item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, request.EnableDirectPlay, request.EnableDirectStream, true, true, true, httpRequest.HttpContext.GetNormalizedRemoteIp()); } 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; } /// /// Normalize media source container. /// /// Media source. /// Device profile. /// Dlna profile type. public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) { mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type); } private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) { var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; mediaSource.TranscodeReasons = info.TranscodeReasons; foreach (var profile in profiles) { foreach (var stream in mediaSource.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) { stream.DeliveryMethod = profile.DeliveryMethod; if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) { stream.DeliveryUrl = profile.Url.TrimStart('-'); stream.IsExternalUrl = profile.IsExternalUrl; } } } } } private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress) { var maxBitrate = clientMaxBitrate; var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; if (remoteClientMaxBitrate <= 0) { remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; } if (remoteClientMaxBitrate > 0) { var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress); _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); if (!isInLocalNetwork) { maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); } } return maxBitrate; } } }