< Summary - Jellyfin

Information
Class: Jellyfin.Api.Helpers.MediaInfoHelper
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Helpers/MediaInfoHelper.cs
Line coverage
44%
Covered lines: 115
Uncovered lines: 142
Coverable lines: 257
Total lines: 627
Line coverage: 44.7%
Branch coverage
28%
Covered branches: 37
Total branches: 128
Branch coverage: 28.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/6/2026 - 12:15:23 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: 5278/3/2026 - 12:16:46 AM Line coverage: 44.7% (115/257) Branch coverage: 28.9% (37/128) Total lines: 627 5/6/2026 - 12:15:23 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: 5278/3/2026 - 12:16:46 AM Line coverage: 44.7% (115/257) Branch coverage: 28.9% (37/128) Total lines: 627

Coverage delta

Coverage delta 27 -27

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPlaybackInfo()83.33%121286.66%
ResolvePlaybackMediaSources()75%4470%
SetDeviceSpecificData(...)0%3660600%
SortMediaSources(...)100%22100%
OpenMediaSource()50%421031.42%
NormalizeMediaSourceContainer(...)100%11100%
SetDeviceSpecificSubtitleInfo(...)0%110100%
GetMaxBitrate(...)0%4260%
RewritePublishedLiveStreamPath(...)37.5%10870%
GetPublishedLiveStreamPath(...)87.5%161695%

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;
 18using MediaBrowser.Controller.Configuration;
 19using MediaBrowser.Controller.Devices;
 20using MediaBrowser.Controller.Entities;
 21using MediaBrowser.Controller.Entities.Audio;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.MediaEncoding;
 24using MediaBrowser.Model.Dlna;
 25using MediaBrowser.Model.Dto;
 26using MediaBrowser.Model.Entities;
 27using MediaBrowser.Model.MediaInfo;
 28using MediaBrowser.Model.Session;
 29using Microsoft.AspNetCore.Http;
 30using Microsoft.AspNetCore.Http.HttpResults;
 31using Microsoft.Extensions.Logging;
 32
 33namespace Jellyfin.Api.Helpers;
 34
 35/// <summary>
 36/// Media info helper.
 37/// </summary>
 38public class MediaInfoHelper
 39{
 40    private readonly IUserManager _userManager;
 41    private readonly ILibraryManager _libraryManager;
 42    private readonly IMediaSourceManager _mediaSourceManager;
 43    private readonly IMediaEncoder _mediaEncoder;
 44    private readonly IServerConfigurationManager _serverConfigurationManager;
 45    private readonly ILogger<MediaInfoHelper> _logger;
 46    private readonly INetworkManager _networkManager;
 47    private readonly IDeviceManager _deviceManager;
 48    private readonly IServerApplicationHost _appHost;
 49
 50    /// <summary>
 51    /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
 52    /// </summary>
 53    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 54    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 55    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 56    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 57    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 58    /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
 59    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
 60    /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
 61    /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
 62    public MediaInfoHelper(
 63        IUserManager userManager,
 64        ILibraryManager libraryManager,
 65        IMediaSourceManager mediaSourceManager,
 66        IMediaEncoder mediaEncoder,
 67        IServerConfigurationManager serverConfigurationManager,
 68        ILogger<MediaInfoHelper> logger,
 69        INetworkManager networkManager,
 70        IDeviceManager deviceManager,
 71        IServerApplicationHost appHost)
 72    {
 2173        _userManager = userManager;
 2174        _libraryManager = libraryManager;
 2175        _mediaSourceManager = mediaSourceManager;
 2176        _mediaEncoder = mediaEncoder;
 2177        _serverConfigurationManager = serverConfigurationManager;
 2178        _logger = logger;
 2179        _networkManager = networkManager;
 2180        _deviceManager = deviceManager;
 2181        _appHost = appHost;
 2182    }
 83
 84    /// <summary>
 85    /// Get playback info.
 86    /// </summary>
 87    /// <param name="item">The item.</param>
 88    /// <param name="user">The user.</param>
 89    /// <param name="request">The current <see cref="HttpRequest"/>.</param>
 90    /// <param name="mediaSourceId">Media source id.</param>
 91    /// <param name="liveStreamId">Live stream id.</param>
 92    /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
 93    public async Task<PlaybackInfoResponse> GetPlaybackInfo(
 94        BaseItem item,
 95        User? user,
 96        HttpRequest request,
 97        string? mediaSourceId = null,
 98        string? liveStreamId = null)
 99    {
 4100        var result = new PlaybackInfoResponse();
 101
 4102        var mediaSources = await ResolvePlaybackMediaSources(item, user, mediaSourceId, liveStreamId).ConfigureAwait(fal
 103
 4104        if (mediaSources.Length == 0)
 105        {
 0106            result.MediaSources = Array.Empty<MediaSourceInfo>();
 107
 0108            result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
 109        }
 110        else
 111        {
 112            // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we s
 113            // Should we move this directly into MediaSourceManager?
 4114            var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(me
 4115            if (mediaSourcesClone is not null)
 116            {
 117                // Carry over the default audio index source.
 118                // This field is not intended to be exposed to API clients, but it is used internally by the server
 16119                for (int i = 0; i < mediaSourcesClone.Length && i < mediaSources.Length; i++)
 120                {
 4121                    mediaSourcesClone[i].DefaultAudioIndexSource = mediaSources[i].DefaultAudioIndexSource;
 122                }
 123
 16124                foreach (var mediaSource in mediaSourcesClone)
 125                {
 4126                    RewritePublishedLiveStreamPath(mediaSource, request);
 127                }
 128
 4129                result.MediaSources = mediaSourcesClone;
 130            }
 131
 4132            result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 133        }
 134
 4135        return result;
 4136    }
 137
 138    private async Task<MediaSourceInfo[]> ResolvePlaybackMediaSources(BaseItem item, User? user, string? mediaSourceId, 
 139    {
 4140        if (!string.IsNullOrWhiteSpace(liveStreamId))
 141        {
 3142            var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwa
 143
 3144            return new[] { mediaSource };
 145        }
 146
 147        // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
 1148        var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationTok
 149
 1150        if (string.IsNullOrWhiteSpace(mediaSourceId))
 151        {
 1152            return mediaSourcesList.ToArray();
 153        }
 154
 0155        return mediaSourcesList
 0156            .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
 0157            .ToArray();
 4158    }
 159
 160    /// <summary>
 161    /// SetDeviceSpecificData.
 162    /// </summary>
 163    /// <param name="item">Item to set data for.</param>
 164    /// <param name="mediaSource">Media source info.</param>
 165    /// <param name="profile">Device profile.</param>
 166    /// <param name="claimsPrincipal">Current claims principal.</param>
 167    /// <param name="maxBitrate">Max bitrate.</param>
 168    /// <param name="startTimeTicks">Start time ticks.</param>
 169    /// <param name="mediaSourceId">Media source id.</param>
 170    /// <param name="audioStreamIndex">Audio stream index.</param>
 171    /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
 172    /// <param name="maxAudioChannels">Max audio channels.</param>
 173    /// <param name="playSessionId">Play session id.</param>
 174    /// <param name="userId">User id.</param>
 175    /// <param name="enableDirectPlay">Enable direct play.</param>
 176    /// <param name="enableDirectStream">Enable direct stream.</param>
 177    /// <param name="enableTranscoding">Enable transcoding.</param>
 178    /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
 179    /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
 180    /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
 181    /// <param name="ipAddress">Requesting IP address.</param>
 182    public void SetDeviceSpecificData(
 183        BaseItem item,
 184        MediaSourceInfo mediaSource,
 185        DeviceProfile profile,
 186        ClaimsPrincipal claimsPrincipal,
 187        int? maxBitrate,
 188        long startTimeTicks,
 189        string mediaSourceId,
 190        int? audioStreamIndex,
 191        int? subtitleStreamIndex,
 192        int? maxAudioChannels,
 193        string playSessionId,
 194        Guid userId,
 195        bool enableDirectPlay,
 196        bool enableDirectStream,
 197        bool enableTranscoding,
 198        bool allowVideoStreamCopy,
 199        bool allowAudioStreamCopy,
 200        bool alwaysBurnInSubtitleWhenTranscoding,
 201        IPAddress ipAddress)
 202    {
 0203        var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
 204
 0205        var options = new MediaOptions
 0206        {
 0207            MediaSources = new[] { mediaSource },
 0208            Context = EncodingContext.Streaming,
 0209            DeviceId = claimsPrincipal.GetDeviceId(),
 0210            ItemId = item.Id,
 0211            Profile = profile,
 0212            MaxAudioChannels = maxAudioChannels,
 0213            AllowAudioStreamCopy = allowAudioStreamCopy,
 0214            AllowVideoStreamCopy = allowVideoStreamCopy,
 0215            AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding,
 0216        };
 217
 0218        if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
 219        {
 0220            options.MediaSourceId = mediaSourceId;
 0221            options.AudioStreamIndex = audioStreamIndex;
 0222            options.SubtitleStreamIndex = subtitleStreamIndex;
 223        }
 224
 0225        var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
 226
 0227        if (!enableDirectPlay)
 228        {
 0229            mediaSource.SupportsDirectPlay = false;
 230        }
 231
 0232        if (!enableDirectStream || !allowVideoStreamCopy)
 233        {
 0234            mediaSource.SupportsDirectStream = false;
 235        }
 236
 0237        if (!enableTranscoding)
 238        {
 0239            mediaSource.SupportsTranscoding = false;
 240        }
 241
 0242        if (item is Audio)
 243        {
 0244            _logger.LogInformation(
 0245                "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
 0246                user.Username,
 0247                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 248        }
 249        else
 250        {
 0251            _logger.LogInformation(
 0252                "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybac
 0253                user.Username,
 0254                user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 0255                user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 0256                user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
 257        }
 258
 0259        options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
 260
 0261        if (!options.ForceDirectStream)
 262        {
 263            // direct-stream http streaming is currently broken
 0264            options.EnableDirectStream = false;
 265        }
 266
 267        // Beginning of Playback Determination
 0268        var streamInfo = item.MediaType == MediaType.Audio
 0269            ? streamBuilder.GetOptimalAudioStream(options)
 0270            : streamBuilder.GetOptimalVideoStream(options);
 271
 0272        if (streamInfo is not null)
 273        {
 0274            streamInfo.PlaySessionId = playSessionId;
 0275            streamInfo.StartPositionTicks = startTimeTicks;
 276
 0277            mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
 278
 279            // Players do not handle this being set according to PlayMethod
 0280            mediaSource.SupportsDirectStream =
 0281                options.EnableDirectStream
 0282                    ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
 0283                    : streamInfo.PlayMethod == PlayMethod.DirectPlay;
 284
 0285            mediaSource.SupportsTranscoding =
 0286                streamInfo.PlayMethod == PlayMethod.DirectStream
 0287                || mediaSource.TranscodingContainer is not null
 0288                || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
 289
 0290            if (item is Audio)
 291            {
 0292                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
 293                {
 0294                    mediaSource.SupportsTranscoding = false;
 295                }
 296            }
 0297            else if (item is Video)
 298            {
 0299                if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
 0300                    && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
 0301                    && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
 302                {
 0303                    mediaSource.SupportsTranscoding = false;
 304                }
 305            }
 306
 0307            if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
 308            {
 0309                mediaSource.SupportsDirectPlay = false;
 0310                mediaSource.SupportsDirectStream = false;
 311
 0312                mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), "&allowVideoStreamCopy=f
 0313                mediaSource.TranscodingContainer = streamInfo.Container;
 0314                mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
 0315                if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
 316                {
 0317                    mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
 318                }
 319            }
 320            else
 321            {
 0322                if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStr
 323                {
 0324                    streamInfo.PlayMethod = PlayMethod.Transcode;
 0325                    mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), null);
 326
 0327                    if (!allowVideoStreamCopy)
 328                    {
 0329                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
 330                    }
 331
 0332                    if (!allowAudioStreamCopy)
 333                    {
 0334                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
 335                    }
 336
 0337                    if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
 338                    {
 0339                        mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
 340                    }
 341                }
 342            }
 343
 344            // Do this after the above so that StartPositionTicks is set
 345            // The token must not be null
 0346            SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
 0347            mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
 348        }
 349
 0350        foreach (var attachment in mediaSource.MediaAttachments)
 351        {
 0352            attachment.DeliveryUrl = string.Format(
 0353                CultureInfo.InvariantCulture,
 0354                "/Videos/{0}/{1}/Attachments/{2}",
 0355                item.Id,
 0356                mediaSource.Id,
 0357                attachment.Index);
 358        }
 0359    }
 360
 361    /// <summary>
 362    /// Sort media source.
 363    /// </summary>
 364    /// <param name="result">Playback info response.</param>
 365    /// <param name="maxBitrate">Max bitrate.</param>
 366    /// <param name="preferredItemId">The id of the queried item, whose own media source must stay the default.</param>
 367    public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate, Guid preferredItemId = default)
 368    {
 3369        var originalList = result.MediaSources.ToList();
 370
 371        // The queried item's source carries the user's resume state for that version, so it must stay the
 372        // default the client plays. An unfavorable bitrate means transcoding it, not switching to a sibling version.
 3373        var preferredId = preferredItemId.IsEmpty()
 3374            ? null
 3375            : preferredItemId.ToString("N", CultureInfo.InvariantCulture);
 376
 3377        result.MediaSources = result.MediaSources
 3378            .OrderByDescending(i => preferredId is not null && string.Equals(i.Id, preferredId, StringComparison.Ordinal
 3379            .ThenBy(i =>
 3380            {
 3381                // Nothing beats direct playing a file
 3382                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
 3383                {
 3384                    return 0;
 3385                }
 3386
 3387                return 1;
 3388            })
 3389            .ThenBy(i =>
 3390            {
 3391                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 3392                if (i.SupportsDirectPlay || i.SupportsDirectStream)
 3393                {
 3394                    return 0;
 3395                }
 3396
 3397                return 1;
 3398            })
 3399            .ThenBy(i =>
 3400            {
 3401                return i.Protocol switch
 3402                {
 3403                    MediaProtocol.File => 0,
 3404                    _ => 1,
 3405                };
 3406            })
 3407            .ThenBy(i =>
 3408            {
 3409                if (maxBitrate.HasValue && i.Bitrate.HasValue)
 3410                {
 3411                    return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
 3412                }
 3413
 3414                return 1;
 3415            })
 3416            .ThenBy(originalList.IndexOf)
 3417            .ToArray();
 3418    }
 419
 420    /// <summary>
 421    /// Open media source.
 422    /// </summary>
 423    /// <param name="httpContext">Http Context.</param>
 424    /// <param name="request">Live stream request.</param>
 425    /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
 426    public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
 427    {
 11428        var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
 429
 11430        RewritePublishedLiveStreamPath(result.MediaSource, httpContext.Request);
 431
 11432        var profile = request.DeviceProfile;
 11433        if (profile is null)
 434        {
 11435            var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
 11436            if (clientCapabilities is not null)
 437            {
 0438                profile = clientCapabilities.DeviceProfile;
 439            }
 440        }
 441
 11442        if (profile is not null)
 443        {
 0444            var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
 0445                ?? throw new ResourceNotFoundException();
 446
 0447            SetDeviceSpecificData(
 0448                item,
 0449                result.MediaSource,
 0450                profile,
 0451                httpContext.User,
 0452                request.MaxStreamingBitrate,
 0453                request.StartTimeTicks ?? 0,
 0454                result.MediaSource.Id,
 0455                request.AudioStreamIndex,
 0456                request.SubtitleStreamIndex,
 0457                request.MaxAudioChannels,
 0458                request.PlaySessionId,
 0459                request.UserId,
 0460                request.EnableDirectPlay,
 0461                request.EnableDirectStream,
 0462                true,
 0463                true,
 0464                true,
 0465                request.AlwaysBurnInSubtitleWhenTranscoding,
 0466                httpContext.GetNormalizedRemoteIP());
 467        }
 468        else
 469        {
 11470            if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
 471            {
 0472                result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
 473            }
 474        }
 475
 476        // here was a check if (result.MediaSource is not null) but Rider said it will never be null
 11477        NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
 478
 11479        return result;
 11480    }
 481
 482    /// <summary>
 483    /// Normalize media source container.
 484    /// </summary>
 485    /// <param name="mediaSource">Media source.</param>
 486    /// <param name="profile">Device profile.</param>
 487    /// <param name="type">Dlna profile type.</param>
 488    public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
 489    {
 11490        mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profi
 11491    }
 492
 493    private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
 494    {
 0495        var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
 0496        mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
 497
 0498        mediaSource.TranscodeReasons = info.TranscodeReasons;
 499
 0500        foreach (var profile in profiles)
 501        {
 0502            foreach (var stream in mediaSource.MediaStreams)
 503            {
 0504                if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
 505                {
 0506                    stream.DeliveryMethod = profile.DeliveryMethod;
 507
 0508                    if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
 509                    {
 0510                        stream.DeliveryUrl = profile.Url.TrimStart('-');
 0511                        stream.IsExternalUrl = profile.IsExternalUrl;
 512                    }
 513                }
 514            }
 515        }
 0516    }
 517
 518    private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
 519    {
 0520        var maxBitrate = clientMaxBitrate;
 0521        var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
 522
 0523        if (remoteClientMaxBitrate <= 0)
 524        {
 0525            remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
 526        }
 527
 0528        if (remoteClientMaxBitrate > 0)
 529        {
 0530            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
 531
 0532            _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMa
 0533            if (!isInLocalNetwork)
 534            {
 0535                maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
 536            }
 537        }
 538
 0539        return maxBitrate;
 540    }
 541
 542    /// <summary>
 543    /// Rewrites a Live TV media source's <see cref="MediaSourceInfo.Path"/> to the request-appropriate published
 544    /// URL when it points at a Jellyfin-hosted live stream buffer, so response copies never leak server-local
 545    /// addresses. Only opened live streams are eligible. The shared instance held by
 546    /// <see cref="IMediaSourceManager"/> is never touched by this method.
 547    /// </summary>
 548    /// <param name="mediaSource">The media source clone to rewrite in place.</param>
 549    /// <param name="request">The current <see cref="HttpRequest"/>.</param>
 550    private void RewritePublishedLiveStreamPath(MediaSourceInfo mediaSource, HttpRequest request)
 551    {
 552        // Opened live streams always carry a LiveStreamId; this excludes pre-open and plugin/remote sources.
 15553        if (string.IsNullOrEmpty(mediaSource.LiveStreamId))
 554        {
 7555            return;
 556        }
 557
 8558        var baseUrl = _serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
 8559        var publishedPath = GetPublishedLiveStreamPath(_appHost.GetSmartApiUrl(request), mediaSource.Path, mediaSource.P
 560
 8561        if (publishedPath is not null)
 562        {
 8563            mediaSource.Path = publishedPath;
 8564            return;
 565        }
 566
 0567        if (mediaSource.Path is not null && mediaSource.Path.Contains("/LiveTv/LiveStreamFiles/", StringComparison.Ordin
 568        {
 0569            _logger.LogDebug("Not rewriting live stream path for media source {MediaSourceId}: the local path did not re
 570        }
 0571    }
 572
 573    /// <summary>
 574    /// Resolves a Jellyfin-hosted Live TV buffer path to its request-appropriate published equivalent.
 575    /// Returns null when the path isn't a Jellyfin-hosted <c>/LiveTv/LiveStreamFiles/</c> HTTP URL.
 576    /// </summary>
 577    /// <param name="smartApiUrl">The request-appropriate base URL, as returned by <see cref="IServerApplicationHost.Get
 578    /// <param name="localPath">The media source's local (LAN-access) path, as built from <see cref="IServerApplicationH
 579    /// <param name="protocol">The media source's protocol.</param>
 580    /// <param name="baseUrl">The server's configured BaseUrl, if any.</param>
 581    /// <returns>The published path, or null if the local path should be left unchanged.</returns>
 582    internal static string? GetPublishedLiveStreamPath(
 583        string smartApiUrl,
 584        string? localPath,
 585        MediaProtocol protocol,
 586        string baseUrl)
 587    {
 21588        if (protocol != MediaProtocol.Http
 21589            || !Uri.TryCreate(localPath, UriKind.Absolute, out var localUri))
 590        {
 2591            return null;
 592        }
 593
 19594        var relativePath = localUri.PathAndQuery;
 19595        if (!string.IsNullOrEmpty(baseUrl))
 596        {
 7597            var basePrefix = baseUrl + "/";
 7598            if (!relativePath.StartsWith(basePrefix, StringComparison.OrdinalIgnoreCase))
 599            {
 1600                return null;
 601            }
 602
 6603            relativePath = relativePath[baseUrl.Length..];
 604        }
 605
 18606        if (!relativePath.StartsWith("/LiveTv/LiveStreamFiles/", StringComparison.OrdinalIgnoreCase))
 607        {
 0608            return null;
 609        }
 610
 18611        var prefix = smartApiUrl.TrimEnd('/');
 18612        if (!string.IsNullOrEmpty(baseUrl))
 613        {
 6614            var includesBaseUrl = Uri.TryCreate(prefix, UriKind.Absolute, out var publishedUri)
 6615                && Uri.UnescapeDataString(publishedUri.AbsolutePath)
 6616                    .TrimEnd('/')
 6617                    .EndsWith(baseUrl, StringComparison.OrdinalIgnoreCase);
 618
 6619            if (!includesBaseUrl)
 620            {
 2621                prefix += baseUrl;
 622            }
 623        }
 624
 18625        return prefix + relativePath;
 626    }
 627}

Methods/Properties

.ctor(MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Library.IMediaSourceManager,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,MediaBrowser.Controller.Configuration.IServerConfigurationManager,Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Api.Helpers.MediaInfoHelper>,MediaBrowser.Common.Net.INetworkManager,MediaBrowser.Controller.Devices.IDeviceManager,MediaBrowser.Controller.IServerApplicationHost)
GetPlaybackInfo()
ResolvePlaybackMediaSources()
SetDeviceSpecificData(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.DeviceProfile,System.Security.Claims.ClaimsPrincipal,System.Nullable`1<System.Int32>,System.Int64,System.String,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.String,System.Guid,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Net.IPAddress)
SortMediaSources(MediaBrowser.Model.MediaInfo.PlaybackInfoResponse,System.Nullable`1<System.Int64>,System.Guid)
OpenMediaSource()
NormalizeMediaSourceContainer(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.DeviceProfile,MediaBrowser.Model.Dlna.DlnaProfileType)
SetDeviceSpecificSubtitleInfo(MediaBrowser.Model.Dlna.StreamInfo,MediaBrowser.Model.Dto.MediaSourceInfo,System.String)
GetMaxBitrate(System.Nullable`1<System.Int32>,Jellyfin.Database.Implementations.Entities.User,System.Net.IPAddress)
RewritePublishedLiveStreamPath(MediaBrowser.Model.Dto.MediaSourceInfo,Microsoft.AspNetCore.Http.HttpRequest)
GetPublishedLiveStreamPath(System.String,System.String,MediaBrowser.Model.MediaInfo.MediaProtocol,System.String)