|
@@ -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));
|
|
|
}
|
|
|
}
|