瀏覽代碼

Merge pull request #11054 from barronpm/livetv-mediasourceprovider

LiveTV MediaSourceProvider refactor
Bond-009 1 年之前
父節點
當前提交
c72bd8a092

+ 2 - 3
Jellyfin.Server/Startup.cs

@@ -6,9 +6,8 @@ using System.Net.Mime;
 using System.Text;
 using Emby.Server.Implementations.EntryPoints;
 using Jellyfin.Api.Middleware;
-using Jellyfin.LiveTv;
-using Jellyfin.LiveTv.EmbyTV;
 using Jellyfin.LiveTv.Extensions;
+using Jellyfin.LiveTv.Recordings;
 using Jellyfin.MediaEncoding.Hls.Extensions;
 using Jellyfin.Networking;
 using Jellyfin.Networking.HappyEyeballs;
@@ -128,7 +127,7 @@ namespace Jellyfin.Server
             services.AddHlsPlaylistGenerator();
             services.AddLiveTvServices();
 
-            services.AddHostedService<LiveTvHost>();
+            services.AddHostedService<RecordingsHost>();
             services.AddHostedService<AutoDiscoveryHost>();
             services.AddHostedService<PortForwardingHost>();
             services.AddHostedService<NfoUserDataSaver>();

+ 0 - 19
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -10,7 +10,6 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
@@ -105,16 +104,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task{QueryResult{SeriesTimerInfoDto}}.</returns>
         Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Gets the channel stream.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="mediaSourceId">The media source identifier.</param>
-        /// <param name="currentLiveStreams">The current live streams.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{StreamResponseInfo}.</returns>
-        Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
-
         /// <summary>
         /// Gets the program.
         /// </summary>
@@ -220,14 +209,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Internal channels.</returns>
         QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Gets the channel media sources.
-        /// </summary>
-        /// <param name="item">Item to search for.</param>
-        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
-        /// <returns>Channel media sources wrapped in a task.</returns>
-        Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken);
-
         /// <summary>
         /// Adds the information to program dto.
         /// </summary>

+ 9 - 0
src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.LiveTv;
 
 namespace Jellyfin.LiveTv.Configuration;
@@ -15,4 +16,12 @@ public static class LiveTvConfigurationExtensions
     /// <returns>The <see cref="LiveTvOptions"/>.</returns>
     public static LiveTvOptions GetLiveTvConfiguration(this IConfigurationManager configurationManager)
         => configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+
+    /// <summary>
+    /// Gets the <see cref="XbmcMetadataOptions"/>.
+    /// </summary>
+    /// <param name="configurationManager">The <see cref="IConfigurationManager"/>.</param>
+    /// <returns>The <see cref="XbmcMetadataOptions"/>.</returns>
+    public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager)
+        => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
 }

+ 5 - 5
src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs → src/Jellyfin.LiveTv/DefaultLiveTvService.cs

@@ -24,13 +24,13 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.LiveTv;
 using Microsoft.Extensions.Logging;
 
