< 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: 152
Coverable lines: 161
Total lines: 511
Line coverage: 5.5%
Branch coverage
0%
Covered branches: 0
Total branches: 76
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%3660600%
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;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Database.Implementations.Entities;
 13using Jellyfin.Database.Implementations.Enums;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Common.Extensions;
 16using MediaBrowser.Common.Net;
 17using MediaBrowser.Controller.Configuration;
 18using MediaBrowser.Controller.Devices;
 19using MediaBrowser.Controller.Entities;
 20using MediaBrowser.Controller.Entities.Audio;
 21using MediaBrowser.Controller.Library;
 22using MediaBrowser.Controller.MediaEncoding;
 23using MediaBrowser.Model.Dlna;
 24using MediaBrowser.Model.Dto;
 25using MediaBrowser.Model.Entities;
 26using MediaBrowser.Model.MediaInfo;
 27using MediaBrowser.Model.Session;
 28using Microsoft.AspNetCore.Http;
 29using Microsoft.AspNetCore.Http.HttpResults;
 30using Microsoft.Extensions.Logging;
 31
 32namespace Jellyfin.Api.Helpers;
 33
 34/// <summary>
 35/// Media info helper.
 36/// </summary>
 37public class MediaInfoHelper
 38{
 39    private readonly IUserManager _userManager;
 40    private readonly ILibraryManager _libraryManager;
 41    private readonly IMediaSourceManager _mediaSourceManager;
 42    private readonly IMediaEncoder _mediaEncoder;
 43    private readonly IServerConfigurationManager _serverConfigurationManager;
 44    private readonly ILogger<MediaInfoHelper> _logger;
 45    private readonly INetworkManager _networkManager;
 46    private readonly IDeviceManager _deviceManager;
 47
 48    /// <summary>
 49    /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
 50    /// </summary>
 51    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 52    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 53    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 54    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 55    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 56    /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
 57    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
 58    /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
 59    public MediaInfoHelper(
 60        IUserManager userManager,
 61        ILibraryManager libraryManager,
 62        IMediaSourceManager mediaSourceManager,
 63        IMediaEncoder mediaEncoder,
 64        IServerConfigurationManager serverConfigurationManager,
 65        ILogger<MediaInfoHelper> logger,
 66        INetworkManager networkManager,
 67        IDeviceManager deviceManager)
 68    {
 569        _userManager = userManager;
 570        _libraryManager = libraryManager;
 571        _mediaSourceManager = mediaSourceManager;
 572        _mediaEncoder = mediaEncoder;
 573        _serverConfigurationManager = serverConfigurationManager;
 574        _logger = logger;
 575        _networkManager = networkManager;
 576        _deviceManager = deviceManager;
 577    }
 78
 79    /// <summary>
 80    /// Get playback info.
 81    /// </summary>
 82    /// <param name="item">The item.</param>
 83    /// <param name="user">The user.</param>
 84    /// <param name="mediaSourceId">Media source id.</param>
 85    /// <param name="liveStreamId">Live stream id.</param>
 86    /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
 87    public async Task<PlaybackInfoResponse> GetPlaybackInfo(
 88        BaseItem item,
 89        User? user,
 90        string? mediaSourceId = null,
 91        string? liveStreamId = null)
 92    {
 93        var result = new PlaybackInfoResponse();
 94
 95        MediaSourceInfo[] mediaSources;
 96        if (string.IsNullOrWhiteSpace(liveStreamId))
 97        {
 98            // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
 99            var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, Cancellatio
 100
 101            if (string.IsNullOrWhiteSpace(mediaSourceId))
 102            {
 103                mediaSources = mediaSourcesList.ToArray();
 104            }
 105            else
 106            {
 107                mediaSources = mediaSourcesList
 108                    .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
 109                    .ToArray();
 110            }
 111        }
 112        else
 113        {
 114            var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwa
 115
 116            mediaSources = new[] { mediaSource };
 117        }
 118
 119        if (mediaSources.Length == 0)
 120        {
 121            result.MediaSources = Array.Empty<MediaSourceInfo>();
 122
 123            result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
 124        }
 125        else
 126        {
 127            // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we s
 128            // Should we move this directly into MediaSourceManager?
 129            var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(me
 130            if (mediaSourcesClone is not null)
 131            {
 132                result.MediaSources = mediaSourcesClone;
 133            }
 134
 135            result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 136        }
 137
 138        return result;
 139    }
 140
 141    /// <summary>
 142    /// SetDeviceSpecificData.
 143    /// </summary>
 144    /// <param name="item">Item to set data for.</param>
 145    /// <param name="mediaSource">Media source info.</param>
 146    /// <param name="profile">Device profile.</param>
 147    /// <param name="claimsPrincipal">Current claims principal.</param>
 148    /// <param name="maxBitrate">Max bitrate.</param>
 149    /// <param name="startTimeTicks">Start time ticks.</param>
 150    /// <param name="mediaSourceId">Media source id.</param>
 151    /// <param name="audioStreamIndex">Audio stream index.</param>
 152    /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
 153    /// <param name="maxAudioChannels">Max audio channels.</param>
 154    /// <param name="playSessionId">Play session id.</param>
 155    /// <param name="userId">User id.</param>
 156    /// <param name="enableDirectPlay">Enable direct play.</param>
 157    /// <param name="enableDirectStream">Enable direct stream.</param>
 158    /// <param name="enableTranscoding">Enable transcoding.</param>
 159    /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
 160    /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
 161    /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
 162    /// <param name="ipAddress">Requesting IP address.</param>
 163    public void SetDeviceSpecificData(
 164        BaseItem item,
 165        MediaSourceInfo mediaSource,
 166        DeviceProfile profile,
 167        ClaimsPrincipal claimsPrincipal,
 168        int? maxBitrate,
 169        long startTimeTicks,
 170        string mediaSourceId,
 171        int? audioStreamIndex,
 172        int? subtitleStreamIndex,
 173        int? maxAudioChannels,
 174        string playSessionId,
 175        Guid userId,
 176        bool enableDirectPlay,
 177        bool enableDirectStream,
 178        bool enableTranscoding,
 179        bool allowVideoStreamCopy,
 180        bool allowAudioStreamCopy,
 181        bool alwaysBurnInSubtitleWhenTranscoding,
 182        IPAddress ipAddress)
 183    {
 0184        var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
 185
 0186        var options = new MediaOptions
 0187        {
 0188            MediaSources = new[] { mediaSource },
 0189            Context = EncodingContext.Streaming,
 0190            DeviceId = claimsPrincipal.GetDeviceId(),
 0191            ItemId = item.Id,
 0192            Profile = profile,
 0193            MaxAudioChannels = maxAudioChannels,
 0194            AllowAudioStreamCopy = allowAudioStreamCopy,
 0195            AllowVideoStreamCopy = allowVideoStreamCopy,
 0196            AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding,
 0197        };
 198
 0199        if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
 200        {
 0201            options.MediaSourceId = mediaSourceId;
 0202            options.AudioStreamIndex = audioStreamIndex;
 0203            options.SubtitleStreamIndex = subtitleStreamIndex;
 204        }
 205
 0206        var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
 207
 0208        if (!enableDirectPlay)
 209        {
 0210            mediaSource.SupportsDirectPlay = false;
 211        }
 212
 0213        if (!enableDirectStream || !allowVideoStreamCopy)
 214        {
 0215            mediaSource.SupportsDirectStream = false;
 216        }
 217
 0218        if (!enableTranscoding)
 219        {
 0220            mediaSource.SupportsTranscoding = false;
 221        }
 222
 0223        if (item is Audio)
 224        {
 0225            _logger.LogInformation(
 0226                "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
 0227                user.Username,
 0228                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 229        }
 230        else
 231        {
 0232            _logger.LogInformation(
 0233                "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybac
 0234                user.Username,
 0235                user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 0236                user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 0237                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 238        }
 239
 0240        options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
 241
 0242        if (!options.ForceDirectStream)
 243        {
 244            // direct-stream http streaming is currently broken
 0245            options.EnableDirectStream = false;
 246        }
 247
 248        // Beginning of Playback Determination
 0249        var streamInfo = item.MediaType == MediaType.Audio
 0250            ? streamBuilder.GetOptimalAudioStream(options)
 0251            : streamBuilder.GetOptimalVideoStream(options);
 252
 0253        if (streamInfo is not null)
 254        {
 0255            streamInfo.PlaySessionId = playSessionId;
 0256            streamInfo.StartPositionTicks = startTimeTicks;
 257
 0258            mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
 259
 260            // Players do not handle this being set according to PlayMethod
 0261            mediaSource.SupportsDirectStream =
 0262                options.EnableDirectStream
 0263                    ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
 0264                    : streamInfo.PlayMethod == PlayMethod.DirectPlay;
 265
 0266            mediaSource.SupportsTranscoding =
 0267                streamInfo.PlayMethod == PlayMethod.DirectStream
 0268                || mediaSource.TranscodingContainer is not null
 0269                || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
 270
 0271            if (item is Audio)
 272            {
 0273                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
 274                {
 0275                    mediaSource.SupportsTranscoding = false;
 276                }
 277            }
 0278            else if (item is Video)
 279            {
 0280                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
 0281                    && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
 0282                    && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
 283                {
 0284                    mediaSource.SupportsTranscoding = false;
 285                }
 286            }
 287
 0288            if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
 289            {
 0290                mediaSource.SupportsDirectPlay = false;
 0291                mediaSource.SupportsDirectStream = false;
 292
 0293                mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), "&allowVideoStreamCopy=f
 0294                mediaSource.TranscodingContainer = streamInfo.Container;
 0295                mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
 0296                if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
 297                {
 0298                    mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
 299                }
 300            }
 301            else
 302            {
 0303                if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStr
 304                {
 0305                    streamInfo.PlayMethod = PlayMethod.Transcode;
 0306                    mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), null);
 307
 0308                    if (!allowVideoStreamCopy)
 309                    {
 0310                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
 311                    }
 312
 0313                    if (!allowAudioStreamCopy)
 314                    {
 0315                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
 316                    }
 317
 0318                    if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
 319                    {
 0320                        mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
 321                    }
 322                }
 323            }
 324
 325            // Do this after the above so that StartPositionTicks is set
 326            // The token must not be null
 0327            SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
 0328            mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
 329        }
 330
 0331        foreach (var attachment in mediaSource.MediaAttachments)
 332        {
 0333            attachment.DeliveryUrl = string.Format(
 0334                CultureInfo.InvariantCulture,
 0335                "/Videos/{0}/{1}/Attachments/{2}",
 0336                item.Id,
 0337                mediaSource.Id,
 0338                attachment.Index);
 339        }
 0340    }
 341
 342    /// <summary>
 343    /// Sort media source.
 344    /// </summary>
 345    /// <param name="result">Playback info response.</param>
 346    /// <param name="maxBitrate">Max bitrate.</param>
 347    public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
 348    {
 0349        var originalList = result.MediaSources.ToList();
 350
 0351        result.MediaSources = result.MediaSources.OrderBy(i =>
 0352            {
 0353                // Nothing beats direct playing a file
 0354                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
 0355                {
 0356                    return 0;
 0357                }
 0358
 0359                return 1;
 0360            })
 0361            .ThenBy(i =>
 0362            {
 0363                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 0364                if (i.SupportsDirectPlay || i.SupportsDirectStream)
 0365                {
 0366                    return 0;
 0367                }
 0368
 0369                return 1;
 0370            })
 0371            .ThenBy(i =>
 0372            {
 0373                return i.Protocol switch
 0374                {
 0375                    MediaProtocol.File => 0,
 0376                    _ => 1,
 0377                };
 0378            })
 0379            .ThenBy(i =>
 0380            {
 0381                if (maxBitrate.HasValue && i.Bitrate.HasValue)
 0382                {
 0383                    return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
 0384                }
 0385
 0386                return 1;
 0387            })
 0388            .ThenBy(originalList.IndexOf)
 0389            .ToArray();
 0390    }
 391
 392    /// <summary>
 393    /// Open media source.
 394    /// </summary>
 395    /// <param name="httpContext">Http Context.</param>
 396    /// <param name="request">Live stream request.</param>
 397    /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
 398    public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
 399    {
 400        var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
 401
 402        var profile = request.DeviceProfile;
 403        if (profile is null)
 404        {
 405            var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
 406            if (clientCapabilities is not null)
 407            {
 408                profile = clientCapabilities.DeviceProfile;
 409            }
 410        }
 411
 412        if (profile is not null)
 413        {
 414            var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
 415                ?? throw new ResourceNotFoundException();
 416
 417            SetDeviceSpecificData(
 418                item,
 419                result.MediaSource,
 420                profile,
 421                httpContext.User,
 422                request.MaxStreamingBitrate,
 423                request.StartTimeTicks ?? 0,
 424                result.MediaSource.Id,
 425                request.AudioStreamIndex,
 426                request.SubtitleStreamIndex,
 427                request.MaxAudioChannels,
 428                request.PlaySessionId,
 429                request.UserId,
 430                request.EnableDirectPlay,
 431                request.EnableDirectStream,
 432                true,
 433                true,
 434                true,
 435                request.AlwaysBurnInSubtitleWhenTranscoding,
 436                httpContext.GetNormalizedRemoteIP());
 437        }
 438        else
 439        {
 440            if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
 441            {
 442                result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
 443            }
 444        }
 445
 446        // here was a check if (result.MediaSource is not null) but Rider said it will never be null
 447        NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
 448
 449        return result;
 450    }
 451
 452    /// <summary>
 453    /// Normalize media source container.
 454    /// </summary>
 455    /// <param name="mediaSource">Media source.</param>
 456    /// <param name="profile">Device profile.</param>
 457    /// <param name="type">Dlna profile type.</param>
 458    public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
 459    {
 0460        mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profi
 0461    }
 462
 463    private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
 464    {
 0465        var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
 0466        mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
 467
 0468        mediaSource.TranscodeReasons = info.TranscodeReasons;
 469
 0470        foreach (var profile in profiles)
 471        {
 0472            foreach (var stream in mediaSource.MediaStreams)
 473            {
 0474                if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
 475                {
 0476                    stream.DeliveryMethod = profile.DeliveryMethod;
 477
 0478                    if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
 479                    {
 0480                        stream.DeliveryUrl = profile.Url.TrimStart('-');
 0481                        stream.IsExternalUrl = profile.IsExternalUrl;
 482                    }
 483                }
 484            }
 485        }
 0486    }
 487
 488    private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
 489    {
 0490        var maxBitrate = clientMaxBitrate;
 0491        var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
 492
 0493        if (remoteClientMaxBitrate <= 0)
 494        {
 0495            remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
 496        }
 497
 0498        if (remoteClientMaxBitrate > 0)
 499        {
 0500            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
 501
 0502            _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMa
 0503            if (!isInLocalNetwork)
 504            {
 0505                maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
 506            }
 507        }
 508
 0509        return maxBitrate;
 510    }
 511}