using System; using System.Globalization; using System.Linq; using System.Net; using System.Security.Claims; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; 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.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; /// /// 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. public MediaInfoHelper( IUserManager userManager, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerConfigurationManager serverConfigurationManager, ILogger logger, INetworkManager networkManager, IDeviceManager deviceManager) { _userManager = userManager; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; _serverConfigurationManager = serverConfigurationManager; _logger = logger; _networkManager = networkManager; _deviceManager = deviceManager; } /// /// 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 is not 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. /// Current claims principal. /// 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, ClaimsPrincipal claimsPrincipal, 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 MediaOptions { MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, DeviceId = claimsPrincipal.GetDeviceId(), ItemId = item.Id, Profile = profile, MaxAudioChannels = maxAudioChannels, AllowAudioStreamCopy = allowAudioStreamCopy, AllowVideoStreamCopy = allowVideoStreamCopy }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) { options.MediaSourceId = mediaSourceId; options.AudioStreamIndex = audioStreamIndex; options.SubtitleStreamIndex = subtitleStreamIndex; } var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); if (!enableDirectPlay) { mediaSource.SupportsDirectPlay = false; } if (!enableDirectStream || !allowVideoStreamCopy) { 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)); } options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); if (!options.ForceDirectStream) { // direct-stream http streaming is currently broken options.EnableDirectStream = false; } // Beginning of Playback Determination var streamInfo = item.MediaType == MediaType.Audio ? streamBuilder.GetOptimalAudioStream(options) : streamBuilder.GetOptimalVideoStream(options); if (streamInfo is not null) { streamInfo.PlaySessionId = playSessionId; streamInfo.StartPositionTicks = startTimeTicks; mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; // Players do not handle this being set according to PlayMethod mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer is not null || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context); if (item is Audio) { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { mediaSource.SupportsTranscoding = false; } } else if (item is Video) { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { mediaSource.SupportsTranscoding = false; } } if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { mediaSource.SupportsDirectPlay = false; mediaSource.SupportsDirectStream = false; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; } else { if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)) { streamInfo.PlayMethod = PlayMethod.Transcode; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); if (!allowVideoStreamCopy) { mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; } if (!allowAudioStreamCopy) { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } } } // Do this after the above so that StartPositionTicks is set // The token must not be null SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!); 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 Context. /// Live stream request. /// A containing the . public async Task OpenMediaSource(HttpContext httpContext, LiveStreamRequest request) { var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); var profile = request.DeviceProfile; if (profile is null) { var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId()); if (clientCapabilities is not null) { profile = clientCapabilities.DeviceProfile; } } if (profile is not null) { var item = _libraryManager.GetItemById(request.ItemId); SetDeviceSpecificData( item, result.MediaSource, profile, httpContext.User, 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, httpContext.GetNormalizedRemoteIP()); } else { if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) { result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; } } // here was a check if (result.MediaSource is not 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; } }