-namespace Jellyfin.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv
 {
-    public sealed class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds
+    public sealed class DefaultLiveTvService : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds
     {
         public const string ServiceName = "Emby";
 
-        private readonly ILogger<EmbyTV> _logger;
+        private readonly ILogger<DefaultLiveTvService> _logger;
         private readonly IServerConfigurationManager _config;
         private readonly ITunerHostManager _tunerHostManager;
         private readonly IListingsManager _listingsManager;
@@ -40,8 +40,8 @@ namespace Jellyfin.LiveTv.EmbyTV
         private readonly TimerManager _timerManager;
         private readonly SeriesTimerManager _seriesTimerManager;
 
-        public EmbyTV(
-            ILogger<EmbyTV> logger,
+        public DefaultLiveTvService(
+            ILogger<DefaultLiveTvService> logger,
             IServerConfigurationManager config,
             ITunerHostManager tunerHostManager,
             IListingsManager listingsManager,

+ 0 - 19
src/Jellyfin.LiveTv/EmbyTV/NfoConfigurationExtensions.cs

@@ -1,19 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
-
-namespace Jellyfin.LiveTv.EmbyTV
-{
-    /// <summary>
-    /// Class containing extension methods for working with the nfo configuration.
-    /// </summary>
-    public static class NfoConfigurationExtensions
-    {
-        /// <summary>
-        /// Gets the nfo configuration.
-        /// </summary>
-        /// <param name="configurationManager">The configuration manager.</param>
-        /// <returns>The nfo configuration.</returns>
-        public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager)
-         => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
-    }
-}

+ 1 - 1
src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs

@@ -37,7 +37,7 @@ public static class LiveTvServiceCollectionExtensions
         services.AddSingleton<IGuideManager, GuideManager>();
         services.AddSingleton<IRecordingsManager, RecordingsManager>();
 
-        services.AddSingleton<ILiveTvService, EmbyTV.EmbyTV>();
+        services.AddSingleton<ILiveTvService, DefaultLiveTvService>();
         services.AddSingleton<ITunerHost, HdHomerunHost>();
         services.AddSingleton<ITunerHost, M3UTunerHost>();
         services.AddSingleton<IListingsProvider, SchedulesDirect>();

+ 1 - 1
src/Jellyfin.LiveTv/Guide/GuideManager.cs

@@ -141,7 +141,7 @@ public class GuideManager : IGuideManager
             CleanDatabase(newProgramIdList.ToArray(), [BaseItemKind.LiveTvProgram], progress, cancellationToken);
         }
 
-        var coreService = _liveTvManager.Services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
+        var coreService = _liveTvManager.Services.OfType<DefaultLiveTvService>().FirstOrDefault();
         if (coreService is not null)
         {
             await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);

+ 4 - 193
src/Jellyfin.LiveTv/LiveTvManager.cs

@@ -12,7 +12,6 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using Jellyfin.LiveTv.Configuration;
-using Jellyfin.LiveTv.IO;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
@@ -72,7 +71,7 @@ namespace Jellyfin.LiveTv
             _recordingsManager = recordingsManager;
             _services = services.ToArray();
 
-            var defaultService = _services.OfType<EmbyTV.EmbyTV>().First();
+            var defaultService = _services.OfType<DefaultLiveTvService>().First();
             defaultService.TimerCreated += OnEmbyTvTimerCreated;
             defaultService.TimerCancelled += OnEmbyTvTimerCancelled;
         }
@@ -152,73 +151,6 @@ namespace Jellyfin.LiveTv
             return _libraryManager.GetItemsResult(internalQuery);
         }
 
-        public async Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
-        {
-            if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
-            {
-                mediaSourceId = null;
-            }
-
-            var channel = (LiveTvChannel)_libraryManager.GetItemById(id);
-
-            bool isVideo = channel.ChannelType == ChannelType.TV;
-            var service = GetService(channel);
-            _logger.LogInformation("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
-
-            MediaSourceInfo info;
-#pragma warning disable CA1859 // TODO: Analyzer bug?
-            ILiveStream liveStream;
-#pragma warning restore CA1859
-            if (service is ISupportsDirectStreamProvider supportsManagedStream)
-            {
-                liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
-                info = liveStream.MediaSource;
-            }
-            else
-            {
-                info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
-                var openedId = info.Id;
-                Func<Task> closeFn = () => service.CloseLiveStream(openedId, CancellationToken.None);
-
-                liveStream = new ExclusiveLiveStream(info, closeFn);
-
-                var startTime = DateTime.UtcNow;
-                await liveStream.Open(cancellationToken).ConfigureAwait(false);
-                var endTime = DateTime.UtcNow;
-                _logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
-            }
-
-            info.RequiresClosing = true;
-
-            var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_";
-
-            info.LiveStreamId = idPrefix + info.Id;
-
-            Normalize(info, service, isVideo);
-
-            return new Tuple<MediaSourceInfo, ILiveStream>(info, liveStream);
-        }
-
-        public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken)
-        {
-            var baseItem = (LiveTvChannel)item;
-            var service = GetService(baseItem);
-
-            var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
-
-            if (sources.Count == 0)
-            {
-                throw new NotImplementedException();
-            }
-
-            foreach (var source in sources)
-            {
-                Normalize(source, service, baseItem.ChannelType == ChannelType.TV);
-            }
-
-            return sources;
-        }
-
         private ILiveTvService GetService(LiveTvChannel item)
         {
             var name = item.ServiceName;
@@ -240,127 +172,6 @@ namespace Jellyfin.LiveTv
                         "No service with the name '{0}' can be found.",
                         name));
 
