< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.MediaSourceManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/MediaSourceManager.cs
Line coverage
20%
Covered lines: 91
Uncovered lines: 359
Coverable lines: 450
Total lines: 1002
Line coverage: 20.2%
Branch coverage
21%
Covered branches: 53
Total branches: 242
Branch coverage: 21.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/16/2026 - 12:14:00 AM Line coverage: 18.2% (36/197) Branch coverage: 10.6% (13/122) Total lines: 9244/19/2026 - 12:14:27 AM Line coverage: 8.8% (36/409) Branch coverage: 6.1% (13/210) Total lines: 9245/8/2026 - 12:15:13 AM Line coverage: 22.6% (100/441) Branch coverage: 26% (63/242) Total lines: 9725/20/2026 - 12:15:44 AM Line coverage: 22.6% (100/441) Branch coverage: 25.2% (61/242) Total lines: 9725/27/2026 - 12:15:38 AM Line coverage: 21.2% (91/428) Branch coverage: 23.2% (53/228) Total lines: 9586/1/2026 - 12:16:05 AM Line coverage: 21.1% (91/430) Branch coverage: 23% (53/230) Total lines: 9636/2/2026 - 12:15:49 AM Line coverage: 20.7% (91/439) Branch coverage: 22% (53/240) Total lines: 9886/9/2026 - 12:16:23 AM Line coverage: 20.2% (91/450) Branch coverage: 21.9% (53/242) Total lines: 1002 3/16/2026 - 12:14:00 AM Line coverage: 18.2% (36/197) Branch coverage: 10.6% (13/122) Total lines: 9244/19/2026 - 12:14:27 AM Line coverage: 8.8% (36/409) Branch coverage: 6.1% (13/210) Total lines: 9245/8/2026 - 12:15:13 AM Line coverage: 22.6% (100/441) Branch coverage: 26% (63/242) Total lines: 9725/20/2026 - 12:15:44 AM Line coverage: 22.6% (100/441) Branch coverage: 25.2% (61/242) Total lines: 9725/27/2026 - 12:15:38 AM Line coverage: 21.2% (91/428) Branch coverage: 23.2% (53/228) Total lines: 9586/1/2026 - 12:16:05 AM Line coverage: 21.1% (91/430) Branch coverage: 23% (53/230) Total lines: 9636/2/2026 - 12:15:49 AM Line coverage: 20.7% (91/439) Branch coverage: 22% (53/240) Total lines: 9886/9/2026 - 12:16:23 AM Line coverage: 20.2% (91/450) Branch coverage: 21.9% (53/242) Total lines: 1002

Coverage delta

