< 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: 518
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                // Carry over the default audio index source.
 133                // This field is not intended to be exposed to API clients, but it is used internally by the server
 134                for (int i = 0; i < mediaSourcesClone.Length && i < mediaSources.Length; i++)
 135                {
 136                    mediaSourcesClone[i].DefaultAudioIndexSource = mediaSources[i].DefaultAudioIndexSource;
 137                }
 138
 139                result.MediaSources = mediaSourcesClone;
 140            }
 141
 142            result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 143        }
 144
 145        return result;
 146    }
 147
 148    /// <summary>
 149    /// SetDeviceSpecificData.
 150    /// </summary>
 151    /// <param name="item">Item to set data for.</param>
 152    /// <param name="mediaSource">Media source info.</param>
 153    /// <param name="profile">Device profile.</param>
 154    /// <param name="claimsPrincipal">Current claims principal.</param>
 155    /// <param name="maxBitrate">Max bitrate.</param>
 156    /// <param name="startTimeTicks">Start time ticks.</param>
 157    /// <param name="mediaSourceId">Media source id.</param>
 158    /// <param name="audioStreamIndex">Audio stream index.</param>
 159    /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
 160    /// <param name="maxAudioChannels">Max audio channels.</param>
 161    /// <param name="playSessionId">Play session id.</param>
 162    /// <param name="userId">User id.</param>
 163    /// <param name="enableDirectPlay">Enable direct play.</param>
 164    /// <param name="enableDirectStream">Enable direct stream.</param>
 165    /// <param name="enableTranscoding">Enable transcoding.</param>
 166    /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
 167    /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
 168    /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
 169    /// <param name="ipAddress">Requesting IP address.</param>
 170    public void SetDeviceSpecificData(
 171        BaseItem item,
 172        MediaSourceInfo mediaSource,
 173        DeviceProfile profile,
 174        ClaimsPrincipal claimsPrincipal,
 175        int? maxBitrate,
 176        long startTimeTicks,
 177        string mediaSourceId,
 178        int? audioStreamIndex,
 179        int? subtitleStreamIndex,
 180        int? maxAudioChannels,
 181        string playSessionId,
 182        Guid userId,
 183        bool enableDirectPlay,
 184        bool enableDirectStream,
 185        bool enableTranscoding,
 186        bool allowVideoStreamCopy,
 187        bool allowAudioStreamCopy,
 188        bool alwaysBurnInSubtitleWhenTranscoding,
 189        IPAddress ipAddress)
 190    {
 0191        var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
 192
 0193        var options = new MediaOptions
 0194        {
 0195            MediaSources = new[] { mediaSource },
 0196            Context = EncodingContext.Streaming,
 0197            DeviceId = claimsPrincipal.GetDeviceId(),
 0198            ItemId = item.Id,
 0199            Profile = profile,
 0200            MaxAudioChannels = maxAudioChannels,
 0201            AllowAudioStreamCopy = allowAudioStreamCopy,
 0202            AllowVideoStreamCopy = allowVideoStreamCopy,
 0203            AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding,
 0204        };
 205
 0206        if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
 207        {
 0208            options.MediaSourceId = mediaSourceId;
 0209            options.AudioStreamIndex = audioStreamIndex;
 0210            options.SubtitleStreamIndex = subtitleStreamIndex;
 211        }
 212
 0213        var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
 214
 0215        if (!enableDirectPlay)
 216        {
 0217            mediaSource.SupportsDirectPlay = false;
 218        }
 219
 0220        if (!enableDirectStream || !allowVideoStreamCopy)
 221        {
 0222            mediaSource.SupportsDirectStream = false;
 223        }
 224
 0225        if (!enableTranscoding)
 226        {
 0227            mediaSource.SupportsTranscoding = false;
 228        }
 229
 0230        if (item is Audio)
 231        {
 0232            _logger.LogInformation(
 0233                "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
 0234                user.Username,
 0235                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 236        }
 237        else
 238        {
 0239            _logger.LogInformation(
 0240                "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybac
 0241                user.Username,
 0242                user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 0243                user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 0244                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 245        }
 246
 0247        options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
 248
 0249        if (!options.ForceDirectStream)
 250        {
 251            // direct-stream http streaming is currently broken
 0252            options.EnableDirectStream = false;
 253        }
 254
 255        // Beginning of Playback Determination
 0256        var streamInfo = item.MediaType == MediaType.Audio
 0257            ? streamBuilder.GetOptimalAudioStream(options)
 0258            : streamBuilder.GetOptimalVideoStream(options);
 259
 0260        if (streamInfo is not null)
 261        {
 0262            streamInfo.PlaySessionId = playSessionId;
 0263            streamInfo.StartPositionTicks = startTimeTicks;
 264
 0265            mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
 266
 267            // Players do not handle this being set according to PlayMethod
 0268            mediaSource.SupportsDirectStream =
 0269                options.EnableDirectStream
 0270                    ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
 0271                    : streamInfo.PlayMethod == PlayMethod.DirectPlay;
 272
 0273            mediaSource.SupportsTranscoding =
 0274                streamInfo.PlayMethod == PlayMethod.DirectStream
 0275                || mediaSource.TranscodingContainer is not null
 0276                || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
 277
 0278            if (item is Audio)
 279            {
 0280                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
 281                {
 0282                    mediaSource.SupportsTranscoding = false;
 283                }
 284            }
 0285            else if (item is Video)
 286            {
 0287                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
 0288                    && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
 0289                    && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
 290                {
 0291                    mediaSource.SupportsTranscoding = false;
 292                }
 293            }
 294
 0295            if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
 296            {
 0297                mediaSource.SupportsDirectPlay = false;
 0298                mediaSource.SupportsDirectStream = false;
 299
 0300                mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), "&allowVideoStreamCopy=f
 0301                mediaSource.TranscodingContainer = streamInfo.Container;
 0302                mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
 0303                if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
 304                {
 0305                    mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
 306                }
 307            }
 308            else
 309            {
 0310                if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStr
 311                {
 0312                    streamInfo.PlayMethod = PlayMethod.Transcode;
 0313                    mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), null);
 314
 0315                    if (!allowVideoStreamCopy)
 316                    {
 0317                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
 318                    }
 319
 0320                    if (!allowAudioStreamCopy)
 321                    {
 0322                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
 323                    }
 324
 0325                    if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
 326                    {
 0327                        mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
 328                    }
 329                }
 330            }
 331
 332            // Do this after the above so that StartPositionTicks is set
 333            // The token must not be null
 0334            SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
 0335            mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
 336        }
 337
 0338        foreach (var attachment in mediaSource.MediaAttachments)
 339        {
 0340            attachment.DeliveryUrl = string.Format(
 0341                CultureInfo.InvariantCulture,
 0342                "/Videos/{0}/{1}/Attachments/{2}",
 0343                item.Id,
 0344                mediaSource.Id,
 0345                attachment.Index);
 346        }
 0347    }
 348
 349    /// <summary>
 350    /// Sort media source.
 351    /// </summary>
 352    /// <param name="result">Playback info response.</param>
 353    /// <param name="maxBitrate">Max bitrate.</param>
 354    public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
 355    {
 0356        var originalList = result.MediaSources.ToList();
 357
 0358        result.MediaSources = result.MediaSources.OrderBy(i =>
 0359            {
 0360                // Nothing beats direct playing a file
 0361                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
 0362                {
 0363                    return 0;
 0364                }
 0365
 0366                return 1;
 0367            })
 0368            .ThenBy(i =>
 0369            {
 0370                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 0371                if (i.SupportsDirectPlay || i.SupportsDirectStream)
 0372                {
 0373                    return 0;
 0374                }
 0375
 0376                return 1;
 0377            })
 0378            .ThenBy(i =>
 0379            {
 0380                return i.Protocol switch
 0381                {
 0382                    MediaProtocol.File => 0,
 0383                    _ => 1,
 0384                };
 0385            })
 0386            .ThenBy(i =>
 0387            {
 0388                if (maxBitrate.HasValue && i.Bitrate.HasValue)
 0389                {
 0390                    return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
 0391                }
 0392
 0393                return 1;
 0394            })
 0395            .ThenBy(originalList.IndexOf)
 0396            .ToArray();
 0397    }
 398
 399    /// <summary>
 400    /// Open media source.
 401    /// </summary>
 402    /// <param name="httpContext">Http Context.</param>
 403    /// <param name="request">Live stream request.</param>
 404    /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
 405    public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
 406    {
 407        var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
 408
 409        var profile = request.DeviceProfile;
 410        if (profile is null)
 411        {
 412            var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
 413            if (clientCapabilities is not null)
 414            {
 415                profile = clientCapabilities.DeviceProfile;
 416            }
 417        }
 418
 419        if (profile is not null)
 420        {
 421            var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
 422                ?? throw new ResourceNotFoundException();
 423
 424            SetDeviceSpecificData(
 425                item,
 426                result.MediaSource,
 427                profile,
 428                httpContext.User,
 429                request.MaxStreamingBitrate,
 430                request.StartTimeTicks ?? 0,
 431                result.MediaSource.Id,
 432                request.AudioStreamIndex,
 433                request.SubtitleStreamIndex,
 434                request.MaxAudioChannels,
 435                request.PlaySessionId,
 436                request.UserId,
 437                request.EnableDirectPlay,
 438                request.EnableDirectStream,
 439                true,
 440                true,
 441                true,
 442                request.AlwaysBurnInSubtitleWhenTranscoding,
 443                httpContext.GetNormalizedRemoteIP());
 444        }
 445        else
 446        {
 447            if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
 448            {
 449                result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
 450            }
 451        }
 452
 453        // here was a check if (result.MediaSource is not null) but Rider said it will never be null
 454        NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
 455
 456        return result;
 457    }
 458
 459    /// <summary>
 460    /// Normalize media source container.
 461    /// </summary>
 462    /// <param name="mediaSource">Media source.</param>
 463    /// <param name="profile">Device profile.</param>
 464    /// <param name="type">Dlna profile type.</param>
 465    public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
 466    {
 0467        mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profi
 0468    }
 469
 470    private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
 471    {
 0472        var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
 0473        mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
 474
 0475        mediaSource.TranscodeReasons = info.TranscodeReasons;
 476
 0477        foreach (var profile in profiles)
 478        {
 0479            foreach (var stream in mediaSource.MediaStreams)
 480            {
 0481                if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
 482                {
 0483                    stream.DeliveryMethod = profile.DeliveryMethod;
 484
 0485                    if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
 486                    {
 0487                        stream.DeliveryUrl = profile.Url.TrimStart('-');
 0488                        stream.IsExternalUrl = profile.IsExternalUrl;
 489                    }
 490                }
 491            }
 492        }
 0493    }
 494
 495    private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
 496    {
 0497        var maxBitrate = clientMaxBitrate;
 0498        var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
 499
 0500        if (remoteClientMaxBitrate <= 0)
 501        {
 0502            remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
 503        }
 504
 0505        if (remoteClientMaxBitrate > 0)
 506        {
 0507            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
 508
 0509            _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMa
 0510            if (!isInLocalNetwork)
 511            {
 0512                maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
 513            }
 514        }
 515
 0516        return maxBitrate;
 517    }
 518}