-        private static void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
-        {
-            // Not all of the plugins are setting this
-            mediaSource.IsInfiniteStream = true;
-
-            if (mediaSource.MediaStreams.Count == 0)
-            {
-                if (isVideo)
-                {
-                    mediaSource.MediaStreams = new MediaStream[]
-                    {
-                        new MediaStream
-                        {
-                            Type = MediaStreamType.Video,
-                            // Set the index to -1 because we don't know the exact index of the video stream within the container
-                            Index = -1,
-
-                            // Set to true if unknown to enable deinterlacing
-                            IsInterlaced = true
-                        },
-                        new MediaStream
-                        {
-                            Type = MediaStreamType.Audio,
-                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
-                            Index = -1
-                        }
-                    };
-                }
-                else
-                {
-                    mediaSource.MediaStreams = new MediaStream[]
-                    {
-                        new MediaStream
-                        {
-                            Type = MediaStreamType.Audio,
-                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
-                            Index = -1
-                        }
-                    };
-                }
-            }
-
-            // Clean some bad data coming from providers
-            foreach (var stream in mediaSource.MediaStreams)
-            {
-                if (stream.BitRate.HasValue && stream.BitRate <= 0)
-                {
-                    stream.BitRate = null;
-                }
-
-                if (stream.Channels.HasValue && stream.Channels <= 0)
-                {
-                    stream.Channels = null;
-                }
-
-                if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
-                {
-                    stream.AverageFrameRate = null;
-                }
-
-                if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
-                {
-                    stream.RealFrameRate = null;
-                }
-
-                if (stream.Width.HasValue && stream.Width <= 0)
-                {
-                    stream.Width = null;
-                }
-
-                if (stream.Height.HasValue && stream.Height <= 0)
-                {
-                    stream.Height = null;
-                }
-
-                if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
-                {
-                    stream.SampleRate = null;
-                }
-
-                if (stream.Level.HasValue && stream.Level <= 0)
-                {
-                    stream.Level = null;
-                }
-            }
-
-            var indexes = mediaSource.MediaStreams.Select(i => i.Index).Distinct().ToList();
-
-            // If there are duplicate stream indexes, set them all to unknown
-            if (indexes.Count != mediaSource.MediaStreams.Count)
-            {
-                foreach (var stream in mediaSource.MediaStreams)
-                {
-                    stream.Index = -1;
-                }
-            }
-
-            // Set the total bitrate if not already supplied
-            mediaSource.InferTotalBitrate();
-
-            if (service is not EmbyTV.EmbyTV)
-            {
-                // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
-                // mediaSource.SupportsDirectPlay = false;
-                // mediaSource.SupportsDirectStream = false;
-                mediaSource.SupportsTranscoding = true;
-                foreach (var stream in mediaSource.MediaStreams)
-                {
-                    if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize))
-                    {
-                        stream.NalLengthSize = "0";
-                    }
-
-                    if (stream.Type == MediaStreamType.Video)
-                    {
-                        stream.IsInterlaced = true;
-                    }
-                }
-            }
-        }
-
         public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
         {
             var program = _libraryManager.GetItemById(id);
@@ -769,7 +580,7 @@ namespace Jellyfin.LiveTv
 
             var channel = string.IsNullOrWhiteSpace(info.ChannelId)
                 ? null
-                : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(EmbyTV.EmbyTV.ServiceName, info.ChannelId));
+                : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(DefaultLiveTvService.ServiceName, info.ChannelId));
 
             dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId)
                 ? null
