< 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
18%
Covered lines: 36
Uncovered lines: 161
Coverable lines: 197
Total lines: 924
Line coverage: 18.2%
Branch coverage
10%
Covered branches: 13
Total branches: 122
Branch coverage: 10.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/14/2025 - 12:11:05 AM Line coverage: 17.9% (35/195) Branch coverage: 10% (12/120) Total lines: 91911/18/2025 - 12:11:25 AM Line coverage: 18.2% (36/197) Branch coverage: 10.6% (13/122) Total lines: 924 8/14/2025 - 12:11:05 AM Line coverage: 17.9% (35/195) Branch coverage: 10% (12/120) Total lines: 91911/18/2025 - 12:11:25 AM Line coverage: 18.2% (36/197) Branch coverage: 10.6% (13/122) Total lines: 924

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.Library;
 27using MediaBrowser.Controller.LiveTv;
 28using MediaBrowser.Controller.MediaEncoding;
 29using MediaBrowser.Controller.Persistence;
 30using MediaBrowser.Controller.Providers;
 31using MediaBrowser.Model.Dlna;
 32using MediaBrowser.Model.Dto;
 33using MediaBrowser.Model.Entities;
 34using MediaBrowser.Model.Globalization;
 35using MediaBrowser.Model.IO;
 36using MediaBrowser.Model.MediaInfo;
 37using Microsoft.Extensions.Logging;
 38
 39namespace Emby.Server.Implementations.Library
 40{
 41    public class MediaSourceManager : IMediaSourceManager, IDisposable
 42    {
 43        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message
 44        private const char LiveStreamIdDelimiter = '_';
 45
 46        private readonly IServerApplicationHost _appHost;
 47        private readonly IItemRepository _itemRepo;
 48        private readonly IUserManager _userManager;
 49        private readonly ILibraryManager _libraryManager;
 50        private readonly IFileSystem _fileSystem;
 51        private readonly ILogger<MediaSourceManager> _logger;
 52        private readonly IUserDataManager _userDataManager;
 53        private readonly IMediaEncoder _mediaEncoder;
 54        private readonly ILocalizationManager _localizationManager;
 55        private readonly IApplicationPaths _appPaths;
 56        private readonly IDirectoryService _directoryService;
 57        private readonly IMediaStreamRepository _mediaStreamRepository;
 58        private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
 2759        private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILive
 2760        private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
 2761        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 62
 63        private IMediaSourceProvider[] _providers;
 64
 65        public MediaSourceManager(
 66            IServerApplicationHost appHost,
 67            IItemRepository itemRepo,
 68            IApplicationPaths applicationPaths,
 69            ILocalizationManager localizationManager,
 70            IUserManager userManager,
 71            ILibraryManager libraryManager,
 72            ILogger<MediaSourceManager> logger,
 73            IFileSystem fileSystem,
 74            IUserDataManager userDataManager,
 75            IMediaEncoder mediaEncoder,
 76            IDirectoryService directoryService,
 77            IMediaStreamRepository mediaStreamRepository,
 78            IMediaAttachmentRepository mediaAttachmentRepository)
 79        {
 2780            _appHost = appHost;
 2781            _itemRepo = itemRepo;
 2782            _userManager = userManager;
 2783            _libraryManager = libraryManager;
 2784            _logger = logger;
 2785            _fileSystem = fileSystem;
 2786            _userDataManager = userDataManager;
 2787            _mediaEncoder = mediaEncoder;
 2788            _localizationManager = localizationManager;
 2789            _appPaths = applicationPaths;
 2790            _directoryService = directoryService;
 2791            _mediaStreamRepository = mediaStreamRepository;
 2792            _mediaAttachmentRepository = mediaAttachmentRepository;
 2793        }
 94
 95        public void AddParts(IEnumerable<IMediaSourceProvider> providers)
 96        {
 2197            _providers = providers.ToArray();
 2198        }
 99
 100        public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
 101        {
 0102            var list = _mediaStreamRepository.GetMediaStreams(query);
 103
 0104            foreach (var stream in list)
 105            {
 0106                stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
 107            }
 108
 0109            return list;
 110        }
 111
 112        private static bool StreamSupportsExternalStream(MediaStream stream)
 113        {
 0114            if (stream.IsExternal)
 115            {
 0116                return true;
 117            }
 118
 0119            if (stream.IsTextSubtitleStream)
 120            {
 0121                return true;
 122            }
 123
 0124            if (stream.IsPgsSubtitleStream)
 125            {
 0126                return true;
 127            }
 128
 0129            return false;
 130        }
 131
 132        public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
 133        {
 0134            var list = GetMediaStreams(new MediaStreamQuery
 0135            {
 0136                ItemId = itemId
 0137            });
 138
 0139            return GetMediaStreamsForItem(list);
 140        }
 141
 142        private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
 143        {
 0144            foreach (var stream in streams)
 145            {
 0146                if (stream.Type == MediaStreamType.Subtitle)
 147                {
 0148                    stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
 149                }
 150            }
 151
 0152            return streams;
 153        }
 154
 155        /// <inheritdoc />
 156        public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
 157        {
 0158            return _mediaAttachmentRepository.GetMediaAttachments(query);
 159        }
 160
 161        /// <inheritdoc />
 162        public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
 163        {
 0164            return GetMediaAttachments(new MediaAttachmentQuery
 0165            {
 0166                ItemId = itemId
 0167            });
 168        }
 169
 170        public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMe
 171        {
 172            var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
 173
 174            // If file is strm or main media stream is missing, force a metadata refresh with remote probing
 175            if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
 176                && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
 177                    || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStream
 178                    || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStream
 179            {
 180                await item.RefreshMetadata(
 181                    new MetadataRefreshOptions(_directoryService)
 182                    {
 183                        EnableRemoteContentProbe = true,
 184                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh
 185                    },
 186                    cancellationToken).ConfigureAwait(false);
 187
 188                mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
 189            }
 190
 191            var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false);
 192
 193            var list = new List<MediaSourceInfo>();
 194
 195            list.AddRange(mediaSources);
 196
 197            foreach (var source in dynamicMediaSources)
 198            {
 199                // Validate that this is actually possible
 200                if (source.SupportsDirectStream)
 201                {
 202                    source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol);
 203                }
 204
 205                if (user is not null)
 206                {
 207                    SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
 208
 209                    if (item.MediaType == MediaType.Audio)
 210                    {
 211                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
 212                    }
 213                    else if (item.MediaType == MediaType.Video)
 214                    {
 215                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
 216                        source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
 217                    }
 218                }
 219
 220                list.Add(source);
 221            }
 222
 223            return SortMediaSources(list).ToArray();
 224        }
 225
 226        /// <inheritdoc />>
 227        public MediaProtocol GetPathProtocol(string path)
 228        {
 2063229            if (string.IsNullOrEmpty(path))
 230            {
 0231                return MediaProtocol.File;
 232            }
 233
 2063234            if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
 235            {
 1236                return MediaProtocol.Rtsp;
 237            }
 238
 2062239            if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase))
 240            {
 0241                return MediaProtocol.Rtmp;
 242            }
 243
 2062244            if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase))
 245            {
 2246                return MediaProtocol.Http;
 247            }
 248
 2060249            if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase))
 250            {
 0251                return MediaProtocol.Rtp;
 252            }
 253
 2060254            if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase))
 255            {
 0256                return MediaProtocol.Ftp;
 257            }
 258
 2060259            if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
 260            {
 0261                return MediaProtocol.Udp;
 262            }
 263
 2060264            return _fileSystem.IsPathFile(path) ? MediaProtocol.File : MediaProtocol.Http;
 265        }
 266
 267        public bool SupportsDirectStream(string path, MediaProtocol protocol)
 268        {
 0269            if (protocol == MediaProtocol.File)
 270            {
 0271                return true;
 272            }
 273
 0274            if (protocol == MediaProtocol.Http)
 275            {
 0276                if (path is not null)
 277                {
 0278                    if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase))
 279                    {
 0280                        return false;
 281                    }
 282
 0283                    return true;
 284                }
 285            }
 286
 0287            return false;
 288        }
 289
 290        private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancell
 291        {
 292            var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
 293            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 294
 295            return results.SelectMany(i => i);
 296        }
 297
 298        private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider prov
 299        {
 300            try
 301            {
 302                var sources = await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false);
 303                var list = sources.ToList();
 304
 305                foreach (var mediaSource in list)
 306                {
 307                    mediaSource.InferTotalBitrate();
 308
 309                    SetKeyProperties(provider, mediaSource);
 310                }
 311
 312                return list;
 313            }
 314            catch (Exception ex)
 315            {
 316                _logger.LogError(ex, "Error getting media sources");
 317                return [];
 318            }
 319        }
 320
 321        private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
 322        {
 0323            var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamId
 324
 0325            if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparis
 326            {
 0327                mediaSource.OpenToken = prefix + mediaSource.OpenToken;
 328            }
 329
 0330            if (!string.IsNullOrEmpty(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringCo
 331            {
 0332                mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId;
 333            }
 0334        }
 335
 336        public async Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool
 337        {
 338            if (!string.IsNullOrEmpty(liveStreamId))
 339            {
 340                return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
 341            }
 342
 343            var sources = await GetPlaybackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).Co
 344
 345            return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
 346        }
 347
 348        public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User use
 349        {
 0350            ArgumentNullException.ThrowIfNull(item);
 351
 0352            var hasMediaSources = (IHasMediaSources)item;
 353
 0354            var sources = hasMediaSources.GetMediaSources(enablePathSubstitution);
 355
 0356            if (user is not null)
 357            {
 0358                foreach (var source in sources)
 359                {
 0360                    SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
 361
 0362                    if (item.MediaType == MediaType.Audio)
 363                    {
 0364                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
 365                    }
 0366                    else if (item.MediaType == MediaType.Video)
 367                    {
 0368                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
 0369                        source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
 370                    }
 371                }
 372            }
 373
 0374            return sources;
 375        }
 376
 377        private IReadOnlyList<string> NormalizeLanguage(string language)
 378        {
 0379            if (string.IsNullOrEmpty(language))
 380            {
 0381                return [];
 382            }
 383
 0384            var culture = _localizationManager.FindLanguageInfo(language);
 0385            if (culture is not null)
 386            {
 0387                return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLe
 388            }
 389
 0390            return [language];
 391        }
 392
 393        private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowR
 394        {
 0395            if (userData is not null
 0396                && userData.SubtitleStreamIndex.HasValue
 0397                && user.RememberSubtitleSelections
 0398                && user.SubtitleMode != SubtitlePlaybackMode.None
 0399                && allowRememberingSelection)
 400            {
 0401                var index = userData.SubtitleStreamIndex.Value;
 402                // Make sure the saved index is still valid
 0403                if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
 404                {
 0405                    source.DefaultSubtitleStreamIndex = index;
 0406                    return;
 407                }
 408            }
 409
 0410            var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
 411
 0412            var defaultAudioIndex = source.DefaultAudioStreamIndex;
 0413            var audioLanguage = defaultAudioIndex is null
 0414                ? null
 0415                : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select
 416
 0417            source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
 0418                source.MediaStreams,
 0419                preferredSubs,
 0420                user.SubtitleMode,
 0421                audioLanguage);
 422
 0423            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLang
 0424        }
 425
 426        private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowReme
 427        {
 0428            if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRemem
 429            {
 0430                var index = userData.AudioStreamIndex.Value;
 431                // Make sure the saved index is still valid
 0432                if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
 433                {
 0434                    source.DefaultAudioStreamIndex = index;
 0435                    source.DefaultAudioIndexSource = AudioIndexSource.User;
 0436                    return;
 437                }
 438            }
 439
 0440            var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
 441
 0442            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferr
 0443            if (user.PlayDefaultAudioTrack)
 444            {
 0445                source.DefaultAudioIndexSource |= AudioIndexSource.Default;
 446            }
 447
 0448            if (preferredAudio.Count > 0)
 449            {
 0450                source.DefaultAudioIndexSource |= AudioIndexSource.Language;
 451            }
 0452        }
 453
 454        public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
 455        {
 456            // Item would only be null if the app didn't supply ItemId as part of the live stream open request
 0457            var mediaType = item?.MediaType ?? MediaType.Video;
 458
 0459            if (mediaType == MediaType.Video)
 460            {
 0461                var userData = item is null ? null : _userDataManager.GetUserData(user, item);
 462
 0463                var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
 464
 0465                SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
 0466                SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
 467            }
 0468            else if (mediaType == MediaType.Audio)
 469            {
 0470                var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 471
 0472                if (audio is not null)
 473                {
 0474                    source.DefaultAudioStreamIndex = audio.Index;
 475                }
 476            }
 0477        }
 478
 479        private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
 480        {
 0481            return sources.OrderBy(i =>
 0482            {
 0483                if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile)
 0484                {
 0485                    return 0;
 0486                }
 0487
 0488                return 1;
 0489            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 0490            .ThenByDescending(i =>
 0491            {
 0492                var stream = i.VideoStream;
 0493
 0494                return stream?.Width ?? 0;
 0495            })
 0496            .Where(i => i.Type != MediaSourceType.Placeholder);
 497        }
 498
 499        public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest req
 500        {
 501            MediaSourceInfo mediaSource;
 502            ILiveStream liveStream;
 503
 504            using (await _liveStreamLocker.LockAsync(cancellationToken).ConfigureAwait(false))
 505            {
 506                var (provider, keyId) = GetProvider(request.OpenToken);
 507
 508                var currentLiveStreams = _openStreams.Values.ToList();
 509
 510                liveStream = await provider.OpenMediaSource(keyId, currentLiveStreams, cancellationToken).ConfigureAwait
 511
 512                mediaSource = liveStream.MediaSource;
 513
 514                // Validate that this is actually possible
 515                if (mediaSource.SupportsDirectStream)
 516                {
 517                    mediaSource.SupportsDirectStream = SupportsDirectStream(mediaSource.Path, mediaSource.Protocol);
 518                }
 519
 520                SetKeyProperties(provider, mediaSource);
 521
 522                _openStreams[mediaSource.LiveStreamId] = liveStream;
 523            }
 524
 525            try
 526            {
 527                if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
 528                {
 529                    AddMediaInfo(mediaSource);
 530                }
 531                else
 532                {
 533                    // hack - these two values were taken from LiveTVMediaSourceProvider
 534                    string cacheKey = request.OpenToken;
 535
 536                    await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
 537                        .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken)
 538                        .ConfigureAwait(false);
 539                }
 540            }
 541            catch (Exception ex)
 542            {
 543                _logger.LogError(ex, "Error probing live tv stream");
 544                AddMediaInfo(mediaSource);
 545            }
 546
 547            // TODO: @bond Fix
 548            var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
 549            _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
 550            var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
 551
 552            if (!request.UserId.IsEmpty())
 553            {
 554                var user = _userManager.GetUserById(request.UserId);
 555                var item = request.ItemId.IsEmpty()
 556                    ? null
 557                    : _libraryManager.GetItemById(request.ItemId);
 558                SetDefaultAudioAndSubtitleStreamIndices(item, clone, user);
 559            }
 560
 561            return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDi
 562        }
 563
 564        private static void AddMediaInfo(MediaSourceInfo mediaSource)
 565        {
 0566            mediaSource.DefaultSubtitleStreamIndex = null;
 567
 568            // Null this out so that it will be treated like a live stream
 0569            if (mediaSource.IsInfiniteStream)
 570            {
 0571                mediaSource.RunTimeTicks = null;
 572            }
 573
 0574            var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 575
 0576            if (audioStream is null || audioStream.Index == -1)
 577            {
 0578                mediaSource.DefaultAudioStreamIndex = null;
 579            }
 580            else
 581            {
 0582                mediaSource.DefaultAudioStreamIndex = audioStream.Index;
 583            }
 584
 0585            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
 0586            if (videoStream is not null)
 587            {
 0588                if (!videoStream.BitRate.HasValue)
 589                {
 0590                    var width = videoStream.Width ?? 1920;
 591
 0592                    if (width >= 3000)
 593                    {
 0594                        videoStream.BitRate = 30000000;
 595                    }
 0596                    else if (width >= 1900)
 597                    {
 0598                        videoStream.BitRate = 20000000;
 599                    }
 0600                    else if (width >= 1200)
 601                    {
 0602                        videoStream.BitRate = 8000000;
 603                    }
 0604                    else if (width >= 700)
 605                    {
 0606                        videoStream.BitRate = 2000000;
 607                    }
 608                }
 609            }
 610
 611            // Try to estimate this
 0612            mediaSource.InferTotalBitrate();
 0613        }
 614
 615        public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationTo
 616        {
 617            var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
 618            return result.Item1;
 619        }
 620
 621        public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
 622        {
 623            // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
 624            var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
 625
 626            var mediaSource = liveStreamInfo.MediaSource;
 627
 628            if (liveStreamInfo is IDirectStreamProvider)
 629            {
 630                var info = await _mediaEncoder.GetMediaInfo(
 631                    new MediaInfoRequest
 632                    {
 633                        MediaSource = mediaSource,
 634                        ExtractChapters = false,
 635                        MediaType = DlnaProfileType.Video
 636                    },
 637                    cancellationToken).ConfigureAwait(false);
 638
 639                mediaSource.MediaStreams = info.MediaStreams;
 640                mediaSource.Container = info.Container;
 641                mediaSource.Bitrate = info.Bitrate;
 642            }
 643
 644            return mediaSource;
 645        }
 646
 647        public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProb
 648        {
 649            var originalRuntime = mediaSource.RunTimeTicks;
 650
 651            var now = DateTime.UtcNow;
 652
 653            MediaInfo mediaInfo = null;
 654            var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", c
 655
 656            if (!string.IsNullOrEmpty(cacheKey))
 657            {
 658                FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
 659                try
 660                {
 661                    mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationT
 662                }
 663                catch (Exception ex)
 664                {
 665                    _logger.LogDebug(ex, "Error parsing cached media info.");
 666                }
 667                finally
 668                {
 669                    await jsonStream.DisposeAsync().ConfigureAwait(false);
 670                }
 671            }
 672
 673            if (mediaInfo is null)
 674            {
 675                if (addProbeDelay)
 676                {
 677                    var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
 678                    delayMs = Math.Max(3000, delayMs);
 679                    await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
 680                }
 681
 682                if (isLiveStream)
 683                {
 684                    mediaSource.AnalyzeDurationMs = 3000;
 685                }
 686
 687                mediaInfo = await _mediaEncoder.GetMediaInfo(
 688                    new MediaInfoRequest
 689                    {
 690                        MediaSource = mediaSource,
 691                        MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
 692                        ExtractChapters = false
 693                    },
 694                    cancellationToken).ConfigureAwait(false);
 695
 696                if (cacheFilePath is not null)
 697                {
 698                    Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
 699                    FileStream createStream = AsyncFile.Create(cacheFilePath);
 700                    await using (createStream.ConfigureAwait(false))
 701                    {
 702                        await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).Co
 703                    }
 704
 705                    // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
 706                }
 707            }
 708
 709            var mediaStreams = mediaInfo.MediaStreams;
 710
 711            if (isLiveStream && !string.IsNullOrEmpty(cacheKey))
 712            {
 713                var newList = new List<MediaStream>();
 714                newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1));
 715                newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1));
 716
 717                foreach (var stream in newList)
 718                {
 719                    stream.Index = -1;
 720                    stream.Language = null;
 721                }
 722
 723                mediaStreams = newList;
 724            }
 725
 726            _logger.LogInformation("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToS
 727
 728            mediaSource.Bitrate = mediaInfo.Bitrate;
 729            mediaSource.Container = mediaInfo.Container;
 730            mediaSource.Formats = mediaInfo.Formats;
 731            mediaSource.MediaStreams = mediaStreams;
 732            mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks;
 733            mediaSource.Size = mediaInfo.Size;
 734            mediaSource.Timestamp = mediaInfo.Timestamp;
 735            mediaSource.Video3DFormat = mediaInfo.Video3DFormat;
 736            mediaSource.VideoType = mediaInfo.VideoType;
 737
 738            mediaSource.DefaultSubtitleStreamIndex = null;
 739
 740            if (isLiveStream)
 741            {
 742                // Null this out so that it will be treated like a live stream
 743                if (!originalRuntime.HasValue)
 744                {
 745                    mediaSource.RunTimeTicks = null;
 746                }
 747            }
 748
 749            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 750
 751            if (audioStream is null || audioStream.Index == -1)
 752            {
 753                mediaSource.DefaultAudioStreamIndex = null;
 754            }
 755            else
 756            {
 757                mediaSource.DefaultAudioStreamIndex = audioStream.Index;
 758            }
 759
 760            var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
 761            if (videoStream is not null)
 762            {
 763                if (!videoStream.BitRate.HasValue)
 764                {
 765                    var width = videoStream.Width ?? 1920;
 766
 767                    if (width >= 3000)
 768                    {
 769                        videoStream.BitRate = 30000000;
 770                    }
 771                    else if (width >= 1900)
 772                    {
 773                        videoStream.BitRate = 20000000;
 774                    }
 775                    else if (width >= 1200)
 776                    {
 777                        videoStream.BitRate = 8000000;
 778                    }
 779                    else if (width >= 700)
 780                    {
 781                        videoStream.BitRate = 2000000;
 782                    }
 783                }
 784
 785                // This is coming up false and preventing stream copy
 786                videoStream.IsAVC = null;
 787            }
 788
 789            if (isLiveStream)
 790            {
 791                mediaSource.AnalyzeDurationMs = 3000;
 792            }
 793
 794            // Try to estimate this
 795            mediaSource.InferTotalBitrate(true);
 796        }
 797
 798        public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, Canc
 799        {
 0800            ArgumentException.ThrowIfNullOrEmpty(id);
 801
 0802            var info = GetLiveStreamInfo(id);
 0803            if (info is null)
 804            {
 0805                return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirect
 806            }
 807
 0808            return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStre
 809        }
 810
 811        public ILiveStream GetLiveStreamInfo(string id)
 812        {
 0813            ArgumentException.ThrowIfNullOrEmpty(id);
 814
 0815            if (_openStreams.TryGetValue(id, out ILiveStream info))
 816            {
 0817                return info;
 818            }
 819
 0820            return null;
 821        }
 822
 823        /// <inheritdoc />
 824        public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
 825        {
 0826            return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparis
 827        }
 828
 829        public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
 830        {
 831            var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
 832            return result.Item1;
 833        }
 834
 835        public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, Cance
 836        {
 837            var stream = new MediaSourceInfo
 838            {
 839                EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
 840                EncoderProtocol = MediaProtocol.Http,
 841                Path = info.Path,
 842                Protocol = MediaProtocol.File,
 843                Id = info.Id,
 844                SupportsDirectPlay = false,
 845                SupportsDirectStream = true,
 846                SupportsTranscoding = true,
 847                IsInfiniteStream = true,
 848                RequiresOpening = false,
 849                RequiresClosing = false,
 850                BufferMs = 0,
 851                IgnoreDts = true,
 852                IgnoreIndex = true
 853            };
 854
 855            await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
 856                .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
 857
 858            return [stream];
 859        }
 860
 861        public async Task CloseLiveStream(string id)
 862        {
 863            ArgumentException.ThrowIfNullOrEmpty(id);
 864
 865            using (await _liveStreamLocker.LockAsync().ConfigureAwait(false))
 866            {
 867                if (_openStreams.TryGetValue(id, out ILiveStream liveStream))
 868                {
 869                    liveStream.ConsumerCount--;
 870
 871                    _logger.LogInformation("Live stream {0} consumer count is now {1}", liveStream.OriginalStreamId, liv
 872
 873                    if (liveStream.ConsumerCount <= 0)
 874                    {
 875                        _openStreams.TryRemove(id, out _);
 876
 877                        _logger.LogInformation("Closing live stream {0}", id);
 878
 879                        await liveStream.Close().ConfigureAwait(false);
 880                        _logger.LogInformation("Live stream {0} closed successfully", id);
 881                    }
 882                }
 883            }
 884        }
 885
 886        private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
 887        {
 0888            ArgumentException.ThrowIfNullOrEmpty(key);
 889
 0890            var keys = key.Split(LiveStreamIdDelimiter, 2);
 891
 0892            var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", Cult
 893
 0894            var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
 0895            var keyId = key.Substring(splitIndex + 1);
 896
 0897            return (provider, keyId);
 898        }
 899
 900        /// <inheritdoc />
 901        public void Dispose()
 902        {
 21903            Dispose(true);
 21904            GC.SuppressFinalize(this);
 21905        }
 906
 907        /// <summary>
 908        /// Releases unmanaged and - optionally - managed resources.
 909        /// </summary>
 910        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 911        protected virtual void Dispose(bool dispose)
 912        {
 21913            if (dispose)
 914            {
 42915                foreach (var key in _openStreams.Keys.ToList())
 916                {
 0917                    CloseLiveStream(key).GetAwaiter().GetResult();
 918                }
 919
 21920                _liveStreamLocker.Dispose();
 921            }
 21922        }
 923    }
 924}

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)
GetPathProtocol(System.String)
SupportsDirectStream(System.String,MediaBrowser.Model.MediaInfo.MediaProtocol)
SetKeyProperties(MediaBrowser.Controller.Library.IMediaSourceProvider,MediaBrowser.Model.Dto.MediaSourceInfo)
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)
SetDefaultAudioAndSubtitleStreamIndices(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceInfo,Jellyfin.Database.Implementations.Entities.User)
SortMediaSources(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dto.MediaSourceInfo>)
AddMediaInfo(MediaBrowser.Model.Dto.MediaSourceInfo)
GetLiveStreamWithDirectStreamProvider(System.String,System.Threading.CancellationToken)
GetLiveStreamInfo(System.String)
GetLiveStreamInfoByUniqueId(System.String)
GetProvider(System.String)
Dispose()
Dispose(System.Boolean)