diff options
Diffstat (limited to 'src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs')
| -rw-r--r-- | src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs | 220 |
1 files changed, 211 insertions, 9 deletions
diff --git a/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs b/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs index c6874e4dbc..40ac5ce0fd 100644 --- a/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs +++ b/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)); } } |