Coverage delta 20 -20

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/MediaSourceManager.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Concurrent;
 7using System.Collections.Generic;
 8using System.Collections.Immutable;
 9using System.Globalization;
 10using System.IO;
 11using System.Linq;
 12using System.Text.Json;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using AsyncKeyedLock;
 16using Jellyfin.Data;
 17using Jellyfin.Data.Enums;
 18using Jellyfin.Database.Implementations.Entities;
 19using Jellyfin.Database.Implementations.Enums;
 20using Jellyfin.Extensions;
 21using Jellyfin.Extensions.Json;
 22using MediaBrowser.Common.Configuration;
 23using MediaBrowser.Common.Extensions;
 24using MediaBrowser.Controller;
 25using MediaBrowser.Controller.Entities;
 26using MediaBrowser.Controller.Entities.TV;
 27using MediaBrowser.Controller.IO;
 28using MediaBrowser.Controller.Library;
 29using MediaBrowser.Controller.LiveTv;
 30using MediaBrowser.Controller.MediaEncoding;
 31using MediaBrowser.Controller.Persistence;
 32using MediaBrowser.Controller.Providers;
 33using MediaBrowser.Model.Dlna;
 34using MediaBrowser.Model.Dto;
 35using MediaBrowser.Model.Entities;
 36using MediaBrowser.Model.Globalization;
 37using MediaBrowser.Model.IO;
 38using MediaBrowser.Model.MediaInfo;
 39using Microsoft.Extensions.Logging;
 40
 41namespace Emby.Server.Implementations.Library
 42{
 43    public class MediaSourceManager : IMediaSourceManager, IDisposable
 44    {
 45        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message
 46        private const char LiveStreamIdDelimiter = '_';
 47
 48        private readonly IServerApplicationHost _appHost;
 49        private readonly IItemRepository _itemRepo;
 50        private readonly IUserManager _userManager;
 51        private readonly ILibraryManager _libraryManager;
 52        private readonly IFileSystem _fileSystem;
 53        private readonly ILogger<MediaSourceManager> _logger;
 54        private readonly IUserDataManager _userDataManager;
 55        private readonly IMediaEncoder _mediaEncoder;
 56        private readonly ILocalizationManager _localizationManager;
 57        private readonly IApplicationPaths _appPaths;
 58        private readonly IDirectoryService _directoryService;
 59        private readonly IMediaStreamRepository _mediaStreamRepository;
 60        private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
 4961        private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILive
 4962        private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
 4963        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 64
 65        private IMediaSourceProvider[] _providers;
 66
 67        public MediaSourceManager(
 68            IServerApplicationHost appHost,
 69            IItemRepository itemRepo,
 70            IApplicationPaths applicationPaths,
 71            ILocalizationManager localizationManager,
 72            IUserManager userManager,
 73            ILibraryManager libraryManager,
 74            ILogger<MediaSourceManager> logger,
 75            IFileSystem fileSystem,
 76            IUserDataManager userDataManager,
 77            IMediaEncoder mediaEncoder,
 78            IDirectoryService directoryService,
 79            IMediaStreamRepository mediaStreamRepository,
 80            IMediaAttachmentRepository mediaAttachmentRepository)
 81        {
 4982            _appHost = appHost;
 4983            _itemRepo = itemRepo;
 4984            _userManager = userManager;
 4985            _libraryManager = libraryManager;
 4986            _logger = logger;
 4987            _fileSystem = fileSystem;
 4988            _userDataManager = userDataManager;
 4989            _mediaEncoder = mediaEncoder;
 4990            _localizationManager = localizationManager;
 4991            _appPaths = applicationPaths;
 4992            _directoryService = directoryService;
 4993            _mediaStreamRepository = mediaStreamRepository;
 4994            _mediaAttachmentRepository = mediaAttachmentRepository;
 4995        }
 96
 97        public void AddParts(IEnumerable<IMediaSourceProvider> providers)
 98        {
 2199            _providers = providers.ToArray();
 21100        }
 101
 102        public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
 103        {
 0104            var list = _mediaStreamRepository.GetMediaStreams(query);
 105
 0106            foreach (var stream in list)
 107            {
 0108                stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
 109            }
 110
 0111            return list;
 112        }
 113
 114        private static bool StreamSupportsExternalStream(MediaStream stream)
 115        {
 0116            if (stream.IsExternal)
 117            {
 0118                return true;
 119            }
 120
 0121            if (stream.IsTextSubtitleStream)
 122            {
 0123                return true;
 124            }
 125
 0126            if (stream.IsPgsSubtitleStream)
 127            {
 0128                return true;
 129            }
 130
 0131            if (stream.IsVobSubSubtitleStream)
 132            {
 0133                return true;
 134            }
 135
 0136            return false;
 137        }
 138
 139        public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
 140        {
 0141            var list = GetMediaStreams(new MediaStreamQuery
 0142            {
 0143                ItemId = itemId
 0144            });
 145
 0146            return GetMediaStreamsForItem(list);
 147        }
 148
 149        private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
 150        {
 0151            foreach (var stream in streams)
 152            {
 0153                if (stream.Type == MediaStreamType.Subtitle)
 154                {
 0155                    stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
 156                }
 157            }
 158
 0159            return streams;
 160        }
 161
 162        /// <inheritdoc />
 163        public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
 164        {
 0165            return _mediaAttachmentRepository.GetMediaAttachments(query);
 166        }
 167
 168        /// <inheritdoc />
 169        public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
 170        {
 0171            return GetMediaAttachments(new MediaAttachmentQuery
 0172            {
 0173                ItemId = itemId
 0174            });
 175        }
 176
 177        public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMe
 178        {
 0179            var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
 0180            ResolveSymlinkPaths(mediaSources, enablePathSubstitution);
 181
 182            // If file is strm or main media stream is missing, force a metadata refresh with remote probing
 0183            if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
 0184                && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
 0185                    || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStream
 0186                    || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStream
 187            {
 0188                await item.RefreshMetadata(
 0189                    new MetadataRefreshOptions(_directoryService)
 0190                    {
 0191                        EnableRemoteContentProbe = true,
 0192                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh
 0193                    },
 0194                    cancellationToken).ConfigureAwait(false);
 195
 0196                mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
 0197                ResolveSymlinkPaths(mediaSources, enablePathSubstitution);
 198            }
 199
 0200            var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false);
 201
 0202            var list = new List<MediaSourceInfo>();
 203
 0204            list.AddRange(mediaSources);
 205
 0206            foreach (var source in dynamicMediaSources)
 207            {
 208                // Validate that this is actually possible
 0209                if (source.SupportsDirectStream)
 210                {
 0211                    source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol);
 212                }
 213
 0214                if (user is not null)
 215                {
 0216                    SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
 217
 0218                    if (item.MediaType == MediaType.Audio)
 219                    {
 0220                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
 221                    }
 0222                    else if (item.MediaType == MediaType.Video)
 223                    {
 0224                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
 0225                        source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
 226                    }
 227                }
 228
 0229                list.Add(source);
 230            }
 231
 0232            return SortMediaSources(list, item.Id).ToArray();
 0233        }
 234
 235        /// <inheritdoc />>
 236        public MediaProtocol GetPathProtocol(string path)
 237        {
 2248238            if (string.IsNullOrEmpty(path))
 239            {
 0240                return MediaProtocol.File;
 241            }
 242
 2248243            if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
 244            {
 1245                return MediaProtocol.Rtsp;
 246            }
 247
 2247248            if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase))
 249            {
 0250                return MediaProtocol.Rtmp;
 251            }
 252
 2247253            if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase))
 254            {
 2255                return MediaProtocol.Http;
 256            }
 257
 2245258            if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase))
 259            {
 0260                return MediaProtocol.Rtp;
 261            }
 262
 2245263            if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase))
 264            {
 0265                return MediaProtocol.Ftp;
 266            }
 267
 2245268            if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
 269            {
 0270                return MediaProtocol.Udp;
 271            }
 272
 2245273            return _fileSystem.IsPathFile(path) ? MediaProtocol.File : MediaProtocol.Http;
 274        }
 275
 276        public bool SupportsDirectStream(string path, MediaProtocol protocol)
 277        {
 0278            if (protocol == MediaProtocol.File)
 279            {
 0280                return true;
 281            }
 282
 0283            if (protocol == MediaProtocol.Http)
 284            {
 0285                if (path is not null)
 286                {
 0287                    if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase))
 288                    {
 0289                        return false;
 290                    }
 291
 0292                    return true;
 293                }
 294            }
 295
 0296            return false;
 297        }
 298
 299        private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancell
 300        {
 0301            var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
 0302            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 303
 0304            return results.SelectMany(i => i);
 0305        }
 306
 307        private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider prov
 308        {
 309            try
 310            {
 0311                var sources = await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false);
 0312                var list = sources.ToList();
 313
 0314                foreach (var mediaSource in list)
 315                {
 0316                    mediaSource.InferTotalBitrate();
 317
 0318                    SetKeyProperties(provider, mediaSource);
 319                }
 320
 0321                return list;
 322            }
 0323            catch (Exception ex)
 324            {
 0325                _logger.LogError(ex, "Error getting media sources");
 0326                return [];
 327            }
 0328        }
 329
 330        /// <summary>
 331        /// Resolves symlinked file paths on the supplied sources to the real on-disk target.
 332        /// Skipped when <paramref name="enablePathSubstitution"/> is set because the path may
 333        /// already have been rewritten to a UNC/URL meant for the client to consume directly.
 334        /// </summary>
 335        private static void ResolveSymlinkPaths(IReadOnlyList<MediaSourceInfo> sources, bool enablePathSubstitution)
 336        {
 0337            if (enablePathSubstitution)
 338            {
 0339                return;
 340            }
 341
 0342            foreach (var source in sources)
 343            {
 0344                if (source.Protocol == MediaProtocol.File
 0345                    && FileSystemHelper.ResolveLinkTarget(source.Path, returnFinalTarget: true) is { Exists: true } targ
 346                {
 0347                    source.Path = target.FullName;
 348                }
 349            }
 0350        }
 351
 352        private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
 353        {
 0354            var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamId
 355
 0356            if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparis
 357            {
 0358                mediaSource.OpenToken = prefix + mediaSource.OpenToken;
 359            }
 360
 0361            if (!string.IsNullOrEmpty(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringCo
 362            {
 0363                mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId;
 364            }
 0365        }
 366
 367        public async Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool
 368        {
 0369            if (!string.IsNullOrEmpty(liveStreamId))
 370            {
 0371                return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
 372            }
 373
 0374            var sources = await GetPlaybackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).Co
 375
 0376            return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
 0377        }
 378
 379        public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User use
 380        {
 0381            ArgumentNullException.ThrowIfNull(item);
 382
 0383            var hasMediaSources = (IHasMediaSources)item;
 384
 0385            var sources = hasMediaSources.GetMediaSources(enablePathSubstitution);
 386
 0387            if (user is not null)
 388            {
 0389                sources = sources
 0390                    .Where(source => !Guid.TryParse(source.Id, out var sourceId)
 0391                        || sourceId.Equals(item.Id)
 0392                        || _libraryManager.GetItemById<BaseItem>(sourceId, user) is not null)
 0393                    .ToArray();
 394
 0395                foreach (var source in sources)
 396                {
 0397                    SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
 398
 0399                    if (item.MediaType == MediaType.Audio)
 400                    {
 0401                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
 402                    }
 0403                    else if (item.MediaType == MediaType.Video)
 404                    {
 0405                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
 0406                        source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
 407                    }
 408                }
 409            }
 410
 0411            return sources;
 412        }
 413
 414        private IReadOnlyList<string> NormalizeLanguage(string language)
 415        {
 49416            if (string.IsNullOrEmpty(language))
 417            {
 1418                return [];
 419            }
 420
 48421            var culture = _localizationManager.FindLanguageInfo(language);
 48422            if (culture is not null)
 423            {
 48424                return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLe
 425            }
 426
 0427            return [language];
 428        }
 429
 430        private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowR
 431        {
 22432            if (userData is not null
 22433                && userData.SubtitleStreamIndex.HasValue
 22434                && user.RememberSubtitleSelections
 22435                && user.SubtitleMode != SubtitlePlaybackMode.None
 22436                && allowRememberingSelection)
 437            {
 0438                var index = userData.SubtitleStreamIndex.Value;
 439                // Make sure the saved index is still valid
 0440                if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
 441                {
 0442                    source.DefaultSubtitleStreamIndex = index;
 0443                    return;
 444                }
 445            }
 446
 22447            var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
 448
 22449            var defaultAudioIndex = source.DefaultAudioStreamIndex;
 22450            var audioLanguage = defaultAudioIndex is null
 22451                ? null
 22452                : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select
 453
 22454            source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
 22455                source.MediaStreams,
 22456                preferredSubs,
 22457                user.SubtitleMode,
 22458                audioLanguage);
 459
 22460            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLang
 22461        }
 462
 463        private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowReme
 464        {
 22465            if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRemem
 466            {
 0467                var index = userData.AudioStreamIndex.Value;
 468                // Make sure the saved index is still valid
 0469                if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
 470                {
 0471                    source.DefaultAudioStreamIndex = index;
 0472                    source.DefaultAudioIndexSource = AudioIndexSource.User;
 0473                    return;
 474                }
 475            }
 476
 22477            if (string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase))
 478            {
 16479                if (user.PlayDefaultAudioTrack)
 480                {
 6481                    source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(
 6482                        source.MediaStreams,
 6483                        NormalizeLanguage(originalLanguage),
 6484                        user.PlayDefaultAudioTrack);
 6485                    return;
 486                }
 487
 10488                var originalIndex = source.MediaStreams.FindIndex(i => i.Type == MediaStreamType.Audio && i.IsOriginal);
 489
 10490                if (!string.IsNullOrWhiteSpace(originalLanguage) && originalIndex != -1)
 491                {
 4492                    var mediaLanguageOriginal = source.MediaStreams[originalIndex].Language;
 4493                    if (NormalizeLanguage(mediaLanguageOriginal).Contains(NormalizeLanguage(originalLanguage).FirstOrDef
 494                    {
 1495                        source.DefaultAudioStreamIndex = originalIndex;
 1496                        return;
 497                    }
 498                }
 6499                else if (originalIndex != -1)
 500                {
 2501                    source.DefaultAudioStreamIndex = originalIndex;
 2502                    return;
 503                }
 504            }
 505
 13506            var preferredAudio = string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.Ordina
 13507                ? NormalizeLanguage(originalLanguage)
 13508                : NormalizeLanguage(user.AudioLanguagePreference);
 509
 13510            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferr
 13511            if (user.PlayDefaultAudioTrack)
 512            {
 3513                source.DefaultAudioIndexSource |= AudioIndexSource.Default;
 514            }
 515
 13516            if (preferredAudio.Count > 0)
 517            {
 13518                source.DefaultAudioIndexSource |= AudioIndexSource.Language;
 519            }
 13520        }
 521
 522        public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
 523        {
 524            // Item would only be null if the app didn't supply ItemId as part of the live stream open request
 22525            var mediaType = item?.MediaType ?? MediaType.Video;
 526
 22527            if (mediaType == MediaType.Video)
 528            {
 22529                var userData = item is null ? null : _userDataManager.GetUserData(user, item);
 530
 22531                var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
 532
 22533                var originalLanguage = item?.GetInheritedOriginalLanguage();
 534
 22535                SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection, originalLanguage);
 22536                SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
 537            }
 0538            else if (mediaType == MediaType.Audio)
 539            {
 0540                var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 541
 0542                if (audio is not null)
 543                {
 0544                    source.DefaultAudioStreamIndex = audio.Index;
 545                }
 546            }
 0547        }
 548
 549        private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources, Guid preferre
 550        {
 551            // The source belonging to the queried item sorts first so it stays the default that gets played.
 0552            var preferredId = preferredItemId.IsEmpty()
 0553                ? null
 0554                : preferredItemId.ToString("N", CultureInfo.InvariantCulture);
 555
 0556            return sources
 0557                .OrderByDescending(i => preferredId is not null && string.Equals(i.Id, preferredId, StringComparison.Ord
 0558                .ThenBy(i =>
 0559                {
 0560                    if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile)
 0561                    {
 0562                        return 0;
 0563                    }
 0564
 0565                    return 1;
 0566                })
 0567                .ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 0568                .ThenByDescending(i =>
 0569                {
 0570                    var stream = i.VideoStream;
 0571
 0572                    return stream?.Width ?? 0;
 0573                })
 0574                .Where(i => i.Type != MediaSourceType.Placeholder);
 575        }
 576
 577        public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest req
 578        {
 579            MediaSourceInfo mediaSource;
 580            ILiveStream liveStream;
 581
 0582            using (await _liveStreamLocker.LockAsync(cancellationToken).ConfigureAwait(false))
 583            {
 0584                var (provider, keyId) = GetProvider(request.OpenToken);
 585
 0586                var currentLiveStreams = _openStreams.Values.ToList();
 587
 0588                liveStream = await provider.OpenMediaSource(keyId, currentLiveStreams, cancellationToken).ConfigureAwait
 589
 0590                mediaSource = liveStream.MediaSource;
 591
 592                // Validate that this is actually possible
 0593                if (mediaSource.SupportsDirectStream)
 594                {
 0595                    mediaSource.SupportsDirectStream = SupportsDirectStream(mediaSource.Path, mediaSource.Protocol);
 596                }
 597
 0598                SetKeyProperties(provider, mediaSource);
 599
 0600                _openStreams[mediaSource.LiveStreamId] = liveStream;
 0601            }
 602
 603            try
 604            {
 0605                if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
 606                {
 0607                    AddMediaInfo(mediaSource);
 608                }
 609                else
 610                {
 611                    // hack - these two values were taken from LiveTVMediaSourceProvider
 0612                    string cacheKey = request.OpenToken;
 613
 0614                    await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
 0615                        .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken)
 0616                        .ConfigureAwait(false);
 617                }
 0618            }
 0619            catch (Exception ex)
 620            {
 0621                _logger.LogError(ex, "Error probing live tv stream");
 0622                AddMediaInfo(mediaSource);
 0623            }
 624
 625            // TODO: @bond Fix
 0626            var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
 0627            _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
 0628            var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
 629
 0630            if (!request.UserId.IsEmpty())
 631            {
 0632                var user = _userManager.GetUserById(request.UserId);
 0633                var item = request.ItemId.IsEmpty()
 0634                    ? null
 0635                    : _libraryManager.GetItemById(request.ItemId);
 0636                SetDefaultAudioAndSubtitleStreamIndices(item, clone, user);
 637            }
 638
 0639            return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDi
 0640        }
 641
 642        private static void AddMediaInfo(MediaSourceInfo mediaSource)
 643        {
 0644            mediaSource.DefaultSubtitleStreamIndex = null;
 645
 646            // Null this out so that it will be treated like a live stream
 0647            if (mediaSource.IsInfiniteStream)
 648            {
 0649                mediaSource.RunTimeTicks = null;
 650            }
 651
 0652            var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 653
 0654            if (audioStream is null || audioStream.Index == -1)
 655            {
 0656                mediaSource.DefaultAudioStreamIndex = null;
 657            }
 658            else
 659            {
 0660                mediaSource.DefaultAudioStreamIndex = audioStream.Index;
 661            }
 662
 0663            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
 0664            if (videoStream is not null)
 665            {
 0666                if (!videoStream.BitRate.HasValue)
 667                {
 0668                    var width = videoStream.Width ?? 1920;
 669
 0670                    if (width >= 3000)
 671                    {
 0672                        videoStream.BitRate = 30000000;
 673                    }
 0674                    else if (width >= 1900)
 675                    {
 0676                        videoStream.BitRate = 20000000;
 677                    }
 0678                    else if (width >= 1200)
 679                    {
 0680                        videoStream.BitRate = 8000000;
 681                    }
 0682                    else if (width >= 700)
 683                    {
 0684                        videoStream.BitRate = 2000000;
 685                    }
 686                }
 687            }
 688
 689            // Try to estimate this
 0690            mediaSource.InferTotalBitrate();
 0691        }
 692
 693        public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationTo
 694        {
 0695            var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
 0696            return result.Item1;
 0697        }
 698
 699        public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
 700        {
 701            // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
 0702            var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
 703
 0704            var mediaSource = liveStreamInfo.MediaSource;
 705
 0706            if (liveStreamInfo is IDirectStreamProvider)
 707            {
 0708                var info = await _mediaEncoder.GetMediaInfo(
 0709                    new MediaInfoRequest
 0710                    {
 0711                        MediaSource = mediaSource,
 0712                        ExtractChapters = false,
 0713                        MediaType = DlnaProfileType.Video
 0714                    },
 0715                    cancellationToken).ConfigureAwait(false);
 716
 0717                mediaSource.MediaStreams = info.MediaStreams;
 0718                mediaSource.Container = info.Container;
 0719                mediaSource.Bitrate = info.Bitrate;
 720            }
 721
 0722            return mediaSource;
 0723        }
 724
 725        public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProb
 726        {
 0727            var originalRuntime = mediaSource.RunTimeTicks;
 728
 0729            var now = DateTime.UtcNow;
 730
 0731            MediaInfo mediaInfo = null;
 0732            var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", c
 733
 0734            if (!string.IsNullOrEmpty(cacheKey))
 735            {
 0736                FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
 737                try
 738                {
 0739                    mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationT
 0740                }
 0741                catch (Exception ex)
 742                {
 0743                    _logger.LogDebug(ex, "Error parsing cached media info.");
 0744                }
 745                finally
 746                {
 0747                    await jsonStream.DisposeAsync().ConfigureAwait(false);
 748                }
 0749            }
 750
 0751            if (mediaInfo is null)
 752            {
 0753                if (addProbeDelay)
 754                {
 0755                    var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
 0756                    delayMs = Math.Max(3000, delayMs);
 0757                    await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
 758                }
 759
 0760                if (isLiveStream)
 761                {
 0762                    mediaSource.AnalyzeDurationMs = 3000;
 763                }
 764
 0765                mediaInfo = await _mediaEncoder.GetMediaInfo(
 0766                    new MediaInfoRequest
 0767                    {
 0768                        MediaSource = mediaSource,
 0769                        MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
 0770                        ExtractChapters = false
 0771                    },
 0772                    cancellationToken).ConfigureAwait(false);
 773
 0774                if (cacheFilePath is not null)
 775                {
 0776                    Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
 0777                    FileStream createStream = AsyncFile.Create(cacheFilePath);
 0778                    await using (createStream.ConfigureAwait(false))
 779                    {
 0780                        await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).Co
 781                    }
 782
 783                    // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
 784                }
 785            }
 786
 0787            var mediaStreams = mediaInfo.MediaStreams;
 788
 0789            if (isLiveStream && !string.IsNullOrEmpty(cacheKey))
 790            {
 0791                var newList = new List<MediaStream>();
 0792                newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1));
 0793                newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1));
 794
 0795                foreach (var stream in newList)
 796                {
 0797                    stream.Index = -1;
 0798                    stream.Language = null;
 799                }
 800
 0801                mediaStreams = newList;
 802            }
 803
 0804            _logger.LogInformation("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToS
 805
 0806            mediaSource.Bitrate = mediaInfo.Bitrate;
 0807            mediaSource.Container = mediaInfo.Container;
 0808            mediaSource.Formats = mediaInfo.Formats;
 0809            mediaSource.MediaStreams = mediaStreams;
 0810            mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks;
 0811            mediaSource.Size = mediaInfo.Size;
 0812            mediaSource.Timestamp = mediaInfo.Timestamp;
 0813            mediaSource.Video3DFormat = mediaInfo.Video3DFormat;
 0814            mediaSource.VideoType = mediaInfo.VideoType;
 815
 0816            mediaSource.DefaultSubtitleStreamIndex = null;
 817
 0818            if (isLiveStream)
 819            {
 820                // Null this out so that it will be treated like a live stream
 0821                if (!originalRuntime.HasValue)
 822                {
 0823                    mediaSource.RunTimeTicks = null;
 824                }
 825            }
 826
 0827            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 828
 0829            if (audioStream is null || audioStream.Index == -1)
 830            {
 0831                mediaSource.DefaultAudioStreamIndex = null;
 832            }
 833            else
 834            {
 0835                mediaSource.DefaultAudioStreamIndex = audioStream.Index;
 836            }
 837
 0838            var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
 0839            if (videoStream is not null)
 840            {
 0841                if (!videoStream.BitRate.HasValue)
 842                {
 0843                    var width = videoStream.Width ?? 1920;
 844
 0845                    if (width >= 3000)
 846                    {
 0847                        videoStream.BitRate = 30000000;
 848                    }
 0849                    else if (width >= 1900)
 850                    {
 0851                        videoStream.BitRate = 20000000;
 852                    }
 0853                    else if (width >= 1200)
 854                    {
 0855                        videoStream.BitRate = 8000000;
 856                    }
 0857                    else if (width >= 700)
 858                    {
 0859                        videoStream.BitRate = 2000000;
 860                    }
 861                }
 862
 863                // This is coming up false and preventing stream copy
 0864                videoStream.IsAVC = null;
 865            }
 866
 0867            if (isLiveStream)
 868            {
 0869                mediaSource.AnalyzeDurationMs = 3000;
 870            }
 871
 872            // Try to estimate this
 0873            mediaSource.InferTotalBitrate(true);
 0874        }
 875
 876        public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, Canc
 877        {
 0878            ArgumentException.ThrowIfNullOrEmpty(id);
 879
 0880            var info = GetLiveStreamInfo(id);
 0881            if (info is null)
 882            {
 0883                return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirect
 884            }
 885
 0886            return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStre
 887        }
 888
 889        public ILiveStream GetLiveStreamInfo(string id)
 890        {
 0891            ArgumentException.ThrowIfNullOrEmpty(id);
 892
 0893            if (_openStreams.TryGetValue(id, out ILiveStream info))
 894            {
 0895                return info;
 896            }
 897
 0898            return null;
 899        }
 900
 901        /// <inheritdoc />
 902        public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
 903        {
 0904            return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparis
 905        }
 906
 907        public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
 908        {
 0909            var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
 0910            return result.Item1;
 0911        }
 912
 913        public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, Cance
 914        {
 0915            var stream = new MediaSourceInfo
 0916            {
 0917                EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
 0918                EncoderProtocol = MediaProtocol.Http,
 0919                Path = info.Path,
 0920                Protocol = MediaProtocol.File,
 0921                Id = info.Id,
 0922                SupportsDirectPlay = false,
 0923                SupportsDirectStream = true,
 0924                SupportsTranscoding = true,
 0925                IsInfiniteStream = true,
 0926                RequiresOpening = false,
 0927                RequiresClosing = false,
 0928                BufferMs = 0,
 0929                IgnoreDts = true,
 0930                IgnoreIndex = true
 0931            };
 932
 0933            await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
 0934                .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
 935
 0936            return [stream];
 0937        }
 938
 939        public async Task CloseLiveStream(string id)
 940        {
 0941            ArgumentException.ThrowIfNullOrEmpty(id);
 942
 0943            using (await _liveStreamLocker.LockAsync().ConfigureAwait(false))
 944            {
 0945                if (_openStreams.TryGetValue(id, out ILiveStream liveStream))
 946                {
 0947                    liveStream.ConsumerCount--;
 948
 0949                    _logger.LogInformation("Live stream {0} consumer count is now {1}", liveStream.OriginalStreamId, liv
 950
 0951                    if (liveStream.ConsumerCount <= 0)
 952                    {
 0953                        _openStreams.TryRemove(id, out _);
 954
 0955                        _logger.LogInformation("Closing live stream {0}", id);
 956
 0957                        await liveStream.Close().ConfigureAwait(false);
 0958                        _logger.LogInformation("Live stream {0} closed successfully", id);
 959                    }
 960                }
 0961            }
 0962        }
 963
 964        private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
 965        {
 0966            ArgumentException.ThrowIfNullOrEmpty(key);
 967
 0968            var keys = key.Split(LiveStreamIdDelimiter, 2);
 969
 0970            var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", Cult
 971
 0972            var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
 0973            var keyId = key.Substring(splitIndex + 1);
 974
 0975            return (provider, keyId);
 976        }
 977
 978        /// <inheritdoc />
 979        public void Dispose()
 980        {
 21981            Dispose(true);
 21982            GC.SuppressFinalize(this);
 21983        }
 984
 985        /// <summary>
 986        /// Releases unmanaged and - optionally - managed resources.
 987        /// </summary>
 988        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 989        protected virtual void Dispose(bool dispose)
 990        {
 21991            if (dispose)
 992            {
 42993                foreach (var key in _openStreams.Keys.ToList())
 994                {
 0995                    CloseLiveStream(key).GetAwaiter().GetResult();
 996                }
 997
 21998                _liveStreamLocker.Dispose();
 999            }
 211000        }
 1001    }
 1002}

