mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-07 06:43:26 +01:00
move classes to portable
This commit is contained in:
@@ -1,249 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public abstract class BaseTunerHost
|
||||
{
|
||||
protected readonly IServerConfigurationManager Config;
|
||||
protected readonly ILogger Logger;
|
||||
protected IJsonSerializer JsonSerializer;
|
||||
protected readonly IMediaEncoder MediaEncoder;
|
||||
|
||||
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
|
||||
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
Config = config;
|
||||
Logger = logger;
|
||||
JsonSerializer = jsonSerializer;
|
||||
MediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
protected abstract Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
|
||||
public abstract string Type { get; }
|
||||
|
||||
public async Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
|
||||
{
|
||||
ChannelCache cache = null;
|
||||
var key = tuner.Id;
|
||||
|
||||
if (enableCache && !string.IsNullOrWhiteSpace(key) && _channelCache.TryGetValue(key, out cache))
|
||||
{
|
||||
if (DateTime.UtcNow - cache.Date < TimeSpan.FromMinutes(60))
|
||||
{
|
||||
return cache.Channels.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
|
||||
var list = result.ToList();
|
||||
Logger.Debug("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(key) && list.Count > 0)
|
||||
{
|
||||
cache = cache ?? new ChannelCache();
|
||||
cache.Date = DateTime.UtcNow;
|
||||
cache.Channels = list;
|
||||
_channelCache.AddOrUpdate(key, cache, (k, v) => cache);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected virtual List<TunerHostInfo> GetTunerHosts()
|
||||
{
|
||||
return GetConfiguration().TunerHosts
|
||||
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<ChannelInfo>();
|
||||
|
||||
var hosts = GetTunerHosts();
|
||||
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var channels = await GetChannels(host, enableCache, cancellationToken).ConfigureAwait(false);
|
||||
var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
list.AddRange(newChannels);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error getting channel list", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
|
||||
|
||||
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsValidChannelId(channelId))
|
||||
{
|
||||
var hosts = GetTunerHosts();
|
||||
|
||||
var hostsWithChannel = new List<TunerHostInfo>();
|
||||
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
hostsWithChannel.Add(host);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error getting channels", ex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var host in hostsWithChannel)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check to make sure the tuner is available
|
||||
// If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
|
||||
if (hostsWithChannel.Count > 1 &&
|
||||
!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Logger.Error("Tuner is not currently available");
|
||||
continue;
|
||||
}
|
||||
|
||||
var mediaSources = await GetChannelStreamMediaSources(host, channelId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Prefix the id with the host Id so that we can easily find it
|
||||
foreach (var mediaSource in mediaSources)
|
||||
{
|
||||
mediaSource.Id = host.Id + mediaSource.Id;
|
||||
}
|
||||
|
||||
return mediaSources;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error opening tuner", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new List<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
|
||||
|
||||
public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsValidChannelId(channelId))
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
var hosts = GetTunerHosts();
|
||||
|
||||
var hostsWithChannel = new List<TunerHostInfo>();
|
||||
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(streamId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
hostsWithChannel.Add(host);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error getting channels", ex);
|
||||
}
|
||||
}
|
||||
else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hostsWithChannel = new List<TunerHostInfo> { host };
|
||||
streamId = streamId.Substring(host.Id.Length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var host in hostsWithChannel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
|
||||
await liveStream.Open(cancellationToken).ConfigureAwait(false);
|
||||
return liveStream;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error opening tuner", ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LiveTvConflictException();
|
||||
}
|
||||
|
||||
protected virtual bool EnableMediaProbing
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await IsAvailableInternal(tuner, channelId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error checking tuner availability", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
|
||||
|
||||
protected abstract bool IsValidChannelId(string channelId);
|
||||
|
||||
protected LiveTvOptions GetConfiguration()
|
||||
{
|
||||
return Config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
|
||||
private class ChannelCache
|
||||
{
|
||||
public DateTime Date;
|
||||
public List<ChannelInfo> Channels;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunDiscovery : IServerEntryPoint
|
||||
{
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IJsonSerializer _json;
|
||||
|
||||
public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json)
|
||||
{
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_liveTvManager = liveTvManager;
|
||||
_httpClient = httpClient;
|
||||
_json = json;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
|
||||
}
|
||||
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
string server = null;
|
||||
var info = e.Argument;
|
||||
|
||||
if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
string location;
|
||||
if (info.Headers.TryGetValue("Location", out location))
|
||||
{
|
||||
//_logger.Debug("HdHomerun found at {0}", location);
|
||||
|
||||
// Just get the beginning of the url
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(location, UriKind.Absolute, out uri))
|
||||
{
|
||||
var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd('/');
|
||||
|
||||
//_logger.Debug("HdHomerun api url: {0}", apiUrl);
|
||||
AddDevice(apiUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddDevice(string url)
|
||||
{
|
||||
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var options = GetConfiguration();
|
||||
|
||||
if (options.TunerHosts.Any(i =>
|
||||
string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
|
||||
UriEquals(i.Url, url)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip off the port
|
||||
url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
|
||||
|
||||
// Test it by pulling down the lineup
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", url),
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = _json.DeserializeFromStream<HdHomerunHost.DiscoverResponse>(stream);
|
||||
|
||||
var existing = GetConfiguration().TunerHosts
|
||||
.FirstOrDefault(i => string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.DeviceId, response.DeviceID, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
await _liveTvManager.SaveTunerHost(new TunerHostInfo
|
||||
{
|
||||
Type = HdHomerunHost.DeviceType,
|
||||
Url = url,
|
||||
DataVersion = 1,
|
||||
DeviceId = response.DeviceID
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.Equals(existing.Url, url, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
existing.Url = url;
|
||||
await _liveTvManager.SaveTunerHost(existing).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error saving device", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private bool UriEquals(string savedUri, string location)
|
||||
{
|
||||
return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string NormalizeUrl(string url)
|
||||
{
|
||||
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url = "http://" + url;
|
||||
}
|
||||
|
||||
url = url.TrimEnd('/');
|
||||
|
||||
// Strip off the port
|
||||
return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
|
||||
}
|
||||
|
||||
private LiveTvOptions GetConfiguration()
|
||||
{
|
||||
return _config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,570 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "HD Homerun"; }
|
||||
}
|
||||
|
||||
public override string Type
|
||||
{
|
||||
get { return DeviceType; }
|
||||
}
|
||||
|
||||
public static string DeviceType
|
||||
{
|
||||
get { return "hdhomerun"; }
|
||||
}
|
||||
|
||||
private const string ChannelIdPrefix = "hdhr_";
|
||||
|
||||
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||
{
|
||||
var id = ChannelIdPrefix + i.GuideNumber.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
if (info.DataVersion >= 1)
|
||||
{
|
||||
id += '_' + (i.GuideName ?? string.Empty).GetMD5().ToString("N");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/lineup.json", GetApiUrl(info, false)),
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
};
|
||||
using (var stream = await _httpClient.Get(options))
|
||||
{
|
||||
var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>();
|
||||
|
||||
if (info.ImportFavoritesOnly)
|
||||
{
|
||||
lineup = lineup.Where(i => i.Favorite).ToList();
|
||||
}
|
||||
|
||||
return lineup.Where(i => !i.DRM).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return lineup.Select(i => new ChannelInfo
|
||||
{
|
||||
Name = i.GuideName,
|
||||
Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
|
||||
Id = GetChannelId(info, i),
|
||||
IsFavorite = i.Favorite,
|
||||
TunerHostId = info.Id,
|
||||
IsHD = i.HD == 1,
|
||||
AudioCodec = i.AudioCodec,
|
||||
VideoCodec = i.VideoCodec
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||
private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_modelCache)
|
||||
{
|
||||
DiscoverResponse response;
|
||||
if (_modelCache.TryGetValue(info.Url, out response))
|
||||
{
|
||||
return response.ModelNumber;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions()
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
|
||||
CancellationToken = cancellationToken,
|
||||
CacheLength = TimeSpan.FromDays(1),
|
||||
CacheMode = CacheMode.Unconditional,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
||||
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache[info.Id] = response;
|
||||
}
|
||||
|
||||
return response.ModelNumber;
|
||||
}
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
var defaultValue = "HDHR";
|
||||
// HDHR4 doesn't have this api
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache[info.Id] = new DiscoverResponse
|
||||
{
|
||||
ModelNumber = defaultValue
|
||||
};
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions()
|
||||
{
|
||||
Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)),
|
||||
CancellationToken = cancellationToken,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var tuners = new List<LiveTvTunerInfo>();
|
||||
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
|
||||
{
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
string line = StripXML(sr.ReadLine());
|
||||
if (line.Contains("Channel"))
|
||||
{
|
||||
LiveTvTunerStatus status;
|
||||
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||
var name = line.Substring(0, index - 1);
|
||||
var currentChannel = line.Substring(index + 7);
|
||||
if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
{
|
||||
Name = name,
|
||||
SourceType = string.IsNullOrWhiteSpace(model) ? Name : model,
|
||||
ProgramName = currentChannel,
|
||||
Status = status
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return tuners;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<LiveTvTunerInfo>();
|
||||
|
||||
foreach (var host in GetConfiguration().TunerHosts
|
||||
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
try
|
||||
{
|
||||
list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error getting tuner info", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private string GetApiUrl(TunerHostInfo info, bool isPlayback)
|
||||
{
|
||||
var url = info.Url;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
throw new ArgumentException("Invalid tuner info");
|
||||
}
|
||||
|
||||
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url = "http://" + url;
|
||||
}
|
||||
|
||||
var uri = new Uri(url);
|
||||
|
||||
if (isPlayback)
|
||||
{
|
||||
var builder = new UriBuilder(uri);
|
||||
builder.Port = 5004;
|
||||
uri = builder.Uri;
|
||||
}
|
||||
|
||||
return uri.AbsoluteUri.TrimEnd('/');
|
||||
}
|
||||
|
||||
private static string StripXML(string source)
|
||||
{
|
||||
char[] buffer = new char[source.Length];
|
||||
int bufferIndex = 0;
|
||||
bool inside = false;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
char let = source[i];
|
||||
if (let == '<')
|
||||
{
|
||||
inside = true;
|
||||
continue;
|
||||
}
|
||||
if (let == '>')
|
||||
{
|
||||
inside = false;
|
||||
continue;
|
||||
}
|
||||
if (!inside)
|
||||
{
|
||||
buffer[bufferIndex] = let;
|
||||
bufferIndex++;
|
||||
}
|
||||
}
|
||||
return new string(buffer, 0, bufferIndex);
|
||||
}
|
||||
|
||||
private class Channels
|
||||
{
|
||||
public string GuideNumber { get; set; }
|
||||
public string GuideName { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
public string URL { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool DRM { get; set; }
|
||||
public int HD { get; set; }
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo> GetMediaSource(TunerHostInfo info, string channelId, string profile)
|
||||
{
|
||||
int? width = null;
|
||||
int? height = null;
|
||||
bool isInterlaced = true;
|
||||
string videoCodec = null;
|
||||
string audioCodec = "ac3";
|
||||
|
||||
int? videoBitrate = null;
|
||||
int? audioBitrate = null;
|
||||
|
||||
if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
width = 1280;
|
||||
height = 720;
|
||||
isInterlaced = false;
|
||||
videoCodec = "h264";
|
||||
videoBitrate = 2000000;
|
||||
}
|
||||
else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
width = 1920;
|
||||
height = 1080;
|
||||
isInterlaced = false;
|
||||
videoCodec = "h264";
|
||||
videoBitrate = 15000000;
|
||||
}
|
||||
else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
width = 960;
|
||||
height = 546;
|
||||
isInterlaced = false;
|
||||
videoCodec = "h264";
|
||||
videoBitrate = 2500000;
|
||||
}
|
||||
else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
width = 848;
|
||||
height = 480;
|
||||
isInterlaced = false;
|
||||
videoCodec = "h264";
|
||||
videoBitrate = 2000000;
|
||||
}
|
||||
else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
width = 640;
|
||||
height = 360;
|
||||
isInterlaced = false;
|
||||
videoCodec = "h264";
|
||||
videoBitrate = 1500000;
|
||||
}
|
||||
else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
width = 432;
|
||||
height = 240;
|
||||
isInterlaced = false;
|
||||
videoCodec = "h264";
|
||||
videoBitrate = 1000000;
|
||||
}
|
||||
|
||||
var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
|
||||
var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
|
||||
if (channel != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(videoCodec))
|
||||
{
|
||||
videoCodec = channel.VideoCodec;
|
||||
}
|
||||
audioCodec = channel.AudioCodec;
|
||||
|
||||
if (!videoBitrate.HasValue)
|
||||
{
|
||||
videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000;
|
||||
}
|
||||
audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
|
||||
}
|
||||
|
||||
// normalize
|
||||
if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
videoCodec = "mpeg2video";
|
||||
}
|
||||
|
||||
string nal = null;
|
||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
nal = "0";
|
||||
}
|
||||
|
||||
var url = GetApiUrl(info, true) + "/auto/v" + channelId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url += "?transcode=" + profile;
|
||||
}
|
||||
|
||||
var id = profile;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = "native";
|
||||
}
|
||||
id += "_" + url.GetMD5().ToString("N");
|
||||
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = url,
|
||||
Protocol = MediaProtocol.Http,
|
||||
MediaStreams = new List<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,
|
||||
IsInterlaced = isInterlaced,
|
||||
Codec = videoCodec,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = videoBitrate,
|
||||
NalLengthSize = nal
|
||||
|
||||
},
|
||||
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,
|
||||
Codec = audioCodec,
|
||||
BitRate = audioBitrate
|
||||
}
|
||||
},
|
||||
RequiresOpening = true,
|
||||
RequiresClosing = false,
|
||||
BufferMs = 0,
|
||||
Container = "ts",
|
||||
Id = id,
|
||||
SupportsDirectPlay = false,
|
||||
SupportsDirectStream = true,
|
||||
SupportsTranscoding = true,
|
||||
IsInfiniteStream = true
|
||||
};
|
||||
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
protected EncodingOptions GetEncodingOptions()
|
||||
{
|
||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
||||
}
|
||||
|
||||
private string GetHdHrIdFromChannelId(string channelId)
|
||||
{
|
||||
return channelId.Split('_')[1];
|
||||
}
|
||||
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<MediaSourceInfo>();
|
||||
|
||||
if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
var hdhrId = GetHdHrIdFromChannelId(channelId);
|
||||
|
||||
list.Add(await GetMediaSource(info, hdhrId, "native").ConfigureAwait(false));
|
||||
|
||||
try
|
||||
{
|
||||
if (info.AllowHWTranscoding)
|
||||
{
|
||||
string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
|
||||
model = model ?? string.Empty;
|
||||
|
||||
if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false));
|
||||
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override bool IsValidChannelId(string channelId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(channelId))
|
||||
{
|
||||
throw new ArgumentNullException("channelId");
|
||||
}
|
||||
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = streamId.Split('_')[0];
|
||||
|
||||
Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
|
||||
|
||||
if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Channel not found");
|
||||
}
|
||||
var hdhrId = GetHdHrIdFromChannelId(channelId);
|
||||
|
||||
var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false);
|
||||
|
||||
var liveStream = new HdHomerunLiveStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
|
||||
liveStream.EnableStreamSharing = true;
|
||||
return liveStream;
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
if (!info.IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Test it by pulling down the lineup
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
||||
|
||||
info.DeviceId = response.DeviceID;
|
||||
}
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
// HDHR4 doesn't have this api
|
||||
return;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return info.Any(i => i.Status == LiveTvTunerStatus.Available);
|
||||
}
|
||||
|
||||
public class DiscoverResponse
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
public string ModelNumber { get; set; }
|
||||
public string FirmwareName { get; set; }
|
||||
public string FirmwareVersion { get; set; }
|
||||
public string DeviceID { get; set; }
|
||||
public string DeviceAuth { get; set; }
|
||||
public string BaseURL { get; set; }
|
||||
public string LineupURL { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource();
|
||||
private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>();
|
||||
private readonly MulticastStream _multicastStream;
|
||||
|
||||
|
||||
public HdHomerunLiveStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
|
||||
: base(mediaSource)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_appHost = appHost;
|
||||
OriginalStreamId = originalStreamId;
|
||||
_multicastStream = new MulticastStream(_logger);
|
||||
}
|
||||
|
||||
protected override async Task OpenInternal(CancellationToken openCancellationToken)
|
||||
{
|
||||
_liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var mediaSource = OriginalMediaSource;
|
||||
|
||||
var url = mediaSource.Path;
|
||||
|
||||
_logger.Info("Opening HDHR Live stream from {0}", url);
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
StartStreaming(url, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
|
||||
|
||||
//OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||
//OpenedMediaSource.Path = tempFile;
|
||||
//OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||
|
||||
OpenedMediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
OpenedMediaSource.Protocol = MediaProtocol.Http;
|
||||
OpenedMediaSource.SupportsDirectPlay = false;
|
||||
OpenedMediaSource.SupportsDirectStream = true;
|
||||
OpenedMediaSource.SupportsTranscoding = true;
|
||||
|
||||
await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
|
||||
//await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override Task Close()
|
||||
{
|
||||
_logger.Info("Closing HDHR live stream");
|
||||
_liveStreamCancellationTokenSource.Cancel();
|
||||
|
||||
return _liveStreamTaskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private async Task StartStreaming(string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var isFirstAttempt = true;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var response = await _httpClient.SendAsync(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
|
||||
}, "GET").ConfigureAwait(false))
|
||||
{
|
||||
_logger.Info("Opened HDHR stream from {0}", url);
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Info("Beginning multicastStream.CopyUntilCancelled");
|
||||
|
||||
Action onStarted = null;
|
||||
if (isFirstAttempt)
|
||||
{
|
||||
onStarted = () => openTaskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
|
||||
await _multicastStream.CopyUntilCancelled(response.Content, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (isFirstAttempt)
|
||||
{
|
||||
_logger.ErrorException("Error opening live stream:", ex);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.ErrorException("Error copying live stream, will reopen", ex);
|
||||
}
|
||||
|
||||
isFirstAttempt = false;
|
||||
}
|
||||
|
||||
_liveStreamTaskCompletionSource.TrySetResult(true);
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
return _multicastStream.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public override string Type
|
||||
{
|
||||
get { return "m3u"; }
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "M3U Tuner"; }
|
||||
}
|
||||
|
||||
private const string ChannelIdPrefix = "m3u_";
|
||||
|
||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
{
|
||||
var list = GetTunerHosts()
|
||||
.Select(i => new LiveTvTunerInfo()
|
||||
{
|
||||
Name = Name,
|
||||
SourceType = Type,
|
||||
Status = LiveTvTunerStatus.Available,
|
||||
Id = i.Url.GetMD5().ToString("N"),
|
||||
Url = i.Url
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var liveStream = new LiveStream(sources.First());
|
||||
return liveStream;
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
using (var stream = await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsValidChannelId(string channelId)
|
||||
{
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = info.Url.GetMD5().ToString("N");
|
||||
var prefix = ChannelIdPrefix + urlHash;
|
||||
if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false);
|
||||
var m3uchannels = channels.Cast<M3UChannel>();
|
||||
var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
|
||||
if (channel != null)
|
||||
{
|
||||
var path = channel.Path;
|
||||
MediaProtocol protocol = MediaProtocol.File;
|
||||
if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Http;
|
||||
}
|
||||
else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Rtmp;
|
||||
}
|
||||
else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Rtsp;
|
||||
}
|
||||
else if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Udp;
|
||||
}
|
||||
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = channel.Path,
|
||||
Protocol = protocol,
|
||||
MediaStreams = new List<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,
|
||||
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
|
||||
|
||||
}
|
||||
},
|
||||
RequiresOpening = false,
|
||||
RequiresClosing = false,
|
||||
|
||||
ReadAtNativeFramerate = false,
|
||||
|
||||
Id = channel.Path.GetMD5().ToString("N"),
|
||||
IsInfiniteStream = true
|
||||
};
|
||||
|
||||
return new List<MediaSourceInfo> { mediaSource };
|
||||
}
|
||||
return new List<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class M3uParser
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = url.GetMD5().ToString("N");
|
||||
|
||||
// Read the file and display it line by line.
|
||||
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
return GetChannels(reader, urlHash, channelIdPrefix, tunerHostId);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken,
|
||||
// Some data providers will require a user agent
|
||||
UserAgent = _appHost.FriendlyName + "/" + _appHost.ApplicationVersion
|
||||
});
|
||||
}
|
||||
return Task.FromResult(_fileSystem.OpenRead(url));
|
||||
}
|
||||
|
||||
private List<M3UChannel> GetChannels(StreamReader reader, string urlHash, string channelIdPrefix, string tunerHostId)
|
||||
{
|
||||
var channels = new List<M3UChannel>();
|
||||
string line;
|
||||
string extInf = "";
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
line = line.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
extInf = line.Substring(8).Trim();
|
||||
_logger.Info("Found m3u channel: {0}", extInf);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith("#", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var channel = GetChannelnfo(extInf, tunerHostId, line);
|
||||
channel.Id = channelIdPrefix + urlHash + line.GetMD5().ToString("N");
|
||||
channel.Path = line;
|
||||
channels.Add(channel);
|
||||
extInf = "";
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
private M3UChannel GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
|
||||
{
|
||||
var titleIndex = extInf.LastIndexOf(',');
|
||||
var channel = new M3UChannel();
|
||||
channel.TunerHostId = tunerHostId;
|
||||
|
||||
channel.Number = extInf.Trim().Split(' ')[0] ?? "0";
|
||||
channel.Name = extInf.Substring(titleIndex + 1);
|
||||
|
||||
//Check for channel number with the format from SatIp
|
||||
int number;
|
||||
var numberIndex = channel.Name.IndexOf('.');
|
||||
if (numberIndex > 0)
|
||||
{
|
||||
if (int.TryParse(channel.Name.Substring(0, numberIndex), out number))
|
||||
{
|
||||
channel.Number = number.ToString();
|
||||
channel.Name = channel.Name.Substring(numberIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(channel.Number, "-1", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(mediaUrl))
|
||||
{
|
||||
channel.Number = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
|
||||
}
|
||||
|
||||
if (string.Equals(channel.Number, "-1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
channel.Number = "0";
|
||||
}
|
||||
|
||||
channel.ImageUrl = FindProperty("tvg-logo", extInf);
|
||||
|
||||
var name = FindProperty("tvg-name", extInf);
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = FindProperty("tvg-id", extInf);
|
||||
}
|
||||
|
||||
channel.Name = name;
|
||||
|
||||
var numberString = FindProperty("tvg-id", extInf);
|
||||
if (string.IsNullOrWhiteSpace(numberString))
|
||||
{
|
||||
numberString = FindProperty("channel-id", extInf);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(numberString))
|
||||
{
|
||||
channel.Number = numberString;
|
||||
}
|
||||
|
||||
return channel;
|
||||
|
||||
}
|
||||
private string FindProperty(string property, string properties)
|
||||
{
|
||||
var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
|
||||
var matches = reg.Matches(properties);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (match.Groups[1].Value == property)
|
||||
{
|
||||
return match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class M3UChannel : ChannelInfo
|
||||
{
|
||||
public string Path { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class MulticastStream
|
||||
{
|
||||
private readonly List<QueueStream> _outputStreams = new List<QueueStream>();
|
||||
private const int BufferSize = 81920;
|
||||
private CancellationToken _cancellationToken;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MulticastStream(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task CopyUntilCancelled(Stream source, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
byte[] buffer = new byte[BufferSize];
|
||||
|
||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
byte[] copy = new byte[bytesRead];
|
||||
Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
|
||||
|
||||
List<QueueStream> streams = null;
|
||||
|
||||
lock (_outputStreams)
|
||||
{
|
||||
streams = _outputStreams.ToList();
|
||||
}
|
||||
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
stream.Queue(copy);
|
||||
}
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
var onStartedCopy = onStarted;
|
||||
onStarted = null;
|
||||
Task.Run(onStartedCopy);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task CopyToAsync(Stream stream)
|
||||
{
|
||||
var result = new QueueStream(stream, _logger)
|
||||
{
|
||||
OnFinished = OnFinished
|
||||
};
|
||||
|
||||
lock (_outputStreams)
|
||||
{
|
||||
_outputStreams.Add(result);
|
||||
}
|
||||
|
||||
result.Start(_cancellationToken);
|
||||
|
||||
return result.TaskCompletion.Task;
|
||||
}
|
||||
|
||||
public void RemoveOutputStream(QueueStream stream)
|
||||
{
|
||||
lock (_outputStreams)
|
||||
{
|
||||
_outputStreams.Remove(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFinished(QueueStream queueStream)
|
||||
{
|
||||
RemoveOutputStream(queueStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class QueueStream
|
||||
{
|
||||
private readonly Stream _outputStream;
|
||||
private readonly ConcurrentQueue<byte[]> _queue = new ConcurrentQueue<byte[]>();
|
||||
private CancellationToken _cancellationToken;
|
||||
public TaskCompletionSource<bool> TaskCompletion { get; private set; }
|
||||
|
||||
public Action<QueueStream> OnFinished { get; set; }
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public QueueStream(Stream outputStream, ILogger logger)
|
||||
{
|
||||
_outputStream = outputStream;
|
||||
_logger = logger;
|
||||
TaskCompletion = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
public void Queue(byte[] bytes)
|
||||
{
|
||||
_queue.Enqueue(bytes);
|
||||
}
|
||||
|
||||
public void Start(CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationToken = cancellationToken;
|
||||
Task.Run(() => StartInternal());
|
||||
}
|
||||
|
||||
private byte[] Dequeue()
|
||||
{
|
||||
byte[] bytes;
|
||||
if (_queue.TryDequeue(out bytes))
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task StartInternal()
|
||||
{
|
||||
var cancellationToken = _cancellationToken;
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var bytes = Dequeue();
|
||||
if (bytes != null)
|
||||
{
|
||||
await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
TaskCompletion.TrySetResult(true);
|
||||
_logger.Debug("QueueStream complete");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Debug("QueueStream cancelled");
|
||||
TaskCompletion.TrySetCanceled();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in QueueStream", ex);
|
||||
TaskCompletion.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnFinished != null)
|
||||
{
|
||||
OnFinished(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.LiveTv.TunerHosts;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@@ -20,7 +21,6 @@ using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user