< Summary - Jellyfin

Information
Class: Jellyfin.Api.Helpers.MediaInfoHelper
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Helpers/MediaInfoHelper.cs
Line coverage
5%
Covered lines: 9
Uncovered lines: 150
Coverable lines: 159
Total lines: 502
Line coverage: 5.6%
Branch coverage
0%
Covered branches: 0
Total branches: 72
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SetDeviceSpecificData(...)0%3192560%
SortMediaSources(...)100%210%
NormalizeMediaSourceContainer(...)100%210%
SetDeviceSpecificSubtitleInfo(...)0%110100%
GetMaxBitrate(...)0%4260%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Helpers/MediaInfoHelper.cs

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.Linq;
 4using System.Net;
 5using System.Security.Claims;
 6using System.Text.Json;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Jellyfin.Api.Extensions;
 10using Jellyfin.Data.Entities;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Common.Extensions;
 14using MediaBrowser.Common.Net;
 15using MediaBrowser.Controller.Configuration;
 16using MediaBrowser.Controller.Devices;
 17using MediaBrowser.Controller.Entities;
 18using MediaBrowser.Controller.Entities.Audio;
 19using MediaBrowser.Controller.Library;
 20using MediaBrowser.Controller.MediaEncoding;
 21using MediaBrowser.Model.Dlna;
 22using MediaBrowser.Model.Dto;
 23using MediaBrowser.Model.Entities;
 24using MediaBrowser.Model.MediaInfo;
 25using MediaBrowser.Model.Session;
 26using Microsoft.AspNetCore.Http;
 27using Microsoft.AspNetCore.Http.HttpResults;
 28using Microsoft.Extensions.Logging;
 29
 30namespace Jellyfin.Api.Helpers;
 31
 32/// <summary>
 33/// Media info helper.
 34/// </summary>
 35public class MediaInfoHelper
 36{
 37    private readonly IUserManager _userManager;
 38    private readonly ILibraryManager _libraryManager;
 39    private readonly IMediaSourceManager _mediaSourceManager;
 40    private readonly IMediaEncoder _mediaEncoder;
 41    private readonly IServerConfigurationManager _serverConfigurationManager;
 42    private readonly ILogger<MediaInfoHelper> _logger;
 43    private readonly INetworkManager _networkManager;
 44    private readonly IDeviceManager _deviceManager;
 45
 46    /// <summary>
 47    /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
 48    /// </summary>
 49    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 50    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 51    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 52    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 53    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 54    /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
 55    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
 56    /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
 57    public MediaInfoHelper(
 58        IUserManager userManager,
 59        ILibraryManager libraryManager,
 60        IMediaSourceManager mediaSourceManager,
 61        IMediaEncoder mediaEncoder,
 62        IServerConfigurationManager serverConfigurationManager,
 63        ILogger<MediaInfoHelper> logger,
 64        INetworkManager networkManager,
 65        IDeviceManager deviceManager)
 66    {
 567        _userManager = userManager;
 568        _libraryManager = libraryManager;
 569        _mediaSourceManager = mediaSourceManager;
 570        _mediaEncoder = mediaEncoder;
 571        _serverConfigurationManager = serverConfigurationManager;
 572        _logger = logger;
 573        _networkManager = networkManager;
 574        _deviceManager = deviceManager;
 575    }
 76
 77    /// <summary>
 78    /// Get playback info.
 79    /// </summary>
 80    /// <param name="item">The item.</param>
 81    /// <param name="user">The user.</param>
 82    /// <param name="mediaSourceId">Media source id.</param>
 83    /// <param name="liveStreamId">Live stream id.</param>
 84    /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
 85    public async Task<PlaybackInfoResponse> GetPlaybackInfo(
 86        BaseItem item,
 87        User? user,
 88        string? mediaSourceId = null,
 89        string? liveStreamId = null)
 90    {
 91        var result = new PlaybackInfoResponse();
 92
 93        MediaSourceInfo[] mediaSources;
 94        if (string.IsNullOrWhiteSpace(liveStreamId))
 95        {
 96            // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
 97            var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, Cancellatio
 98
 99            if (string.IsNullOrWhiteSpace(mediaSourceId))
 100            {
 101                mediaSources = mediaSourcesList.ToArray();
 102            }
 103            else
 104            {
 105                mediaSources = mediaSourcesList
 106                    .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
 107                    .ToArray();
 108            }
 109        }
 110        else
 111        {
 112            var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwa
 113
 114            mediaSources = new[] { mediaSource };
 115        }
 116
 117        if (mediaSources.Length == 0)
 118        {
 119            result.MediaSources = Array.Empty<MediaSourceInfo>();
 120
 121            result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
 122        }
 123        else
 124        {
 125            // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we s
 126            // Should we move this directly into MediaSourceManager?
 127            var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(me
 128            if (mediaSourcesClone is not null)
 129            {
 130                result.MediaSources = mediaSourcesClone;
 131            }
 132
 133            result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 134        }
 135
 136        return result;
 137    }
 138
 139    /// <summary>
 140    /// SetDeviceSpecificData.
 141    /// </summary>
 142    /// <param name="item">Item to set data for.</param>
 143    /// <param name="mediaSource">Media source info.</param>
 144    /// <param name="profile">Device profile.</param>
 145    /// <param name="claimsPrincipal">Current claims principal.</param>
 146    /// <param name="maxBitrate">Max bitrate.</param>
 147    /// <param name="startTimeTicks">Start time ticks.</param>
 148    /// <param name="mediaSourceId">Media source id.</param>
 149    /// <param name="audioStreamIndex">Audio stream index.</param>
 150    /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
 151    /// <param name="maxAudioChannels">Max audio channels.</param>
 152    /// <param name="playSessionId">Play session id.</param>
 153    /// <param name="userId">User id.</param>
 154    /// <param name="enableDirectPlay">Enable direct play.</param>
 155    /// <param name="enableDirectStream">Enable direct stream.</param>
 156    /// <param name="enableTranscoding">Enable transcoding.</param>
 157    /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
 158    /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
 159    /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
 160    /// <param name="ipAddress">Requesting IP address.</param>
 161    public void SetDeviceSpecificData(
 162        BaseItem item,
 163        MediaSourceInfo mediaSource,
 164        DeviceProfile profile,
 165        ClaimsPrincipal claimsPrincipal,
 166        int? maxBitrate,
 167        long startTimeTicks,
 168        string mediaSourceId,
 169        int? audioStreamIndex,
 170        int? subtitleStreamIndex,
 171        int? maxAudioChannels,
 172        string playSessionId,
 173        Guid userId,
 174        bool enableDirectPlay,
 175        bool enableDirectStream,
 176        bool enableTranscoding,
 177        bool allowVideoStreamCopy,
 178        bool allowAudioStreamCopy,
 179        bool alwaysBurnInSubtitleWhenTranscoding,
 180        IPAddress ipAddress)
 181    {
 0182        var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
 183
 0184        var options = new MediaOptions
 0185        {
 0186            MediaSources = new[] { mediaSource },
 0187            Context = EncodingContext.Streaming,
 0188            DeviceId = claimsPrincipal.GetDeviceId(),
 0189            ItemId = item.Id,
 0190            Profile = profile,
 0191            MaxAudioChannels = maxAudioChannels,
 0192            AllowAudioStreamCopy = allowAudioStreamCopy,
 0193            AllowVideoStreamCopy = allowVideoStreamCopy,
 0194            AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding,
 0195        };
 196
 0197        if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
 198        {
 0199            options.MediaSourceId = mediaSourceId;
 0200            options.AudioStreamIndex = audioStreamIndex;
 0201            options.SubtitleStreamIndex = subtitleStreamIndex;
 202        }
 203
 0204        var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
 205
 0206        if (!enableDirectPlay)
 207        {
 0208            mediaSource.SupportsDirectPlay = false;
 209        }
 210
 0211        if (!enableDirectStream || !allowVideoStreamCopy)
 212        {
 0213            mediaSource.SupportsDirectStream = false;
 214        }
 215
 0216        if (!enableTranscoding)
 217        {
 0218            mediaSource.SupportsTranscoding = false;
 219        }
 220
 0221        if (item is Audio)
 222        {
 0223            _logger.LogInformation(
 0224                "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
 0225                user.Username,
 0226                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 227        }
 228        else
 229        {
 0230            _logger.LogInformation(
 0231                "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybac
 0232                user.Username,
 0233                user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 0234                user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 0235                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 236        }
 237
 0238        options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
 239
 0240        if (!options.ForceDirectStream)
 241        {
 242            // direct-stream http streaming is currently broken
 0243            options.EnableDirectStream = false;
 244        }
 245
 246        // Beginning of Playback Determination
 0247        var streamInfo = item.MediaType == MediaType.Audio
 0248            ? streamBuilder.GetOptimalAudioStream(options)
 0249            : streamBuilder.GetOptimalVideoStream(options);
 250
 0251        if (streamInfo is not null)
 252        {
 0253            streamInfo.PlaySessionId = playSessionId;
 0254            streamInfo.StartPositionTicks = startTimeTicks;
 255
 0256            mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
 257
 258            // Players do not handle this being set according to PlayMethod
 0259            mediaSource.SupportsDirectStream =
 0260                options.EnableDirectStream
 0261                    ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
 0262                    : streamInfo.PlayMethod == PlayMethod.DirectPlay;
 263
 0264            mediaSource.SupportsTranscoding =
 0265                streamInfo.PlayMethod == PlayMethod.DirectStream
 0266                || mediaSource.TranscodingContainer is not null
 0267                || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
 268
 0269            if (item is Audio)
 270            {
 0271                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
 272                {
 0273                    mediaSource.SupportsTranscoding = false;
 274                }
 275            }
 0276            else if (item is Video)
 277            {
 0278                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
 0279                    && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
 0280                    && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
 281                {
 0282                    mediaSource.SupportsTranscoding = false;
 283                }
 284            }
 285
 0286            if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
 287            {
 0288                mediaSource.SupportsDirectPlay = false;
 0289                mediaSource.SupportsDirectStream = false;
 290
 0291                mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
 0292                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
 0293                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
 0294                mediaSource.TranscodingContainer = streamInfo.Container;
 0295                mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
 296            }
 297            else
 298            {
 0299                if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStr
 300                {
 0301                    streamInfo.PlayMethod = PlayMethod.Transcode;
 0302                    mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
 303
 0304                    if (!allowVideoStreamCopy)
 305                    {
 0306                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
 307                    }
 308
 0309                    if (!allowAudioStreamCopy)
 310                    {
 0311                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
 312                    }
 313                }
 314            }
 315
 316            // Do this after the above so that StartPositionTicks is set
 317            // The token must not be null
 0318            SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
 0319            mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
 320        }
 321
 0322        foreach (var attachment in mediaSource.MediaAttachments)
 323        {
 0324            attachment.DeliveryUrl = string.Format(
 0325                CultureInfo.InvariantCulture,
 0326                "/Videos/{0}/{1}/Attachments/{2}",
 0327                item.Id,
 0328                mediaSource.Id,
 0329                attachment.Index);
 330        }
 0331    }
 332
 333    /// <summary>
 334    /// Sort media source.
 335    /// </summary>
 336    /// <param name="result">Playback info response.</param>
 337    /// <param name="maxBitrate">Max bitrate.</param>
 338    public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
 339    {
 0340        var originalList = result.MediaSources.ToList();
 341
 0342        result.MediaSources = result.MediaSources.OrderBy(i =>
 0343            {
 0344                // Nothing beats direct playing a file
 0345                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
 0346                {
 0347                    return 0;
 0348                }
 0349
 0350                return 1;
 0351            })
 0352            .ThenBy(i =>
 0353            {
 0354                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 0355                if (i.SupportsDirectPlay || i.SupportsDirectStream)
 0356                {
 0357                    return 0;
 0358                }
 0359
 0360                return 1;
 0361            })
 0362            .ThenBy(i =>
 0363            {
 0364                return i.Protocol switch
 0365                {
 0366                    MediaProtocol.File => 0,
 0367                    _ => 1,
 0368                };
 0369            })
 0370            .ThenBy(i =>
 0371            {
 0372                if (maxBitrate.HasValue && i.Bitrate.HasValue)
 0373                {
 0374                    return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
 0375                }
 0376
 0377                return 1;
 0378            })
 0379            .ThenBy(originalList.IndexOf)
 0380            .ToArray();
 0381    }
 382
 383    /// <summary>
 384    /// Open media source.
 385    /// </summary>
 386    /// <param name="httpContext">Http Context.</param>
 387    /// <param name="request">Live stream request.</param>
 388    /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
 389    public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
 390    {
 391        var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
 392
 393        var profile = request.DeviceProfile;
 394        if (profile is null)
 395        {
 396            var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
 397            if (clientCapabilities is not null)
 398            {
 399                profile = clientCapabilities.DeviceProfile;
 400            }
 401        }
 402
 403        if (profile is not null)
 404        {
 405            var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
 406                ?? throw new ResourceNotFoundException();
 407
 408            SetDeviceSpecificData(
 409                item,
 410                result.MediaSource,
 411                profile,
 412                httpContext.User,
 413                request.MaxStreamingBitrate,
 414                request.StartTimeTicks ?? 0,
 415                result.MediaSource.Id,
 416                request.AudioStreamIndex,
 417                request.SubtitleStreamIndex,
 418                request.MaxAudioChannels,
 419                request.PlaySessionId,
 420                request.UserId,
 421                request.EnableDirectPlay,
 422                request.EnableDirectStream,
 423                true,
 424                true,
 425                true,
 426                request.AlwaysBurnInSubtitleWhenTranscoding,
 427                httpContext.GetNormalizedRemoteIP());
 428        }
 429        else
 430        {
 431            if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
 432            {
 433                result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
 434            }
 435        }
 436
 437        // here was a check if (result.MediaSource is not null) but Rider said it will never be null
 438        NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
 439
 440        return result;
 441    }
 442
 443    /// <summary>
 444    /// Normalize media source container.
 445    /// </summary>
 446    /// <param name="mediaSource">Media source.</param>
 447    /// <param name="profile">Device profile.</param>
 448    /// <param name="type">Dlna profile type.</param>
 449    public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
 450    {
 0451        mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profi
 0452    }
 453
 454    private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
 455    {
 0456        var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
 0457        mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
 458
 0459        mediaSource.TranscodeReasons = info.TranscodeReasons;
 460
 0461        foreach (var profile in profiles)
 462        {
 0463            foreach (var stream in mediaSource.MediaStreams)
 464            {
 0465                if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
 466                {
 0467                    stream.DeliveryMethod = profile.DeliveryMethod;
 468
 0469                    if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
 470                    {
 0471                        stream.DeliveryUrl = profile.Url.TrimStart('-');
 0472                        stream.IsExternalUrl = profile.IsExternalUrl;
 473                    }
 474                }
 475            }
 476        }
 0477    }
 478
 479    private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
 480    {
 0481        var maxBitrate = clientMaxBitrate;
 0482        var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
 483
 0484        if (remoteClientMaxBitrate <= 0)
 485        {
 0486            remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
 487        }
 488
 0489        if (remoteClientMaxBitrate > 0)
 490        {
 0491            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
 492
 0493            _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMa
 0494            if (!isInLocalNetwork)
 495            {
 0496                maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
 497            }
 498        }
 499
 0500        return maxBitrate;
 501    }
 502}