mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Merge pull request #11054 from barronpm/livetv-mediasourceprovider
LiveTV MediaSourceProvider refactor
This commit is contained in:
commit
c72bd8a092
@ -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>();
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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,
|
@ -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");
|
||||
}
|
||||
}
|
@ -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>();
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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,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;
|
@ -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.
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,5 +1,5 @@
|
||||
using System;
|
||||
using Jellyfin.LiveTv.EmbyTV;
|
||||
using Jellyfin.LiveTv.Recordings;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using Xunit;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user