@@ -1005,7 +816,7 @@ namespace Jellyfin.LiveTv
 
             await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
 
-            if (service is not EmbyTV.EmbyTV)
+            if (service is not DefaultLiveTvService)
             {
                 TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
             }
@@ -1314,7 +1125,7 @@ namespace Jellyfin.LiveTv
 
             _logger.LogInformation("New recording scheduled");
 
-            if (service is not EmbyTV.EmbyTV)
+            if (service is not DefaultLiveTvService)
             {
                 TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
                     new TimerEventInfo(newTimerId)

+ 211 - 9
src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs

@@ -8,11 +8,15 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.LiveTv.IO;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.Extensions.Logging;
 
@@ -23,19 +27,27 @@ namespace Jellyfin.LiveTv
         // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
         private const char StreamIdDelimiter = '_';
 
-        private readonly ILiveTvManager _liveTvManager;
-        private readonly IRecordingsManager _recordingsManager;
         private readonly ILogger<LiveTvMediaSourceProvider> _logger;
-        private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerApplicationHost _appHost;
+        private readonly IRecordingsManager _recordingsManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILiveTvService[] _services;
 
-        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IRecordingsManager recordingsManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
+        public LiveTvMediaSourceProvider(
+            ILogger<LiveTvMediaSourceProvider> logger,
+            IServerApplicationHost appHost,
+            IRecordingsManager recordingsManager,
+            IMediaSourceManager mediaSourceManager,
+            ILibraryManager libraryManager,
+            IEnumerable<ILiveTvService> services)
         {
-            _liveTvManager = liveTvManager;
-            _recordingsManager = recordingsManager;
             _logger = logger;
-            _mediaSourceManager = mediaSourceManager;
             _appHost = appHost;
+            _recordingsManager = recordingsManager;
+            _mediaSourceManager = mediaSourceManager;
+            _libraryManager = libraryManager;
+            _services = services.ToArray();
         }
 
         public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
@@ -68,7 +80,7 @@ namespace Jellyfin.LiveTv
                 }
                 else
                 {
-                    sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken)
+                    sources = await GetChannelMediaSources(item, cancellationToken)
                         .ConfigureAwait(false);
                 }
             }
@@ -121,10 +133,200 @@ namespace Jellyfin.LiveTv
             var keys = openToken.Split(StreamIdDelimiter, 3);
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
 
-            var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
+            var info = await GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
             var liveStream = info.Item2;
 
             return liveStream;
         }