Methods/Properties

.ctor(MediaBrowser.Controller.IServerApplicationHost,MediaBrowser.Controller.Persistence.IItemRepository,MediaBrowser.Common.Configuration.IApplicationPaths,MediaBrowser.Model.Globalization.ILocalizationManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.ILibraryManager,Microsoft.Extensions.Logging.ILogger`1<Emby.Server.Implementations.Library.MediaSourceManager>,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.Library.IUserDataManager,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Persistence.IMediaStreamRepository,MediaBrowser.Controller.Persistence.IMediaAttachmentRepository)
AddParts(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Library.IMediaSourceProvider>)
GetMediaStreams(MediaBrowser.Controller.Persistence.MediaStreamQuery)
StreamSupportsExternalStream(MediaBrowser.Model.Entities.MediaStream)
GetMediaStreams(System.Guid)
GetMediaStreamsForItem(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Model.Entities.MediaStream>)
GetMediaAttachments(MediaBrowser.Controller.Persistence.MediaAttachmentQuery)
GetMediaAttachments(System.Guid)
GetPlaybackMediaSources()
GetPathProtocol(System.String)
SupportsDirectStream(System.String,MediaBrowser.Model.MediaInfo.MediaProtocol)
GetDynamicMediaSources()
GetDynamicMediaSources()
ResolveSymlinkPaths(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Model.Dto.MediaSourceInfo>,System.Boolean)
SetKeyProperties(MediaBrowser.Controller.Library.IMediaSourceProvider,MediaBrowser.Model.Dto.MediaSourceInfo)
GetMediaSource()
GetStaticMediaSources(MediaBrowser.Controller.Entities.BaseItem,System.Boolean,Jellyfin.Database.Implementations.Entities.User)
NormalizeLanguage(System.String)
SetDefaultSubtitleStreamIndex(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Controller.Entities.UserItemData,Jellyfin.Database.Implementations.Entities.User,System.Boolean)
SetDefaultAudioStreamIndex(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Controller.Entities.UserItemData,Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.String)
SetDefaultAudioAndSubtitleStreamIndices(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceInfo,Jellyfin.Database.Implementations.Entities.User)
SortMediaSources(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dto.MediaSourceInfo>,System.Guid)
OpenLiveStreamInternal()
AddMediaInfo(MediaBrowser.Model.Dto.MediaSourceInfo)
OpenLiveStream()
GetLiveStreamMediaInfo()
AddMediaInfoWithProbe()
GetLiveStreamWithDirectStreamProvider(System.String,System.Threading.CancellationToken)
GetLiveStreamInfo(System.String)
GetLiveStreamInfoByUniqueId(System.String)
GetLiveStream()
GetRecordingStreamMediaSources()
CloseLiveStream()
GetProvider(System.String)
Dispose()
Dispose(System.Boolean)