< Summary - Jellyfin

Information
Class: Jellyfin.Api.Helpers.MediaInfoHelper
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Helpers/MediaInfoHelper.cs
Line coverage
24%
Covered lines: 55
Uncovered lines: 166
Coverable lines: 221
Total lines: 527
Line coverage: 24.8%
Branch coverage
1%
Covered branches: 2
Total branches: 102
Branch coverage: 1.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/6/2026 - 12:14:09 AM Line coverage: 5.5% (9/161) Branch coverage: 0% (0/76) Total lines: 5184/19/2026 - 12:14:27 AM Line coverage: 4.1% (9/216) Branch coverage: 0% (0/100) Total lines: 5186/9/2026 - 12:16:23 AM Line coverage: 24.8% (55/221) Branch coverage: 1.9% (2/102) Total lines: 527 3/6/2026 - 12:14:09 AM Line coverage: 5.5% (9/161) Branch coverage: 0% (0/76) Total lines: 5184/19/2026 - 12:14:27 AM Line coverage: 4.1% (9/216) Branch coverage: 0% (0/100) Total lines: 5186/9/2026 - 12:16:23 AM Line coverage: 24.8% (55/221) Branch coverage: 1.9% (2/102) Total lines: 527

Coverage delta

Coverage delta 21 -21

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPlaybackInfo()0%210140%
SetDeviceSpecificData(...)0%3660600%
SortMediaSources(...)100%22100%
OpenMediaSource()0%110100%
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    {
 869        _userManager = userManager;
 870        _libraryManager = libraryManager;
 871        _mediaSourceManager = mediaSourceManager;
 872        _mediaEncoder = mediaEncoder;
 873        _serverConfigurationManager = serverConfigurationManager;
 874        _logger = logger;
 875        _networkManager = networkManager;
 876        _deviceManager = deviceManager;
 877    }
 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    {
 093        var result = new PlaybackInfoResponse();
 94
 95        MediaSourceInfo[] mediaSources;
 096        if (string.IsNullOrWhiteSpace(liveStreamId))
 97        {
 98            // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
 099            var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, Cancellatio
 100
 0101            if (string.IsNullOrWhiteSpace(mediaSourceId))
 102            {
 0103                mediaSources = mediaSourcesList.ToArray();
 104            }
 105            else
 106            {
 0107                mediaSources = mediaSourcesList
 0108                    .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
 0109                    .ToArray();
 110            }
 111        }
 112        else
 113        {
 0114            var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwa
 115
 0116            mediaSources = new[] { mediaSource };
 117        }
 118
 0119        if (mediaSources.Length == 0)
 120        {
 0121            result.MediaSources = Array.Empty<MediaSourceInfo>();
 122
 0123            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?
 0129            var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(me
 0130            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
 0134                for (int i = 0; i < mediaSourcesClone.Length && i < mediaSources.Length; i++)
 135                {
 0136                    mediaSourcesClone[i].DefaultAudioIndexSource = mediaSources[i].DefaultAudioIndexSource;
 137                }
 138
 0139                result.MediaSources = mediaSourcesClone;
 140            }
 141
 0142            result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 143        }
 144
 0145        return result;
 0146    }
 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    /// <param name="preferredItemId">The id of the queried item, whose own media source must stay the default.</param>
 355    public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate, Guid preferredItemId = default)
 356    {
 3357        var originalList = result.MediaSources.ToList();
 358
 359        // The queried item's source carries the user's resume state for that version, so it must stay the
 360        // default the client plays. An unfavorable bitrate means transcoding it, not switching to a sibling version.
 3361        var preferredId = preferredItemId.IsEmpty()
 3362            ? null
 3363            : preferredItemId.ToString("N", CultureInfo.InvariantCulture);
 364
 3365        result.MediaSources = result.MediaSources
 3366            .OrderByDescending(i => preferredId is not null && string.Equals(i.Id, preferredId, StringComparison.Ordinal
 3367            .ThenBy(i =>
 3368            {
 3369                // Nothing beats direct playing a file
 3370                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
 3371                {
 3372                    return 0;
 3373                }
 3374
 3375                return 1;
 3376            })
 3377            .ThenBy(i =>
 3378            {
 3379                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 3380                if (i.SupportsDirectPlay || i.SupportsDirectStream)
 3381                {
 3382                    return 0;
 3383                }
 3384
 3385                return 1;
 3386            })
 3387            .ThenBy(i =>
 3388            {
 3389                return i.Protocol switch
 3390                {
 3391                    MediaProtocol.File => 0,
 3392                    _ => 1,
 3393                };
 3394            })
 3395            .ThenBy(i =>
 3396            {
 3397                if (maxBitrate.HasValue && i.Bitrate.HasValue)
 3398                {
 3399                    return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
 3400                }
 3401
 3402                return 1;
 3403            })
 3404            .ThenBy(originalList.IndexOf)
 3405            .ToArray();
 3406    }
 407
 408    /// <summary>
 409    /// Open media source.
 410    /// </summary>
 411    /// <param name="httpContext">Http Context.</param>
 412    /// <param name="request">Live stream request.</param>
 413    /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
 414    public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
 415    {
 0416        var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
 417
 0418        var profile = request.DeviceProfile;
 0419        if (profile is null)
 420        {
 0421            var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
 0422            if (clientCapabilities is not null)
 423            {
 0424                profile = clientCapabilities.DeviceProfile;
 425            }
 426        }
 427
 0428        if (profile is not null)
 429        {
 0430            var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
 0431                ?? throw new ResourceNotFoundException();
 432
 0433            SetDeviceSpecificData(
 0434                item,
 0435                result.MediaSource,
 0436                profile,
 0437                httpContext.User,
 0438                request.MaxStreamingBitrate,
 0439                request.StartTimeTicks ?? 0,
 0440                result.MediaSource.Id,
 0441                request.AudioStreamIndex,
 0442                request.SubtitleStreamIndex,
 0443                request.MaxAudioChannels,
 0444                request.PlaySessionId,
 0445                request.UserId,
 0446                request.EnableDirectPlay,
 0447                request.EnableDirectStream,
 0448                true,
 0449                true,
 0450                true,
 0451                request.AlwaysBurnInSubtitleWhenTranscoding,
 0452                httpContext.GetNormalizedRemoteIP());
 453        }
 454        else
 455        {
 0456            if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
 457            {
 0458                result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
 459            }
 460        }
 461
 462        // here was a check if (result.MediaSource is not null) but Rider said it will never be null
 0463        NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
 464
 0465        return result;
 0466    }
 467
 468    /// <summary>
 469    /// Normalize media source container.
 470    /// </summary>
 471    /// <param name="mediaSource">Media source.</param>
 472    /// <param name="profile">Device profile.</param>
 473    /// <param name="type">Dlna profile type.</param>
 474    public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
 475    {
 0476        mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profi
 0477    }
 478
 479    private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
 480    {
 0481        var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
 0482        mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
 483
 0484        mediaSource.TranscodeReasons = info.TranscodeReasons;
 485
 0486        foreach (var profile in profiles)
 487        {
 0488            foreach (var stream in mediaSource.MediaStreams)
 489            {
 0490                if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
 491                {
 0492                    stream.DeliveryMethod = profile.DeliveryMethod;
 493
 0494                    if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
 495                    {
 0496                        stream.DeliveryUrl = profile.Url.TrimStart('-');
 0497                        stream.IsExternalUrl = profile.IsExternalUrl;
 498                    }
 499                }
 500            }
 501        }
 0502    }
 503
 504    private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
 505    {
 0506        var maxBitrate = clientMaxBitrate;
 0507        var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
 508
 0509        if (remoteClientMaxBitrate <= 0)
 510        {
 0511            remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
 512        }
 513
 0514        if (remoteClientMaxBitrate > 0)
 515        {
 0516            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
 517
 0518            _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMa
 0519            if (!isInLocalNetwork)
 520            {
 0521                maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
 522            }
 523        }
 524
 0525        return maxBitrate;
 526    }
 527}