+
+        private static void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
+        {
+            // Not all of the plugins are setting this
+            mediaSource.IsInfiniteStream = true;
+
+            if (mediaSource.MediaStreams.Count == 0)
+            {
+                if (isVideo)
+                {
+                    mediaSource.MediaStreams = new[]
+                    {
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Video,
+                            // Set the index to -1 because we don't know the exact index of the video stream within the container
+                            Index = -1,
+                            // Set to true if unknown to enable deinterlacing
+                            IsInterlaced = true
+                        },
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Audio,
+                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
+                            Index = -1
+                        }
+                    };
+                }
+                else
+                {
+                    mediaSource.MediaStreams = new[]
+                    {
+                        new MediaStream
+                        {
+                            Type = MediaStreamType.Audio,
+                            // Set the index to -1 because we don't know the exact index of the audio stream within the container
+                            Index = -1
+                        }
+                    };
+                }
+            }
+
+            // Clean some bad data coming from providers
+            foreach (var stream in mediaSource.MediaStreams)
+            {
+                if (stream.BitRate is <= 0)
+                {
+                    stream.BitRate = null;
+                }
+
+                if (stream.Channels is <= 0)
+                {
+                    stream.Channels = null;
+                }
+
+                if (stream.AverageFrameRate is <= 0)
+                {
+                    stream.AverageFrameRate = null;
+                }
+
+                if (stream.RealFrameRate is <= 0)
+                {
+                    stream.RealFrameRate = null;
+                }
+
+                if (stream.Width is <= 0)
+                {
+                    stream.Width = null;
+                }
+
+                if (stream.Height is <= 0)
+                {
+                    stream.Height = null;
+                }
+
+                if (stream.SampleRate is <= 0)
+                {
+                    stream.SampleRate = null;
+                }
+
+                if (stream.Level is <= 0)
+                {
+                    stream.Level = null;
+                }
+            }
+
+            var indexCount = mediaSource.MediaStreams.Select(i => i.Index).Distinct().Count();
+
+            // If there are duplicate stream indexes, set them all to unknown
+            if (indexCount != mediaSource.MediaStreams.Count)
+            {
+                foreach (var stream in mediaSource.MediaStreams)
+                {
+                    stream.Index = -1;
+                }
+            }
+
+            // Set the total bitrate if not already supplied
+            mediaSource.InferTotalBitrate();
+
+            if (service is not DefaultLiveTvService)
+            {
+                mediaSource.SupportsTranscoding = true;
+                foreach (var stream in mediaSource.MediaStreams)
+                {
+                    if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize))
+                    {
+                        stream.NalLengthSize = "0";
+                    }
+
+                    if (stream.Type == MediaStreamType.Video)
+                    {
+                        stream.IsInterlaced = true;
+                    }
+                }
+            }
+        }
+
+        private async Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(
+            string id,
+            string mediaSourceId,
+            List<ILiveStream> currentLiveStreams,
+            CancellationToken cancellationToken)
+        {
+            if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+            {
+                mediaSourceId = null;
+            }
+
+            var channel = (LiveTvChannel)_libraryManager.GetItemById(id);
+
+            bool isVideo = channel.ChannelType == ChannelType.TV;
+            var service = GetService(channel.ServiceName);
+            _logger.LogInformation("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
+
+            MediaSourceInfo info;
+#pragma warning disable CA1859 // TODO: Analyzer bug?
+            ILiveStream liveStream;
+#pragma warning restore CA1859
+            if (service is ISupportsDirectStreamProvider supportsManagedStream)
+            {
+                liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
+                info = liveStream.MediaSource;
+            }
+            else
+            {
+                info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+                var openedId = info.Id;
+                Func<Task> closeFn = () => service.CloseLiveStream(openedId, CancellationToken.None);
+
+                liveStream = new ExclusiveLiveStream(info, closeFn);
+
+                var startTime = DateTime.UtcNow;
+                await liveStream.Open(cancellationToken).ConfigureAwait(false);
+                var endTime = DateTime.UtcNow;
+                _logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
+            }
+
+            info.RequiresClosing = true;
+
+            var idPrefix = service.GetType().FullName!.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_";
+
+            info.LiveStreamId = idPrefix + info.Id;
+
+            Normalize(info, service, isVideo);
+
+            return new Tuple<MediaSourceInfo, ILiveStream>(info, liveStream);
+        }
+
+        private async Task<List<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken)
+        {
+            var baseItem = (LiveTvChannel)item;
+            var service = GetService(baseItem.ServiceName);
+
+            var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
+            if (sources.Count == 0)
+            {
+                throw new NotImplementedException();
+            }
+
+            foreach (var source in sources)
+            {
+                Normalize(source, service, baseItem.ChannelType == ChannelType.TV);
+            }
+
+            return sources;
+        }
+
+        private ILiveTvService GetService(string name)
+            => _services.First(service => string.Equals(service.Name, name, StringComparison.OrdinalIgnoreCase));
     }
 }

+ 1 - 8
src/Jellyfin.LiveTv/EmbyTV/RecordingHelper.cs → src/Jellyfin.LiveTv/Recordings/RecordingHelper.cs

@@ -1,19 +1,12 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Globalization;
 using System.Text;
 using MediaBrowser.Controller.LiveTv;
 
-namespace Jellyfin.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.Recordings
 {
     internal static class RecordingHelper
     {
-        public static DateTime GetStartTime(TimerInfo timer)
-        {
-            return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds);
-        }
-
         public static string GetRecordingName(TimerInfo info)
         {
             var name = info.Name;

+ 1 - 1
src/Jellyfin.LiveTv/RecordingNotifier.cs → src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs

@@ -11,7 +11,7 @@ using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
-namespace Jellyfin.LiveTv
+namespace Jellyfin.LiveTv.Recordings
 {
     /// <summary>
     /// <see cref="IHostedService"/> responsible for notifying users when a LiveTV recording is completed.

+ 5 - 5
src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs → src/Jellyfin.LiveTv/Recordings/RecordingsHost.cs

@@ -4,22 +4,22 @@ using Jellyfin.LiveTv.Timers;
 using MediaBrowser.Controller.LiveTv;
 using Microsoft.Extensions.Hosting;
 
-namespace Jellyfin.LiveTv.EmbyTV;
+namespace Jellyfin.LiveTv.Recordings;
 
 /// <summary>
-/// <see cref="IHostedService"/> responsible for initializing Live TV.
+/// <see cref="IHostedService"/> responsible for Live TV recordings.
 /// </summary>
-public sealed class LiveTvHost : IHostedService
+public sealed class RecordingsHost : IHostedService
 {
     private readonly IRecordingsManager _recordingsManager;
     private readonly TimerManager _timerManager;
 
     /// <summary>
-    /// Initializes a new instance of the <see cref="LiveTvHost"/> class.
+    /// Initializes a new instance of the <see cref="RecordingsHost"/> class.
     /// </summary>
     /// <param name="recordingsManager">The <see cref="IRecordingsManager"/>.</param>
     /// <param name="timerManager">The <see cref="TimerManager"/>.</param>
-    public LiveTvHost(IRecordingsManager recordingsManager, TimerManager timerManager)
+    public RecordingsHost(IRecordingsManager recordingsManager, TimerManager timerManager)
     {
         _recordingsManager = recordingsManager;
         _timerManager = timerManager;

+ 0 - 1
src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs

@@ -11,7 +11,6 @@ using System.Threading.Tasks;
 using AsyncKeyedLock;
 using Jellyfin.Data.Enums;
 using Jellyfin.LiveTv.Configuration;
-using Jellyfin.LiveTv.EmbyTV;
 using Jellyfin.LiveTv.IO;
 using Jellyfin.LiveTv.Timers;
 using MediaBrowser.Common.Configuration;

+ 0 - 1
src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs

@@ -9,7 +9,6 @@ using System.Xml;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.LiveTv.Configuration;
-using Jellyfin.LiveTv.EmbyTV;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;

+ 2 - 2
src/Jellyfin.LiveTv/Timers/TimerManager.cs

@@ -7,7 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Data.Events;
-using Jellyfin.LiveTv.EmbyTV;
+using Jellyfin.LiveTv.Recordings;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.LiveTv;
@@ -95,7 +95,7 @@ namespace Jellyfin.LiveTv.Timers
                 return;
             }
 
-            var startDate = RecordingHelper.GetStartTime(item);
+            var startDate = item.StartDate.AddSeconds(-item.PrePaddingSeconds);
             var now = DateTime.UtcNow;
 
             if (startDate < now)

+ 1 - 1
tests/Jellyfin.LiveTv.Tests/RecordingHelperTests.cs

@@ -1,5 +1,5 @@
 using System;
-using Jellyfin.LiveTv.EmbyTV;
+using Jellyfin.LiveTv.Recordings;
 using MediaBrowser.Controller.LiveTv;
 using Xunit;